add embeddable-and-context-aware-web-pages
This commit is contained in:
154
blog/_posts/2013-05-07-embeddable-and-context-aware-web-pages.md
Normal file
154
blog/_posts/2013-05-07-embeddable-and-context-aware-web-pages.md
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
title: Embeddable and Context-Aware Web Pages
|
||||
layout: post
|
||||
tags: architecture http javascript symfony
|
||||
description: Embedding content in an absolutely relative manner.
|
||||
---
|
||||
|
||||
In my [symfony][5] website applications I frequently make multiple subrequests to reuse content from other controllers.
|
||||
For simple, non-dynamic content this is trivial, but when arguments can change data or when the browser may want to
|
||||
update those subrequests things start to get complicated. Usually it requires tying the logic of the subrequest
|
||||
controller in the main request controller (e.g. knowing that the `q` argument needs to be passed to the template, and
|
||||
then making sure the template passes it in the subrequest). I wanted to simplify it and get rid of those inner
|
||||
dependencies.
|
||||
|
||||
As an example, take a look at this [product search][1]. The [facets][2] and [results][3] are actually subrequests, but
|
||||
the main results content is taking advantage of the request design I implemented. My goals were:
|
||||
|
||||
* remove logic from controller code to keep them independent from each other,
|
||||
* pages work without JavaScript and without requiring newer browsers,
|
||||
* pages work the same whether it's a subrequest or a master request, and
|
||||
* any page should be capable of being a self-contained subrequest.
|
||||
|
||||
|
||||
### Steps
|
||||
|
||||
When a subrequest is self-contained, I call it a *subcontext*. These subcontext requests have an additional requirement
|
||||
of being publicly accessible. In the product search, the [results][3] page is publicly routed and all the pagination and
|
||||
view links will work properly within the `./results.html` page. This makes it easy for using XHR to load updated
|
||||
content.
|
||||
|
||||
Another minor piece of this design is that views don't need to be fully rendered. This means an Ajax request can ask for
|
||||
just the page content and exclude the typical header/footer. In [Twig][4] parlance it is a `frag_content` block which
|
||||
has all the useful content.
|
||||
|
||||
When it comes to passing query parameters down through subcontexts, I decided that each subcontext gets its own scoped
|
||||
variable. So whenever I render a subcontext in a template, I always specify a name for it. The name should be unique
|
||||
within the template context. In the product search example, the facets subcontext is named `f` and the results
|
||||
subcontext is named `r`. When a request arrives for `/?r[offset]=54`, the subrequest will arrive at the results
|
||||
controller looking like `/results.html?offset=54` (which is equivalent to navigating that page directly).
|
||||
|
||||
To keep track of the subcontext names, template content, query data, and relative locations I started using a custom
|
||||
request header named `tle-subcontext`. In practice it looks like:
|
||||
|
||||
tle-subcontext: r:content@/shop/search/availability/in-stock/?q=red
|
||||
|
||||
When that request header exists it means:
|
||||
|
||||
* we're within a subcontext named `r`,
|
||||
* we want to get the view fragment named `content`, and
|
||||
* the root URL we started at was `/shop/search/availability/in-stock/?q=red`.
|
||||
|
||||
Within the controller code that header information should not be relevant. In templating though it becomes useful for
|
||||
rewriting URLs. Whenever a template is going to give a link to itself, I wrap it in a custom `subcontext_rewrite`
|
||||
function. For example, given the `tle-subcontext` configuration above, it would rewrite:
|
||||
|
||||
dataset_generic(...snip...)
|
||||
=> /shop/.../in-stock/results.html?q=red&view=list-tn&offset=54
|
||||
|
||||
subcontext_rewrite(dataset_generic(...snip...))
|
||||
=> /shop/.../in-stock/?q=red&r[view]=list-tn&r[offset]=54#r
|
||||
|
||||
The rewritten URL is completely valid and can be accessed without fancy JavaScript calls. Now, to make that possible I
|
||||
don't use the standard inline renderer in Twig. I created a custom renderer with a little additional logic which takes
|
||||
care of rewriting the subcontext data and injecting the header:
|
||||
|
||||
{% highlight php %}
|
||||
$rootUri = $request->getRequestUri();
|
||||
|
||||
if (preg_match('/^([a-z0-9\-]+):([a-z0-9]+)@(.*)$/', $request->server->get('HTTP_TLE_SUBCONTEXT'), $match)) {
|
||||
# this means a subcontext already exists and a sub-subcontext is being created
|
||||
|
||||
# append our context name to the parent context name
|
||||
$options['name'] = $match[1] . '-' . $options['name'];
|
||||
|
||||
# use the root uri from the header since $request is only a subrequest
|
||||
$rootUri = $match[3];
|
||||
|
||||
# pull out our context-specific query data from the root uri and update our request
|
||||
parse_str(parse_url($match[3], PHP_URL_QUERY), $rootQuery);
|
||||
$subRequest->query->replace(isset($rootQuery[$options['name']]) ? $rootQuery[$options['name']] : array());
|
||||
} elseif ((null !== $subdata = $request->query->get($options['name'])) && (is_array($subdata))) {
|
||||
# pull out our context-specific query data
|
||||
$subRequest->query->replace($subdata);
|
||||
}
|
||||
|
||||
# now add the header with all our combined data to the request
|
||||
$subRequest->server->set(
|
||||
'HTTP_TLE_SUBCONTEXT',
|
||||
$options['name'] . ':' . (empty($options['frag']) ? 'content' : $options['frag']) . '@' . $rootUri
|
||||
);
|
||||
|
||||
unset($options['name'], $options['frag']);
|
||||
{% endhighlight %}
|
||||
|
||||
So now whenever I want a subcontext within a view, I can use the custom renderer:
|
||||
|
||||
{% highlight jinja %}{% raw %}
|
||||
{{ render_subcontext(path('search_results', passthru), { 'name' : 'r' }) }}
|
||||
{% endraw %}{% endhighlight %}
|
||||
|
||||
With those simple customizations I no longer have to worry about knowing what parameters need to be passed on to
|
||||
template subrequests. It also paves the way for some more fancy behavior...
|
||||
|
||||
|
||||
### Adding Some Magic
|
||||
|
||||
Since the subcontext pages are publicly accessible, it should be easy to let Ajax reload individual subcontexts without
|
||||
having to reload the whole page. To enable that, I went ahead and configured subcontext requests to always end up in a
|
||||
specific layout which will wrap it with the subcontext metadata. The template looks like:
|
||||
|
||||
{% highlight jinja %}{% raw %}
|
||||
<article id="{{ subcontext_name() }}" data-href="{{ app.request.uri }}#{{ subcontext_frag() }}">
|
||||
<header><h3>{{ block('def_title') }}</h3></header>
|
||||
<section>{{ block('frag_' ~ subcontext_frag()) }}</section>
|
||||
</article>
|
||||
{% endraw %}{% endhighlight %}
|
||||
|
||||
The `subcontext_*` custom functions simply peek at the request to find the `tle-subcontext` header and appropriate
|
||||
values.
|
||||
|
||||
Now that the extra data is available, we can have JavaScript build links to send partial requests. If a
|
||||
`subcontext_rewrite` link is clicked, it's a matter of starting with the `article[@data-href]` value and peeking at the
|
||||
clicked `a[@href]` to find query parameters that were within the `article[@id]` name. For example:
|
||||
|
||||
// window.location
|
||||
/shop/search/availability/in-stock/?q=red
|
||||
|
||||
// subcontext
|
||||
<article id="r" data-href="/shop/search/availability/in-stock/results.html?q=red#content">
|
||||
|
||||
// clicked anchor
|
||||
<a href="/shop/search/availability/in-stock/?q=red&r[offset]=54#r">
|
||||
|
||||
// becomes the request
|
||||
GET /shop/search/availability/in-stock/results.html?q=red&offset=54
|
||||
tle-subcontext: tle-subcontext: r:content@/shop/search/availability/in-stock/?q=red
|
||||
|
||||
Something easily processable with an Ajax request. And since the clicked anchor was a canonical URL, that easily becomes
|
||||
the new window URL location by using the [HTML5 History API][6].
|
||||
|
||||
|
||||
### Conclusion
|
||||
|
||||
Once I implemented the code snippets for tying all the ideas together, it became much quicker and simpler for me to
|
||||
embed other dynamic controllers within my requests. So far it has been working out quite well and I no longer have to
|
||||
worry abuot page-specific hacks for passing data to subrequests.
|
||||
|
||||
|
||||
[1]: http://www.theloopyewe.com/shop/search/availability/in-stock/?q=red
|
||||
[2]: http://www.theloopyewe.com/shop/search/availability/in-stock/facets.html?q=red
|
||||
[3]: http://www.theloopyewe.com/shop/search/availability/in-stock/results.html?q=red
|
||||
[4]: http://twig.sensiolabs.org/
|
||||
[5]: http://symfony.com/
|
||||
[6]: http://diveintohtml5.info/history.html
|
||||
Reference in New Issue
Block a user