17 May 2014
Reactive Forms in Meteor JS

While I’m not a big fan of form-based interfaces, it’s sometimes the best way to get basic object information into a system. Once there, manipulating objects via a more intuitive UX is the target. But forms live on in systems as a convenient, time-tested way to capture user input.

Meteor puts a different twist on the way interfaces are built - reactivity is what’s at the heart of Meteor interfaces. Instead of getting the data and building the interface from it, the interface is built from reactive templates that re-render when the data they depend on changes. This allows a developer to simply set things up and let them work rather than writing code to check for changes and update the interface. Quite a significant difference to the art of building web applications!

What’s in a Form?

A form is pretty simple. It’s a set of editable controls with a save or cancel button, usually at the bottom. The controls can be fairly sophisticated, but generally they’re not. The sophisticated ones have dependencies between the values being collected so that changing one might somehow change another. There are also validations that can be set up to make sure only acceptable values can be entered. Between validation and dependency, I can make sure forms collect ‘legal’ and ‘consistent’ values.

But these are bigger fish - I’m going to focus on simply getting information and saving it. I’m going to use Bootstrap3 and Coffeescript in the examples to follow.

I’ll create form that takes the logged in user’s identity as their first and last name and store it into the database.

client/identity.html

     a1   <template name='identity'>
     a2     <div id='identityForm'>
     a3       {{> identityForm}}
     a4     </div>
     a5   </template>
     a6
     a7   <template name='identityForm'>
     a8     {{#with person}}
     a9     <form class='form-horizontal' role='identity-form'>
     a10      <div class="form-group">
     a11        <label for="name" class="col-sm-2 control-label">Name</label>
     a12        <div class="col-sm-2">
     a13          <input type="text" class="form-control formitem" id="firstName"
     a14                 placeholder="first" value="{{firstName}}">
     a15        </div>
     a16        <div class="col-sm-4">
     a17          <input type="text" class="form-control formitem" id="lastName"
     a18                 placeholder="last" value="{{lastName}}">
     a19        </div>
     a20      </div>
     a21      {{> updateButtons}}
     a22    </form>
     a23    {{/with}}
     a24  </template>

The form page is simple enough. It starts with a template wrapper (a1) that is accessed by the router, and calls out (a3) to the identity form template (a7). The form pulls uses the person (a8) pulled from the coffeescript (a44) to display the person’s first name (a14) and last name (a18). At the end of the form, the form calls out (a21) to the update buttons template (a25).

client/common.html

     a25  <template name="updateButtons">
     a26    {{#if formitemChanged}}
     a27      <div class="form-group">
     a28        <div class="col-sm-offset-2 col-sm-4">
     a29          <button type="submit" class="btn btn-default form-done">Save</button>
     a30          <button class="btn form-done cancel">Cancel</button>
     a31        </div>
     a32      </div>
     a33    {{/if}}
     a34  </template>

The update buttons template can potentially be used by many forms, so it’s separated out. Basically, if the any form item has changed (a26) a Save button (a29) and a Cancel button (a30) are shown. If no form item has changed, they stay hidden.

client/common.coffee

     a35  window.Events = {}
     a36
     a37  Events.handleNaturally = (e) ->
     a38    e.preventDefault()
     a39    e.stopPropagation()
     a40
     a41  Template.updateButtons.created = -> Session.set 'formitemChanged', false
     a42
     a43  Template.updateButtons.formitemChanged = -> Session.get 'formitemChanged'

The common code is separated out like the common interface. Here there is some event management code (a37) that is typically used by event handlers, set up in a namespace (a35) to keep everything neat and tidy.

The update button management is found here, too. When the update buttons are created (a41) whenever the form renders, the change state is cleared and stored in the Session. This value is read from the Session (a43) and used during rendering the buttons (a26).

client/identity.coffee

     a44  Template.identityForm.person = ->
     a45    person = Meteor.People.findOne { personId: Meteor.userId() }
     a46    person ? {}
     a47
     a48  Template.identityForm.events
     a49
     a50     'submit form': (e) ->
     a51       Events.handleNaturally e
     a52       firstName = $('#firstName').val()
     a53       lastName = $('#lastName').val()
     a54       Meteor.call('setPersonIdentity',firstName,lastName)
     a55       Session.set 'formitemChanged', false
     a56   
     a57     'click .cancel': (e) ->
     a58       Events.handleNaturally e
     a59       form = $('#identityForm')
     a60       form.children().remove()
     a61       UI.insert(UI.render(Template.identityForm),form[0])
     a62       Session.set 'formitemChanged', false
     a63   
     a64     'keypress, change .formitem': (e) ->
     a65       Session.set 'formitemChanged', true

The code that handles the identity form is simple. The person that the form is targeting (a8) is determined by a reactive database search (a45), or given by empty values (a46) the first time around.

The rest of the code is event handling. A click on the Save button (a29) triggers the submit event on the form (a50) which pulls the first name (a52) and last name (a53) from the form and calls the server (a54) to set the value in the database. Changes are then reset (a55) to hide the update buttons (a55).

Clicking the Cancel button (a30) means all changed values should be discarded and reset to the original values. This is done by capturing the form (a59) removing its child elements (a60) and re-rendering it (a61). Changes are also reset (a62) to hide the update buttons (a55).

Finally, when a key is pressed on a form item, or the value of the form item is changed, the event handler (a64) is triggered and marks the form as changed (a65) to display the update buttons (a55).

lib/collections.coffee

     a66  Meteor.People = new Meteor.Collection('people')

None of this could be done without the people collection in the database. I declare it (a66) in a shared file used by the client and the server.

server/identity.coffee

     a67  Meteor.methods
     a68
     a69    setPersonIdentity: (firstName, lastName) ->
     a70      userId = Meteor.userId()
     a71      selector =
     a72        userId: userId
     a73      modifier =
     a74        userId: userId
     a75        firstName: firstName
     a76        lastName: lastName
     a77      Meteor.People.upsert selector, modifier

When I want to save the person into the database (a54) it’s the method on the server (a69) that gets called. It’s Mongo underneath the covers here, and I form a selector (a71) and modifier (a73) to pass to the upsert command (a77) which will create or update the specified record.

router/config.coffee

     a78  Router.map ->
     a79    @route 'home', path: '/'
     a80    @route 'identity, path: 'identity'

Finally, IronRouter is used to declare routes for the application. The identity route is set up to display the identity page (a80).

Of course, I’ve left out a few parts of the app, but this demonstrates the meat of the code fairly accurately. The big idea is that everything is driven by reactivity and user interaction.

  1. When the person is pushed to the client from the server, the values are rendered into the form.

  2. When items on the form have changed, the client-local reactivity affect the visibility of the update buttons.

Relieving the developer of these issues makes programming much simpler. Submitting changes to the server on commital or rerendering the form with original values on cancellation is an easy way to think about what’s going on. It’s all very close to the page, I’m thinking in terms of rendering templates and making simple responses to events, instead of being concerned with managing elements through a higher level interface.

(Note that there are high level form abstractions for Meteor like Dobbertin’s Autoform or just form validations like Copley’s Mesosphere. However, these require some level of detailing the mongo collection schema which has less to do with reactivity and more to do with building manageable systems, so they’ll just get a mention here.)

Dropping Autopublish

When developing a project in Meteor, the autopublish package is part of the app by default. This synchronizes the database between the client and the server. The benefit of this is that developers can concentrate on the logic of event handling and display rather than considering the transmission of which database entries are accessible.

At some point, a switch to the publish/subscribe model should be made and the autopublish package should be dropped. So when that bold step is taken:

$ meteor remove autopublish

all the data in the app disappears. To get it back, the client must subscribe to the database values it cares about, and the server must publish those values to the client.

Fortunately, this is easy.

router/config.coffee

     b1    Router.map ->
     b2      @route 'home', path: '/'
     b3      @route 'identity, path: 'identity'
     b4        waitOn: -> [ Meteor.subscribe 'identity' ]

In the router, I create a subscription (b4) to ‘identity’ that effectively states the the identity route needs the data pushed by the identity publisher on the server (b5).

server/identity.coffee

     b5    Meteor.publish 'identity', ->
     b6       Meteor.People.find { userId: @userId }

Now, instead of pushing all the entities in the people database to the client, only the one with the matching user id will be pushed.

And since I’m about to deal with a little more data, this will be important.

Adding Location

Now I’ll do something that get’s a little tricky to do reactively.

I want to add a Country/State/City selector that changes reactively - that is, one that’s a little bit sophisticated. The state depends on the country and the city depends on the state. Note also that it’s not really called Country/State/City in the interface - it might be Country/Province/City if Canada is selected, for instance. But I’m talking about three levels of cascaded location.

Additionally, I want to use Moreto’s Bootstrap-Select package so I get nice looking pulldowns that match the rest of the bootstrap interface. This may seem innocent, but it forces a whole extra level of interface control that strain’s the reactive mechanisms considerably.

client/identity.html

     c1   <template name='identity'>
     c2     <div id='identityForm'>
     c3       {{> identityForm}}
     c4     </div>
     c5   </template>
     c6
     c7   <template name='identityForm'>
     c8     {{#with person}}
     c9     <form class='form-horizontal' role='identity-form'>
     c10      <div class="form-group">
     c11        <label for="name" class="col-sm-2 control-label">Name</label>
     c12        <div class="col-sm-2">
     c13          <input type="text" class="form-control formitem" id="firstName"
     c14                 placeholder="first" value="{{firstName}}">
     c15        </div>
     c16        <div class="col-sm-4">
     c17          <input type="text" class="form-control formitem" id="lastName"
     c18                 placeholder="last" value="{{lastName}}">
     c19        </div>
     c20      </div>
     c21      <div class="form-group">
     c22        <label for="location" class="col-sm-2 control-label">Location</label>
     c23        <div class="row-fluid">
     c24          <span id="countriesSelect" class="col-sm-3">
     c25            {{> countriesSelect}}
     c26          </span>
     c27          <span id="statesSelect" class="col-sm-3">
     c28            {{> statesSelect}}
     c29          </span>
     c30          <span id="citiesSelect" class="col-sm-3">
     c31            {{> citiesSelect}}
     c32          </span>
     c33        </div>
     c34      </div>
     c35      {{> updateButtons}}
     c36    </form>
     c37    {{/with}}
     c38  </template>
     c39
     c40  <template name="countriesSelect">
     c41    <select id="countries" class="selectpicker changeitem">
     c42      {{> countriesSelectOptions}}
     c43    </select>
     c44  </template>
     c45
     c46  <template name="countriesSelectOptions">
     c47    {{#each options}}
     c48      <option>{{this}}</option>
     c49    {{/each}}
     c50  </template>
     c51
     c52  <template name="statesSelect">
     c53    <select id="states" class="selectpicker changeitem">
     c54      {{> statesSelectOptions}}
     c55    </select>
     c56  </template>
     c57
     c58  <template name="statesSelectOptions">
     c59    {{#each options}}
     c60      <option>{{this}}</option>
     c61    {{/each}}
     c62  </template>
     c63
     c64  <template name="citiesSelect">
     c65    <select id="cities" class="selectpicker changeitem">
     c66      {{> citiesSelectOptions}}
     c67    </select>
     c68  </template>
     c69
     c70  <template name="citiesSelectOptions">
     c71    {{#each options}}
     c72      <option>{{this}}</option>
     c73    {{/each}}
     c74  </template>

I introduce the three new select elements - one for countries (c40), one for states(c52), and one for cities (c64). The options for these selects are in their own templates(c46, c58, c70) - so each set of options can be rendered separately from its corresponsing select.

client/identity.coffee

     c75  Template.identityForm.person = ->
     c76    person = Meteor.People.findOne { personId: Meteor.userId() }
     c77    person ? {}
     c78
     c79  Template.identityForm.events
     c80
     c81    'submit form': (e) ->
     c82      Events.handleNaturally e
     c83      firstName = $('#firstName').val()
     c84      lastName = $('#lastName').val()
     c85      country = $('#countries').val()
     c86      state = $('#states').val()
     c87      city = $('#cities').val()
     c88      Meteor.call('setPersonIdentity',firstName,lastName,country,state,city)
     c89      Session.set 'formitemChanged', false
     c90   
     c91    'click .cancel': (e) ->
     c92      Events.handleNaturally e
     c93      form = $('#identityForm')
     c94      form.children().remove()
     c95      UI.insert(UI.render(Template.identityForm),form[0])
     c96      Session.set 'formitemChanged', false
     c97   
     c98    'keypress, change .formitem': (e) ->
     c99      Session.set 'formitemChanged', true
     c100
     c101   'change .changeitem': (e) ->
     c102     Session.set 'formitemChanged', true
     c103
     c104   'change #countries': (e) ->
     c105     Template.statesSelect.change()
     c106
     c107   'change #states': (e) ->
     c108     Template.citiesSelect.change()
     c109
     c110 Deps.autorun ->
     c111   Meteor.subscribe("locations", ->
     c112     Template.countriesSelect.updateUI())
     c113
     c114 Template.countriesSelect.updateUI = ->
     c115   ui = $('#countriesSelect select')
     c116   if ui.length > 0
     c117     options = $('option',ui)
     c118     if options.length > 0
     c119       options.remove()
     c120     UI.insert(UI.render(Template.countriesSelectOptions),ui[0])
     c121
     c122 Template.countriesSelectOptions.helpers
     c123   options: ->
     c124     countries = Meteor.Lookups.findOne { name: 'location_countries' }
     c125     if countries
     c126       countries.values.split '|'
     c127     else
     c128       [ ]
     c129
     c130 Template.countriesSelectOptions.rendered = ->
     c131   changed = Session.get 'formitemChanged'
     c132   $('#countries.selectpicker').selectpicker()
     c133   $('#countries.selectpicker').selectpicker("refresh")
     c134   $('#countries.selectpicker').selectpicker('val',Template.identityForm.person().country)
     c135   Session.set 'formitemChanged', changed
     c136   Template.statesSelect.change()
     c137
     c138 Template.statesSelect.change = ->
     c139   if country = $('#countries').val()
     c140     Meteor.subscribe("locations",country, -> Template.statesSelect.updateUI())
     c141
     c142 Template.statesSelect.updateUI = ->
     c143   ui = $('#statesSelect select')
     c144   options = $('option',ui)
     c145   if options.length > 0
     c146     options.remove()
     c147   UI.insert(UI.render(Template.statesSelectOptions),ui[0])
     c148
     c149 Template.statesSelectOptions.helpers
     c150   options: ->
     c151     country = $('#countries').val()
     c152     states = Meteor.Lookups.findOne { name: "location_#{country}_states" }
     c153     if country and states
     c154       states.values.split '|'
     c155     else
     c156       [ ]
     c157
     c158 Template.statesSelectOptions.rendered = ->
     c159   changed = Session.get 'formitemChanged'
     c160   $('#states.selectpicker').selectpicker()
     c161   $('#states.selectpicker').selectpicker("refresh")
     c162   $('#states.selectpicker').selectpicker('val',Template.identityForm.person().state)
     c163   Session.set 'formitemChanged', changed
     c164   Template.citiesSelect.change()
     c165
     c166 Template.citiesSelect.change = ->
     c167   country = $('#countries').val()
     c168   state = $('#states').val()
     c169   if country && state
     c170     Meteor.subscribe("locations",country,state, -> Template.citiesSelect.updateUI())
     c171
     c172 Template.citiesSelect.updateUI = ->
     c173   ui = $('#citiesSelect select')
     c174   options = $('option',ui)
     c175   if options.length > 0
     c176     options.remove()
     c177   UI.insert(UI.render(Template.citiesSelectOptions),ui[0])
     c178
     c179 Template.citiesSelectOptions.helpers
     c180   options: ->
     c181     country = $('#countries').val()
     c182     state = $('#states').val()
     c183     cities = Meteor.Lookups.findOne { name: "location_#{country}_#{state}_cities" }
     c184     if country && state && cities
     c185       cities.values.split '|'
     c186     else
     c187       [ ]
     c188
     c189 Template.citiesSelectOptions.rendered = ->
     c190   changed = Session.get 'formitemChanged'
     c191   $('#cities.selectpicker').selectpicker()
     c192   $('#cities.selectpicker').selectpicker("refresh")
     c193   $('#cities.selectpicker').selectpicker('val',Template.identityForm.person().city)
     c194   Session.set 'formitemChanged', changed

When considering the code that drives the selects, things get more complicated.

First the triggers. Responding to events drives the cascade process. When a change item changes (c101) the form item change is set to turn on the update buttons. When a new country is chosen (c104), we make the state change. When a new state is chosen (c107), we make the city change. Finally, a subscription to locations is created to start the cascade when ithey are published.

Next the changers. Changing the level in the cascade above has to subscribe to a different location and trigger user interfaces updates for the selectors. To change the state (c138) I subscribe to the selected country and update the state selector when the states for that country are published (c140). To change the city (c166) I subscribe to the selected country and state and update the city selector when the cities for that country and state are published (c169).

Now the user interface updaters. When the user interface for the selectors is updated, the options in a particular select are replaced by rerendering option templates. To change the country select (c114) a check is made to see if it’s in the DOM yet (c116) and if so, any existing options are removed (c119) and the country select options are rendered and inserted into the select (c120). Getting the country options is done in a helper (c123) that finds the countries that are available (c124) and returns them in an array (c126). To change the state select (c142) any existing options are removed (c146) and the state select options are rendered and inserted into the select (c147). Getting the state options is done in a helper (c123) that finds the states that are available for the chosen country (c152) and returns them in an array (c154). To change the city select (c172) any existing options are removed (c176) and the city select options are rendered and inserted into the select (c177). Getting the city options is done in a helper (c180) that finds the cities that are avialable for the chosen country and state (c183) and returns them in an array (c185).

Finally, the resonders to rendering. This last bit is what sycronizes the bootstrap-selects to the selects through the manipulation of selectpickers. for countries, states and cities, selectpickers are initialized (c132, c160, c191), refreshed (c133, c161, c192) and the person’s country, state or city is selected (c134, c162, c193). Then for the country and state, we fired the cascade for the next level down (c136, c164). Care is taken to preserve change due to initialization versus change due to user interaction.

Admitedly, this code is more than a bit repetitive and should be refactored, but I’m trying to make sure it’s clear what’s going on. Note that the code is reactive - the response to changes is updated subscriptions that cause new rendering, not direct manipulation of the user interface.

lib/collections.coffee

    c195 Meteor.Lookups = new Meteor.Collection("lookups")

The data itself for the country, states and cities is stored in what I’ve traditionally called lookup tables. The idea is that I don’t want to hard code it because it might rarely change, but I’m not going to change the values within the course of running this system. They’re values that can be reliably looked up.

server/identity.coffee

    c196 Meteor.publish 'locations', (country, state) ->
    c197   Meteor.Lookups.find { name: ($in: [ "location_countries",
    c198                                       "location_#{country}_states",
    c199                                       "location_#{country}_#{state}_cities" ] ) }

When I publish locations, I’m really just publishing a few lookup table entries. Note that if country or state isn’t specified, then the corresponding keys won’t be found (c111, c140, c170). This is all designed to pull the correct values when required.

server/db_seed.coffee

    c200 Meteor.startup ->
    c201   assertLookups()
    c202
    c203 assertLookups = ->
    c204   if Meteor.Lookups.find().count() == 0
    c205     Meteor.Lookups.insert name: 'location_countries', values: 'United States'
    c206     Meteor.Lookups.insert name: 'location_United States_states',
    c207                           values: 'Alabama|Alaska|Arizona| . . . |Wisconsin|Wyoming'
    c208     Meteor.Lookups.insert name: 'location_United States_Alabama_cities',
    c209                           values: 'Abernant|Alabaster| . . . |Wetumpka'
    c210     . . .

Finally, like all good Meteor programs, the existence of all needed data on the server is asserted when you start. In this case, the lookup table needs to include a list of countries, a list of states for each country, and a list of each city for each country-state combination. In this case I’m just including the United States, its fifty states, and cities with populations over 8000 people. I left off a lot of data, but you get the idea.

Thoughts

Cascading data turns out to be a non-trivial bit of code when it needs to work with specialized components in a chosen interface framework (in this case Bootstrap-select in Bootstrap). It’s clearly doable, but it feels like more work than it should be. In fact, the chain of “triggered change of subscriptions to updates and interface cleanup” has pretty much become a reactive pattern for me when using components that rely on unobtrusive javascript and setup calls to implement sophisticated interactions.

Doing forms reactively is certainly a different approach than what I did before using Meteor, but it does keep methods short and simple despite the number of moving parts. But once you’re done with them and the data has been entered into the system, you can use the kind of social reactivity that Meteor was made to shine for.