A couple of weeks ago I hacked together a quick proof of concept of editing the same template for using on the client side and the server side with Drupal 8. It looked like this:
Sunday hack. Make #headlessdrupal use #twig for client side templates http://t.co/OQiVya0cu8 #drupal #drupaltwig.
— eiriksm (@orkj) January 4, 2015
If you click the link you can see an animated gif of how I edit the Bartik node template and it reflects in a simple single page app. Or one of these hip headless Drupal things, if you want.
So I thought I should do a quick write up on what it took to make it work, what disadvantages comes with it, what does not actually work, and so on. But then I thought to myself. Why not make a theme that incorporates my thoughts in my last post, "Headless Drupal with head fallback". So I ended up making a proof of concept that also is a live demo of a working Drupal 8 theme with the first page request rendered on the server, and the subsequent requests rendered fully client side. They both use the same node template for both full views and the node listing on the front page. So if you are eager and want to see that, this is the link.
Next, let's take a look at the inner workings:
Part 1: Twig js
Before I even started this, I had heard of twig.js. So my first thought was to just throw the Drupal templates to it, and see what happened.
Well, some small problems happened.
The first problem was that some of the filters and tags we have in Drupal is not supported out of the box by twig.js. Some of these are probably Drupal specific, and some are extensions that is not supported out of the box. One example is the tag {% trans %}
for translating text. But in general, this was not a big problem. Except that I did as I usually do when doing a POC. I just quickly threw together something that worked, resulting for example in that the trans tag just returns the original string. Which obviously is not the intended use for it. But at least now the templates could be rendered. Part one, complete.
Part 2: Enter REST
Next I needed to make sure I could request a node through the REST module, pass it to twig.js and render the same result as Drupal would do server side. This turned out to be the point where I ended up with the worst hacks. You see, ideally I would just have a JSON structure that represents the node, and pass it to twig.js. But there are a couple of obvious problems with that.
Consider this code (following examples are taken from the Bartik theme):
<a href="{{ url }}" rel="bookmark">{{ label }}</a>
This is unproblematic. If we have a node.url property and a node.label property on the object we send to twig.js, this would just work out of the box. Neither of these properties are available like that in the default REST response for a node, however, but a couple of assignments later, that problem went away as well.
Now, consider this:
{{ content|without('comment', 'links') }}
Let's start with the filter, "without". Well, at least that should be easy. We just need a filter that will make sure comment and links properties on the node.content object will not be printed here. No problem.
Now to the problem. The content variable here should include all the rendered fields of the node. As was the case of label and url, .content is not actually a property in the REST response either. This makes the default output from the REST module not so usable to us. Because to make it generic we would also have to know what fields to compose together to this .content property, and how to render them. So what then?
I'll just write a module, I thought. As I often do. Make it return more or less the render array, which I can pass directly to twig.js. So I started looking into what this looked like now, in Drupal 8. I started looking at how I could tweak the render array to look more or less like the least amount of data I needed to be able to render the node. I saw that I needed to recurse through the render array 0, 1 or 2 levels deep, depending on the properties. So I would get for example node.content with markup in all its children, but also node.label without children, just the actual title of the node. Which again made me start to hardcode things I did not want in the response, just like I just had started hardcoding things I wanted from the REST response.
So I gave up the module. After all this is just a hacked together POC, so I'll be frank about that part. And I went back to hardcoding it client side instead. Not really the most flexible solution, but at least - part two: complete.
Part 3: Putting the pieces together
Now, this was the easy part. I had a template function that could accept data. I had transformed the REST response into the pieces I needed for the template. The rest was just adding a couple of AJAX calls and some pushState for the history (which reminds me. This probably does not work in all browsers at all). And then bundling things together with some well known front-end tools. Of course, this is all in the repo if you want all the details.
Conclusions
Twig on the server and on the client. Enough said, right?
Well. The form this demo is now, this is not something you would just start to use. But hopefully get some ideas. Or inspiration. Or maybe inspire (and inform) me of the smartest way to return a "half-rendered render array".
Also, I would love to get some discussion going regarding how to use this approach in the most maintainable way.
Some thoughts on how I would improve this if I would actually use it:
- Request templates via ajax.
- Improve escaping.
- Incorporate it into a framework (right now it just vanilla js).
- Remove hacks, actually implement all the filters.
Finally: The code is up at github. There is a demo on a test site on pantheon. And huge props just mostly go out to both twig and twig js authors. Just another day standing on the shoulders of giants.
I'm going to end this blog post with a classy gif from back in the day. And although it does not apply in the same way these gifs were traditionally used, I think we can say that things said in this blog post are not set in stone, neither in regards to construction or architectural planning.
yched•Friday, Jan 23rd 2015 (over 9 years ago)
Interesting :-)
But yeah, the gap is that:
- the REST API is about raw field values, not HTML strings for display
- the "content" variable in entity templates expects a render array.
generating it from the raw values means running all configured field formatters,
reducing it down to HTML strings means drupal_render()
That's heavy logic that is only available on the server side. Duplicating all the formatters + the render API in JS-land doesn't seem like a scalable approach :-/
I guess part of this could be adressed by a separate, enriched API that returns the raw values but also:
- generates the $render array for the entity in a given view mode passed in the request
- renders all of it field-by-field (ie foreach (element_children($render)) { $content[$field_name] = drupal_render($render[$field_name]) } rather than just drupal_render($render)
(sad thing is that it would not benefit on the render cache)
- returns the $content array in the JSON
Then "granular rendering" in templates ({{content.field_name}}, {{content|without('foo', 'bar')}} works - limited to the first level within content, though.
I'm sure there would be more hurdles, like taking care of #attached JS & CSS and adding them to the page, running JS behaviors...
eiriksm•Friday, Jan 23rd 2015 (over 9 years ago)
Hey, thanks for commenting!
I completely agree with all your points. And reimplementing formatters + render API in JS sounds like madness :)
Your proposed solution would probably work more or less like the module I started to write (described in the post). One of the problems I faced, though, was that the response still ended up feeling somewhat bloated. But I guess that is the price of flexibility in such a way.
I don't think I mentioned it in this blog post (I know I mentioned it in the repo README), but at least this quick POC does not take care of behaviours (like quickedit for example) or css at all. So yes, also valid point.
I am leaning towards that this might be useful for reusing components (like a node component or a block component), and not entire themes with all of core's functionality. Then you could reuse templates, and worry about templates only (or, more or less templates only).