Optimizing for less code with UJS and data-behavior
UJS is a small javascript library that manages interactions with endpoints implementing SJR (Server-generated Javascript Responses), in addition to providing a few behaviors such as non-GET HTTP verbs for links, simple confirm UIs, and the disabling of elements with optional loading text while remote requests are processing. UJS and SJR are a fantastic solution for keeping a web application’s frontend code minimal, in an age where wild over-engineering in this area is increasingly the norm.
UJS behavior binding is declarative, and happens via data attributes (for instance you can use the attribute-value pair data-disable-with='Processing'
to disable a submit button with the loading text “Processing” during a form POST).
For all that is great about UJS, one thing that it does not offer is any formal suggestion of a practice for introducing your own custom unobtrusive javascript behaviors to your application. For this we need to look beyond the library and to the broader developer community surrounding Rails and UJS. Javan Makhmali gave an excellent talk in which he features some screenshots of the Basecamp project tree and various code snippets. In this talk he introduces the convention of binding to custom unobtrusive javascript via data-behavior
attributes.
Consider an e-commerce application wherein we want to pop open a shopping cart and place it into a loading state on certain interactions (clicking an “Add to Cart” button, adjusting item quantities). This shared behavior can have a very minimal, unobtrusive javascript implementation such as the following:
1 2 3 4 5 6 |
# app/assets/javascripts/behaviors/element/cart_loading.coffee $ -> $(document).on 'ajax:send', '[data-behavior~=cart-loading]', -> $('#cart-dropdown').addClass('open').find('#cart').addClass('loading') .on 'ajaxSuccess', -> $('#cart').removeClass('loading') |
Note: we use jQuery’s ajaxSuccess
event for our success handling instead of UJS’s ajax:success
, because that event does not fire on elements if replaced by logic in SJR responses, and in this particular scenario we may want to bind to such elements.
We can now bind this behavior in a way which is both very readable, unambiguous and unobtrusive, i.e. from an “Add to Cart” link:
1 2 |
= link_to 'Add to Cart', line_items_path(line_item: {variant_id: @variant.id}),\ data: {method: :post, behavior: 'cart-loading', disable_with: 'Adding...'} |
…or from an item quantity control:
1 2 3 |
= link_to '+', line_item_path(line_item, line_item: {quantity: line_item.quantity + 1}), \ class: 'quantity-link', \ data: {remote: true, method: :patch, behavior: 'cart-loading'} |
I put together a small sample project demonstrating the custom UJS behavior described in this article, which you can find on GitHub. This project uses no javascript beyond stock UJS, SJR, and the simple 5-line snippet above, and it implements a UI which you can see in the video below:
I hope that this post, the above video and the linked demo project together give you a strong sense of just how much UJS, SJR and the data-behavior binding convention let you achieve while keeping your codebase both extremely minimal and highly legible, and that you keep these tools top-of-mind when evaluating possible paths for frontend architecture.
No Comments