Skip to content

Instantly share code, notes, and snippets.

@kristianpd
Last active January 4, 2016 10:49
Show Gist options
  • Save kristianpd/41a9bd0749197741b824 to your computer and use it in GitHub Desktop.
Save kristianpd/41a9bd0749197741b824 to your computer and use it in GitHub Desktop.
1 week in

Recap -> Wed. Jan 24, 2014

Progress

  1. Continued pagination, filtering refactor and tie in
  2. Investigating tie in of DocumentCloud's visual search
  3. Implemented first few iterations of binding system:
  • bindings for nodes and most input elements
  • visibility toggling binding
  • eval style bindings
  1. Discussed requirements and possible approach for templating
  2. Initial templates for JS "behaviour injection"
  3. Initial decisions on attempt at solving round XHR round trips
  4. Discussed scoping concepts but solution will likely arrive with templating

Angular or other alternatives

Replacing one binding system with another has risks of re-inventing the wheel. We've discussed porting Batman or Angular's binding system as a starting point but have opted for writing our own for the time being.

The reasons for this are:

  1. We keep it lightweight for as long as possible and only opt in to cases we need
  2. We avoid biasing ourselves towards anything that those frameworks may bring with them
  3. The proposed binding system is still very small and simple
  4. By starting from the ground up, we believe we'll have a better understanding of what exactly we're looking for, even if the end solution ends up coming from another framework.

Something to keep in mind

If we hit the 300 line mark of a binding system implementation we may want to more seriously look at porting one in. I'm not sure if this will happen, but IMO the cost of building a ~ 120 line one has been a minimal time effort with huge gains on concepts and solidifying our ideas.

Bindings

Our initial binding system has been designed around server side rendering with opt-in bindings that are DOM driven and backed by a lightweight store.

The following is an example of how the binding system would attach itself to a node value

<h1 bind="page.title"><%= @page.title %></h1>
<input type="text" bind="page.title"><%= @page.title %></input>

Some discussions have come up around defining the true source of the data on a view in the event that multiple nodes bind but don't have the same or any value. For example:

<h1 bind="page.title"></h1>
<input type="text" bind="page.title"><%= @page.title %></input>

In this case, the last value encountered will be the value stored.

Smelly this raises the possible case where multiple ERB values may be inserted slightly differently.

<h1 bind="page.title"><%= handleize @page.title %></h1>
<input type="text" bind="page.title"><%= @page.title %></input>

In this case, the <h1> value would be overriden with the page title bound to on the input element.

storage

We had several initial ideas on how we could store the values that we bind in the DOM. We've decided to opt for a POJO tree structure that translates directly to the delimited binding key entered in the DOM.

<% @pages.each do |page| %>
<tr>
  <td bind="pages.<%= page.id %>.selected"></td>
  <td bind="pages.<%= page.id %>.title"><%= page.title %></td>
</tr>
<% end %>

results in

<tr>
  <td bind="pages.1.selected"></td>
  <td bind="pages.1.title">Bob</td>
</tr>
<tr>
  <td bind="pages.2.selected"></td>
  <td bind="pages.2.title">Hello there</td>
</tr>

and is stored as

{
  pages: {
    1: {
      selected: true
      title: 'Bob'
    },
    2: {
      selected: false
      title: 'Hello there'
    }
  }
  }
}

This makes it easy to eval bindings.

evaling

In order to try and provide more flexibility for things like filters and computing, we've opted for literal execution of the binding string within the context of the scope.

<h1 bind="handleize(page.title)"></h1>
<input type="text" bind="page.title"><%= @page.title %></input>

Values we need that have no node value

One challenge that's come up around having an entirely DOM driven data model is that sometimes data values are required that are not shown in the DOM. A classic example of this is the id property for active record objects.

One solution would be to add a hidden input element or other node value that would bind the value like this:

<span style="display:none" bind="pages.<%= page.id %>.id"><%= page.id %></span>

or

<input type="hidden" bind="pages.<%= page.id %>.id"><%= page.id %></span>

Instead, we've opted to use a custom element.

<define bind="page.<%= page.id %>.id"><%= page.id %></define>

Smelly We may be missing another solution here, though to keep the DOM driven data model concept going we've made the case for <define>.

Workign with JS

The following is an example of how we are planning to tie in rich client functionality to each "app" page.

Modules

These components will wrap behaviours needed to accomplish small segments of functionality on a given app view

function BulkOperations (scope) {
  scope.selectAll = function() {
    var pages = $get('pages')
    selectAll.allSelected = !selectAll.allSelected

    for (i in pages) {
      $set('pages.' + i + '.selected', selectAll.allSelected)
    }
  }
}

They will be injected in to the app page in a declarative style

<%= layout_data section: 'pages' %>
<%= inject 'BulkOperations', 'SomethingElse' %>
<!--
  <script type="text/javascript">
    BulkOperations(scope)
    SomethingElse(scope)
  </script>
-->

<table>
  <tr>
    <td><input type="checkbox" onclick="selectAll()"></input></td>
  </tr>
  <% @pages.each do |page| %>
  <tr>
    <td>
      <input type="checkbox" bind="pages.<%= page.id %>.selected"></input>
    </td>
    <td><%= page.title %></td>
  </tr>
  <% end %>
</table>

Round trips

Our general consensus is that we should aim for "web 1.0" as much as possible. This means classic form submissions should be embraced and used where applicable, like pages#show and pages#new.

In the case of bulk operations on pages#index, the approach became a little more difficult.

Approach #1 - Classic for submission

Our initial approach was to submit a form on the index, POSTing to /admin/pages/set and doing a classic round trip.

Benefits:

  1. No JS
  2. Rails helpers translate well to params server side
  3. Simple

Pain points:

  1. How do we handle success vs. fail cases (redirect vs. render)
  2. If we wish to render the action directly, we need to set up the set action with all data required to render action: :index inside of it.
  3. We need to model the client-side state of selected in to the ERB by looking at the list of page_ids being sent and ERBing in the default checked state.

Approach #2 - XHR requests for interactions that need to keep state around between refreshes. "Stateful" refreshes.

Our second approach was to use XHR to submit the data

Benefits:

  1. Client side state stays client side. The server doesn't need to know about how to toggle things as selected.
  2. Full server refreshes ensure any complexity around updating the DOM to the right state are minimized. An example would be publishing all pages and expecting the list of pages to have a published flag beside them.

Pain points:

  1. It may become more difficult to rebind all state for cases where one interaction would refresh a page that another component depends on. For example, pagination could refresh the list but bulk checked state would be lost.
  2. Trying to make decisions on how the data should be merged in #1 may prove to be challenging.

Templating

This has only been discussed on the whiteboard but I'm hoping to get some of this down for reference.

  1. Will likely introduce the concept of scopes
  2. Will likely require the need for some kind of template node that's not shown in the DOM
  3. Will likely need to be bound to a scope that can be pre-populated in JS
  4. Should only be used for transient client side state, like building a list of product variants before saving.

Some rough ideas:

<ul scope="pages">
  <% @pages.each do |page| %>
  <li><%= page.title %></li>
  <% end %>
  <li template="new_page" bind="title"></li>
</ul>

<button onclick="addPage()"></button>
function addPage() {
  template = $template('new_page').clone()

  var page = { title: "It's a new page!" }

  template.bind(page)

  // inserting in to the DOM somehow. Perhaps a `prepend` before the `template` `<li>` would be feasible?
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment