Robust AJAX UIs with jQuery-UJS and Server-Generated Javascript Responses
The jQuery-UJS (“Unobtrusive Javascript”) library has been around since 2010, making it quite old by the standards of frontend tooling, whose pace of evolution is swift. In many contexts, however, jQuery-UJS, along with complimentary Server-generated Javascript Responses (SJR) still surpass more modern frontend tooling and frameworks in metrics that should matter to anybody building a web app with dynamic UI requirements, even in 2016.
The most profound virtue of UJS is probably how palpably it minimizes your overall code surface area. AJAX bindings are made to the specification of certain data-attributes read off of elements in the DOM, which in practice oftentimes means a lot of boilerplate Javascript files you might otherwise have in your codebase you simply do not need. Furthermore, with UJS, most of the custom Javascript that you do execute to implement your rich user interactions is, by necessity, nicely compartmentalized in server-side templates oftentimes only 1-3 lines long, and very clearly named and situated.
In many contexts, jQuery-UJS, along with complimentary Server-generated Javascript Responses (SJR) still surpass more modern frontend tooling and frameworks in metrics that should matter to anybody building a web app with dynamic UI requirements, even in 2016.
That said, UJS and SJR are not a panacea. I have never worked a project that didn’t have several features that ultimately weren’t a fit. Still, for a non-negligible part of many applications’ AJAX-ey feature-set, SJR with UJS constitute an incredibly robust, minimal (by source line count), testable and maintainable solution.
Let’s look at a concrete example of an AJAX-ey UI implemented with SJR and UJS. The UI that we’re ultimately going to achieve is the following:
It is a simple AJAX interface to manage a list of notes. We can assume the following notes schema:
1 2 3 4 5 6 7 8 9 10 11 |
class DefineSchema < ActiveRecord::Migration[5.0] def change create_table :notes do |t| t.text :body t.integer :position t.timestamps end add_index :notes, :position end end |
And a simple model definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Note < ApplicationRecord validates :body, presence: true after_initialize :init scope :by_position, ->{ order(position: :asc) } private # default the "position" to the number of records in existence. # (i.e. the first record will have position = 0, second # will have position = 1, etc.) def init self.position = self.class.size if position.nil? end end |
We’ll implement our UI with 1 template and 2 partials. First, an “index” template:
1 2 3 4 5 6 7 8 9 |
<div class="row"> <div class="col-md-8 col-md-offset-2"> <h2>Notes</h2> <ul class="list-unstyled" id="notes" data-sortable-list="<%= positions_notes_path %>"> <%= render @notes %> </ul> <%= render 'form' %> </div> </div> |
This index template renders a collection of notes (for each, Rails render
method will render the _note.html.erb partial and pass the record) and a form partial. We will implement the “_note” partial like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<li class="note" id="<%= dom_id(note) %>"> <div class="edit-controls"> <%= link_to note_path(note), remote: true, class: 'destroy-link', remote: true, data: {method: :delete} do %> <i class="fa fa-trash-o"></i> <% end %> <%= link_to edit_note_path(note), remote: true, class: 'edit-link' do %> <i class="fa fa-pencil"></i> <% end %> </div> <div class="note-body"> <%= note.body %> </div> </li> |
…And the “_form” partial like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<%= simple_form_for (@note ||= Note.new), remote: true do |f| %> <%= f.input :body, as: :string, label: false, placeholder: 'Enter your note' %> <div class="update-links"> <%= button_tag type: :submit, class: 'btn btn-link submit-note pull-right' do %> <i class="fa fa-check"></i> <% end %> <% if @note.persisted? %> <%= link_to note_path(@note), remote: true, class: 'btn btn-link pull-right cancel-edit', title: 'Cancel edit', data: {toggle: 'tooltip'} do %> <i class="fa fa-times"></i> <% end %> <% end %> </div> <% end %> |
Hitting the index action and rendering the index template give us a nice empty state:
Now, let’s handle the create
action. We want the create
action to take what we input in the bottom form, validate it, render it into the note partial, append and highlight that partial, and clear the form. At this point let’s have a look at our create
controller action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# ... def create respond_to do |format| @note = Note.new(note_params) if @note.save format.js # default render (create.js.erb) else format.js { render 'new' } end end end # ... |
This controller action implementation should appear pretty idiomatic and familiar if you have worked in any capacity with Ruby on Rails. The only conspicuous deviation from a more traditional, non-SJR controller action implementation is what you find in the bodies of the respond_to blocks, which explicitly indicate “js” as a handled MIME type. The DOM updates that constitute our dynamic UI are found rather in our views directory. When we POST a note to the create action, for instance, in the success case we render the following JS snippet in create.js.erb:
1 2 3 |
$('#notes').append($("<%= j render(@note) %>")) $('#note_body').val('').parent('div.note_body').removeClass('has-error').find('.help-block').remove() $('#<%= dom_id(@note) %>').find('.note-body').effect('highlight', 800) |
This code renders our server-side “note” partial and appends it to our notes list. It then clears out the form, grabs the newly-appended note, and highlights its body using the jQuery-UI “highlight” effect.
The dom_id
method that you see being used to grab the DOM node is a stock helper from the actionview library which generates unique DOM-style IDs when passed an instance of an ActiveRecord model. If you look to the _note partial implementation further up you will see that the dom_id
helper was used to generate the id attribute set on the div defined within that partial. You can find documentation for dom_id
here.
Let’s now look at our failure case. In the case that validation of the user’s posted input fails, we render a new.js.erb js template:
1 |
$('#new_note').replaceWith('<%= j render("form") %>') |
As you can see, this js template simply replaces the in-page form with one that we render out anew with the present controller context. This form, as it’s built with a simple_form helper, will contain elements describing the error to the user.
Now that we’ve seen how we handle adding notes in the UI via AJAX, let’s have a look at how we can implement note editing and deletion features. The editing UI we aim to achieve is the following:
You’ll see that in our _note partial we have “edit” and “destroy” links constructed with the link_to
helper and with both invocations are passing the remote: true
option value (the “destroy” link has also been passed method: :delete
). These options instruct the UJS library preventDefault on any clicks of these elements and instead make an ajax request against the URI indicated in the href and eval the server response. At this point it would be helpful to fill out the rest of our controller class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
class NotesController < ApplicationController before_action :find_note, only: [:show, :edit, :update, :destroy] def index @notes = Note.by_position end def show # default render end def create respond_to do |format| @note = Note.new(note_params) if @note.save format.js # default render (create.js.erb) else format.js { render 'new' } end end end def edit # default render end def update respond_to do |format| if @note.update(note_params) format.js # default render (update.js.erb) else format.js { render 'edit' } end end end def destroy @note.destroy # default render end private def note_params params.fetch(:note, {}).permit(:body) end def find_note @note = Note.find(params[:id]) end end |
When we click the “edit” link, UJS will perform a GET request against the link URI, hitting the edit
controller action above, which will render this javascript snippet for evaluation:
1 |
$('#<%= dom_id(@note) %>').html($('<%= j render("form") %>')) |
This snippet simply grabs the note element in the DOM and swaps out its contents for our edit form. This form POSTs asynchronously to our update
action, which either replaces the form with a refreshed _note partial on success…
1 2 |
$('#<%= dom_id(@note) %>').replaceWith('<%= j render(@note) %>') $('#<%= dom_id(@note) %>').find('.note-body').effect('highlight', 800) |
…or replaces it with a re-rendered form (including validation errors) in the case of validation failure by re-rendering the edit
template.
Similarly, our destroy
action destroys the indicated record, then renders out a snippet which simply grabs the corresponding DOM element, fades it out and removes it from the DOM:
1 |
$('#<%= dom_id(@note) %>').fadeOut().remove() |
Non-UJS Features in a UJS Way
One feature that we do not get in jQuery-UJS but that would be great to have in our UI is drag-and-drop sorting. Let’s look at how we might implement this feature in an unobtrusive way that aligns with the conventions of UJS, introducing a minimum of bespoke scripting to our codebase. 2 key concepts in the jQuery-UJS library are (1) implicit binding based on data-attributes and (2) UI updates via the evaluation of Server-generated Javascript. We’ll try to adhere to these principles in the sortable-list implementation that we write:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
$.extend(true, window.App, {Views: {Common: {SortableList: {}}}}) class App.Views.Common.SortableList constructor: (@el) -> @update_url = @el.attr('data-sortable-list') @init_sortable() init_sortable: -> @el.sortable update: (e, ui) => data = { positions: [], target: @parse_dom_id($(ui.item)) } @el.find('li').each (index, element) => el_id = @parse_dom_id($(element)) data.positions.push({id: el_id, position: index}) $.ajax type: 'PATCH' url: @update_url data: data parse_dom_id: (el) => parts = el.attr('id').match(/[a-zA-Z\-\_\d]+_(\d+)$/) parts[parts.length - 1] $ -> $("[data-sortable-list]").each -> new App.Views.Common.SortableList($(this)) |
At a high level what we’ve done here is take any DOM node with a data-sortable-list
attribute on it and wrap it in an instance of a class (App.Views.Common.SortableList) that configures jQuery sortable for us. The jQuery-sortable integration is pretty stock. The only things worth noting are that we pull the URI to which we post sort-order changes off of the “data-sortable-list” attribute and we parse record IDs out of the list item id attributes based on the assumption that these list items are using the dom_id
helper to set these attributes. You will notice that we do no explicit eval
ing of the response to our PATCH requests, and this is because the jQuery UJS library does this eval
ing implicitly for any server response with a Content-Type of “text/javascript”.
Server-side, we handle PATCH requests with a positions
action that looks as follows:
1 2 3 4 5 6 7 8 9 10 11 |
def positions respond_to do |format| @target = Note.find(params[:target]) params[:positions].values.each do |param| Note.find(param[:id]).update!(position: param[:position]) end format.js # default render end end |
This action simply fetches the indicated “target” record (the record being dragged) into an instance variable so it is in context for the template, then iterates over the positions
hash that has been supplied to the action, grabbing Note records and updating their positions as specified by the hash. The positions.js.erb Javascript template that is rendered with this action simply grabs and highlights the target Note node:
1 |
$('#<%= dom_id(@target) %> .note-body').effect('highlight', 800) |
What we end up with is a drag-and drop sorting interface which is slick, functional and, from an implementation perspective, incredibly minimal and robust:
With that, we’ve covered the entirety of the features and interactions we’d set out to. This walk-through can be downloaded in a complete and runnable form from https://github.com/hackernotes/ujs-demo. I hope this walk-through has done a decent job of demonstrating the benefits of UJS and SJR in respect of minimalism and robustness, even if you ultimately decide these tools and practices aren’t a fit for your application’s particular needs.
No Comments