- Continued pagination, filtering refactor and tie in
- Investigating tie in of DocumentCloud's visual search
- Implemented first few iterations of binding system:
- bindings for nodes and most input elements
- visibility toggling binding
- eval style bindings
- Discussed requirements and possible approach for templating
- Initial templates for JS "behaviour injection"
- Initial decisions on attempt at solving round XHR round trips
- Discussed scoping concepts but solution will likely arrive with templating
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:
- We keep it lightweight for as long as possible and only opt in to cases we need
- We avoid biasing ourselves towards anything that those frameworks may bring with them
- The proposed binding system is still very small and simple
- 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.
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.
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.
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>
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>
.
The following is an example of how we are planning to tie in rich client functionality to each "app" page.
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>
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.
Our initial approach was to submit a form on the index, POST
ing to /admin/pages/set
and doing a classic round trip.
Benefits:
- No JS
- Rails helpers translate well to params server side
- Simple
Pain points:
- How do we handle success vs. fail cases (redirect vs. render)
- If we wish to render the action directly, we need to set up the
set
action with all data required torender action: :index
inside of it. - We need to model the client-side state of
selected
in to the ERB by looking at the list ofpage_ids
being sent and ERBing in the defaultchecked
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:
- Client side state stays client side. The server doesn't need to know about how to toggle things as selected.
- 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:
- 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.
- Trying to make decisions on how the data should be merged in #1 may prove to be challenging.
This has only been discussed on the whiteboard but I'm hoping to get some of this down for reference.
- Will likely introduce the concept of scopes
- Will likely require the need for some kind of template node that's not shown in the DOM
- Will likely need to be bound to a scope that can be pre-populated in JS
- 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?
}