In my team we are trying out new ways of describing interactive JavaScript behaviors on the page, without needing of writing the same code over and over again. At the same time we are trying to make the actions resulting from clicking this link or submitting that form obvious to anyone looking at the markup.
Why?
To reduce the need to repeat handling AJAX requests and to improve maintainability.
So let’s imagine you see some (ERB enhanced) HTML Markup like this:
<%= form_for(Comment.new) do |f| %> <%= f.text_area :content %> <button type="submit">Write comment</button> <% end %> |
You would expect that by writing something in the textarea and hitting the button, the page will be reloaded and you’ll be taken to a page, where you can see the comment after it was added. But now imagine that there some JavaScript code is loaded in a JS file that contains something like this:
$('#new_comment').on('submit', function(event) { // This will send the form to the server without reloading the page $.ajaxPost(/* do some stuff here to send the form */).done(function(data) { // data.html contains the html of the newly written comment $('body').append(data.html); }); event.preventDefault(); }); |
Without knowing that those lines exist, you would still assume that by sendind the form a reload will take place. But if you try it out, magic happens and you have to start looking into the code to find out where the things that happen are defined (which may or may not take some time depending on how good the structure of your code base is and how familiar you are with this structure – you may be a new guy in the team).
So if you are new to the team or are a backend developer, you may need some time to find out what happens, e.g. in the case of a bug. If you had another form somewhere, such as a form to write some new fancy post or upload an image, you’d be tempted to write the same kind of code again so this other form also feels more state of the art with AJAX and the like.
What?
(Don’t want to paste the code because it’s mostly bound to our internal libraries, but will explain how it is used and how it feels like.)
This was partially inspired by two-way data binding libraries like KnockoutJS or fully fledged frameworks like AngularJS.
This solution is currently being tested for genuine maintainability benefits. Let’s make a few changes to the comment form we defined above so it looks like this:
<%= form_for(Comment.new, :'data-instant-update' => JSON.generate({ appendBefore: '#new_comment' })) do |f| %> <%= f.text_area :content %> <button type="submit">Write comment</button> <% end %> |
What would you expect to happen on submitting the form now? Maybe you will be warned now, that this form is no longer a usual one. It’s generally good practice to use data attributes on HTML elements to add JavaScript behaviour to that particular element. Also KnocktoutJS and AngularJS are doing it – so the big players are doing it, so it must be good right? ;-) If someone explained to you in 2 minutes that this data-instant-update
attribute would trigger the form or link to be send via AJAX and, after doing that, respond with what you would tell it to (using a JSON syntax), you might well think that by write something and hitting the button the comment will magically appear above the form field you entered it.
So far, so good. Now everyone would know just from looking at the form that it will have AJAX behavior plugged in to it. But that’s not all you could do with this DSL. What if you want the comment field to remove the text you entered and highlight the comment you just appended, and maybe also increment a counter on the page?
The markup might well be transformed into something like this:
<%= form_for(Comment.new, :'data-instant-update' => JSON.generate({ appendBefore: { selector: '#new_comment', highlight: true }, reset: true, increment: { parent: '.comments-count' } })) do |f| %> <%= f.text_area :content %> <button type="submit">Write comment</button> <% end %> |
This kind of DSL has a huge scope and seems easily understandable. We tested it with new colleagues who saw the code for the first time, and they immediately grasped that something would happen and generally guessed what.
Learnings (so far)
It is really crucial to provide good errors (in cases you forget to specify something or something is not there etc), because no one wants to trace the origins of the errors. But providing good errors is easy as every action has a clearly defined behavior and checking the parameters should be relatively straight forward.
You should provide a consequent language. If you have some actions taking a parameter named selector
, you shouldn’t build another action that takes a parameter called element
, which is clearly doing the same thing. This makes it is easier for everyone to write an action without having to look into the specs while being confident that what they’re writing will work at the end.
This DSL is a great way to prototype interactions.
Discussing the pros & cons
Unfortunately this DSL has also some shortcomings, or, to put it another way, a well-defined scope. Implementing actions that need access to a lot of states or only apply only under certain circumstances requires more effort. In that case you should probably stick to regular class or module structured code.
Here are the benefits and shortcomings in a nutshell:
Pros:
- Far less repetition (DRYed-up code)
- Easy to read and understand
- Immediately visible what happens when triggering an element
- A subset of actions can also be used without AJAX, just with a click (
data-instant-click
)
Contras:
- At the moment not everything is possible ;-)
- It’s not that flexible (limited applicability)
- It’s not yet clear, how error handling could look like
- It’s hard to do sequential stuff (first do A, after A is done do B, and so on…)
What’s next?
First of all we need to overcome some of the cons listed above, such as dealing with 4xx responses. I imagine that having a second attribute like data-instant-fail
could solve this problem. But showing an error message on failure would also require a hook to hide the error message before the next AJAX request is sent. While writing that, I figured that performing actions sequentially could be easily done with delay parameters on all actions where that makes sense, or maybe by doing it the capistrano way and somehow defining before and after hooks?
There are a lot of ideas swirling around in my head as to where this kind of DSL can be useful or how it can be improved. What do you think?