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.


     a1   <template name='identity'>
     a2     <div id='identityForm'>
     a3       {{> identityForm}}
     a4     </div>
     a5   </template>
     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).


     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.


     a35  window.Events = {}
     a37  Events.handleNaturally = (e) ->
     a38    e.preventDefault()
     a39    e.stopPropagation()
     a41  Template.updateButtons.created = -> Session.set 'formitemChanged', false
     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).


     a44  Template.identityForm.person = ->
     a45    person = Meteor.People.findOne { personId: Meteor.userId() }
     a46    person ? {}
     a48  Template.identityForm.events
     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
     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
     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).


     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.


     a67  Meteor.methods
     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.


     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.


     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).


     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.


     c1   <template name='identity'>
     c2     <div id='identityForm'>
     c3       {{> identityForm}}
     c4     </div>
     c5   </template>
     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>
     c40  <template name="countriesSelect">
     c41    <select id="countries" class="selectpicker changeitem">
     c42      {{> countriesSelectOptions}}
     c43    </select>
     c44  </template>
     c46  <template name="countriesSelectOptions">
     c47    {{#each options}}
     c48      <option>{{this}}</option>
     c49    {{/each}}
     c50  </template>
     c52  <template name="statesSelect">
     c53    <select id="states" class="selectpicker changeitem">
     c54      {{> statesSelectOptions}}
     c55    </select>
     c56  </template>
     c58  <template name="statesSelectOptions">
     c59    {{#each options}}
     c60      <option>{{this}}</option>
     c61    {{/each}}
     c62  </template>
     c64  <template name="citiesSelect">
     c65    <select id="cities" class="selectpicker changeitem">
     c66      {{> citiesSelectOptions}}
     c67    </select>
     c68  </template>
     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.


     c75  Template.identityForm.person = ->
     c76    person = Meteor.People.findOne { personId: Meteor.userId() }
     c77    person ? {}
     c79  Template.identityForm.events
     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
     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
     c98    'keypress, change .formitem': (e) ->
     c99      Session.set 'formitemChanged', true
     c101   'change .changeitem': (e) ->
     c102     Session.set 'formitemChanged', true
     c104   'change #countries': (e) ->
     c105     Template.statesSelect.change()
     c107   'change #states': (e) ->
     c108     Template.citiesSelect.change()
     c110 Deps.autorun ->
     c111   Meteor.subscribe("locations", ->
     c112     Template.countriesSelect.updateUI())
     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])
     c122 Template.countriesSelectOptions.helpers
     c123   options: ->
     c124     countries = Meteor.Lookups.findOne { name: 'location_countries' }
     c125     if countries
     c126       countries.values.split '|'
     c127     else
     c128       [ ]
     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()
     c138 Template.statesSelect.change = ->
     c139   if country = $('#countries').val()
     c140     Meteor.subscribe("locations",country, -> Template.statesSelect.updateUI())
     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])
     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       [ ]
     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()
     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())
     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])
     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       [ ]
     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.


    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.


    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.


    c200 Meteor.startup ->
    c201   assertLookups()
    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.


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.