How We Use Knockout

Posted Aug 12, 2016 by Brandon Gafford

Here at Handshake, we’ve been successfully using Knockout in production for several years. In a codebase the size of ours, one of the major difficulties is balancing maintainability with the ability to make changes quickly. A key strength of Knockout is its focused approach to the presentation layer. This makes it easy and fast to surgically add interaction to a view, especially if the view is primarily static HTML with some interaction sprinkled on. Knockout does provide some tools to help with encapsulation and composition, but it is not very opinionated about the structure of the JavaScript as a whole. This left us to find our own way to deal with the complexity of scaling to a large, feature-rich application and the split between the back-end and the front-end codebases. In the next few posts, we will be exploring how we use Knockout’s tools, our experience with Knockout in production, and where we’re headed next.

Understanding Knockout

The core functionality of Knockout is the concept of making values “observable”. This allows creating two-way bindings between HTML and a JavaScript “view model”. Changing the JavaScript variable updates the HTML, and changing the HTML (typing into an input, selecting an option in a select box, etc) updates the JavaScript variable. The other thing it lets you do is have a JavaScript variable that depends on other variables, staying in sync whenever the dependent value changes. For example, if you have an array of objects users, but you only want to list users in the HTML if they are registered, you could have a second “computed” variable registered_users that filters users based on their registration status. If any objects are added or removed from users, registered_users will update. This makes it pretty easy to keep everything in sync. You don’t have to worry about manually recalculating registered_users every time you make changes to users. This becomes even more useful if you have multiple different ways to filter users: you just update users and everything updates from there.

As interactivity on a page grows, though, the dependency graphs can get really complicated. It’s tempting to just keep tacking on new computed values as requirements for the feature grow and change. In the short term, it is usually very easy to do this: just find the data you need to depend on, wrap it in a computation, and call it a day. Debugging a large graph of dependencies is very difficult, though. If something is updating in an unexpected or incorrect way, it can take a while to even figure out what’s triggering it because each level of the computation could be depending on multiple other values. While some simple common sense and organization can prevent a lot of major headaches like this, it can be difficult to maintain this kind of discipline on complex pages.

Organizing View Models

As the size and interactivity on pages grow, the view model that backs the observable values will grow proportionally. In a small, simple application, this can all live in one view model, but that quickly gets out of hand. A key aspect of keeping the code organized is having a system for splitting that up. We found that there are really two different categories to split the logic between: view models that represent model data from the server and view models that represent pages or containers within pages. The data view models are a great place to encapsulate logic related to syncing and managing data. They consume JSON from the server, set up which properties should be observable or computed, define helpers, and generally handle anything related to the data. The container view models can then use those data view models similarly to how you would use ActiveRecord objects inside of a Rails view. They don’t have to worry about setting up the data, they just have to worry about wiring the data up to the view. This helps to keep the code organized and the various pieces of the functionality in predictable locations.

Custom Bindings

One of the best ways in Knockout to encapsulate complex logic is through custom bindings. Custom bindings can be used in views just like regular knockout bindings, but they give you complete control over the setup and update processes. This helps with both directions of complexity described above. As the complexity grows, patterns start to emerge in their respective view models. Many times, those patterns can be extracted into a custom binding in a similar way to how logic can be extracted out into functions. This is an especially big win when those patterns are large, since well-defined APIs on the bindings can drastically reduce the amount that an individual contributor needs to know about them in order to use them on a given page. Let’s dive into some examples to see how they work:

Wrapping an existing binding

# Binding:
ko.bindingHandlers.invisible =
  update: (element, valueAccessor) ->
    val = ko.utils.unwrapObservable(valueAccessor())
    ko.bindingHandlers.visible.update(element, () => !val)

# Example Usage:
shouldHide = ko.observable(true)
...
<span data-bind="invisible: shouldHide">

The invisible binding is one of the simplest custom bindings possible, but it still includes many of the core features and ideas, so it makes a great starting point. One important thing to notice right off the bat is that there is just one method defined on the binding: update. This function is called by Knockout whenever an observable bound to the binding updates. In this case, if shouldHide is updated, the binding’s update method will be called. The two parameters to the update method are the two parts of the binding: element is the HTML element that the binding was declared on, and valueAccessor is a function that returns the value that was bound to. We can’t just directly use valueAccessor, since we don’t know whether that value was observable or a simple value. Instead, we wrap it with ko.utils.unwrapObservable. If the value is an observable, it will call it to retrieve the contents. Otherwise, it will simply return the value. This ensure’s that val is always an actual value instead of an observable function. The second part of the binding inverts the value of val and hands it off to the built-in binding visible. We rely on the visible binding to do the heavy lifting of actually showing and hiding the element.

Wrapping a jQuery call

# Binding:
ko.bindingHandlers.popover =
  init: (element, valueAccessor, allBindings) ->
    options = ko.utils.unwrapObservable(valueAccessor())

    $(element).popover(options).click( (e) =>
      e.preventDefault()
    )

# Example Usage:
@popover_options = {
  html: true,
  content: =>
    "<span>I am popover content!</span>"
}
...
<button type="button" data-toggle="popover" data-placement="top" title="Popover" data-bind="popover: popover_options, text: 'Click Me!'" />

The popover binding shows that a custom binding doesn’t have to be something that sets up a two-way connection like most of the built-in bindings do. In this case, we are hiding away interaction with jQuery’s Popover. All of the logic for setting up the popover is then encapsulated and isolated to just one place. This way, when we use the binding, we don’t have to interact directly with the library. All of the setup and default configuration is done for us. If, sometime in the future, we decide we want to change the defaults or perform some extra setup, we can just do it here instead of updating the Popover usage in a bunch of different places. One other neat thing about wrapping this in a binding is that we don’t have to use a special id or class to select the element later, since we have direct access to the element node from within the binding. Just like in the previous example we’re unwrapping valueAccessor, but instead of passing the value onto another binding, we’re passing it on to a library call.

Using both init and update

# Binding:
ko.bindingHandlers.tooltip =
  init: (element, valueAccessor) ->
    text = ko.utils.unwrapObservable(valueAccessor())

    $(element).tooltip({
      title: text
      container: 'body'
    })

  update: (element, valueAccessor) ->
    text = ko.utils.unwrapObservable(valueAccessor())

    $(element).attr('data-original-title', text)
    # if the tooltip was already open, force it to update the text
    # bootstrap adds the aria-describedby attribute to an element when its tooltip is triggered
    if $(element).attr('aria-describedby') != undefined
      $(element).tooltip('show')

# Example Usage:
tooltipText = ko.observable("I am tooltip text")
...
<button data-toggle="tooltip" data-bind="tooltip: tooltipText" />

The tooltip binding is similar to the popover binding, but it uses both the init and update methods. The init method gets the initial text value and initializes the tooltip just like above. The update method will get called whenever the observable holding the text changes. It takes the new value and updates the title text, and if the tooltip was already open it will force it to reset. Again, the encapsulation here makes the usage really easy. All you need is an observable with a string in it and a simple bind and it does all the hard work for you.

Extensions

While custom bindings help cut down on duplication and complexity when integrating JavaScript with HTML, extensions help with enhancing the functionality of the observables themselves. They can be used for preprocessing or encapsulating a set of dependencies that all rely on a single observable value. They are completely opt-in, though, which means that the extra features are easy to use when you need them, but they don’t get in the way when you don’t.

One common pattern we found was using observable arrays to hold asynchronously loaded data. In our views we would end up doing something like data-bind="if: collection().size > 0". That can get unnecessarily expensive, though, since the view would re-render every time the collection changes, even if the status of that size check did not. We also wanted to be able to be able to show some indication that the list was loading before the results came back and a different indication if the results came back empty. This meant that we had to have another observable to track wether a search was currently in progress, and forgetting to set it or reset in all the right locations could lead to an awkward experience for the user. Extensions allowed us to encapsulate all of that logic into the observable itself. Let’s take a look at how it works:

# Binding
ko.observableArray.fn.trackHasItems = ->
  # create sub-observables to track the states
  @hasItems = ko.observable(@length > 0)
  @finishedLoading = ko.observable(@length > 0)

  # convenience helpers
  @isEmpty = ko.computed( =>
    (not @hasItems()) and @finishedLoading()
  )
  @isLoading = ko.computed( =>
    (not @hasItems()) and (not @finishedLoading())
  )
  @clear = =>
    @([]) # set value to empty
    @finishedLoading(false)

  # update it when the observableArray is updated
  @subscribe( (newValue) =>
    @hasItems(newValue && newValue.length > 0)
    if newValue
      @finishedLoading(true) # if you set the value, the loading has probably finished
  )

  # support chaining by returning the array
  this

# Example Usage:
<div data-bind="visible: array.isEmpty">
  <!-- empty state -->
</div>
<div data-bind="visible: array.isLoading">
  <!-- loading state -->
</div>
<div data-bind="visible: array.hasItems">
  <!-- items present state -->
</div>

The first thing to notice is in the first line. We’re registering a new extension to observableArray through its fn property. This means that whenever we make a new observableArray, we can call trackHasItems on it to apply this functionality to it. Another important thing to know is that the @ (or this in regular JavaScript) refers to the observable itself. That means that @isEmpty and @isLoading are new observable properties on the array, and @subscribe is creating a subscription to the array’s value. Because of the way the computeds are set up, only one of isEmpty, isLoading, and hasItems is true at a time, and they act as a switch to show which state the array is in. Usually, this gets initialized on an empty observable array, like array = ko.observableArray().trackHasItems(), so the initial state is that isLoading is true. Once the the value of the array has been set, either hasItems or isEmpty will be true, depending on whether there were any results. The best part is that someone using this extension doesn’t really need to know how this works. All they need to do is call trackHasItems on their observable array and then they can use isLoading, hasItems, and isEmpty in the view. It takes care of tracking the internal state and efficiently updating the view when necessary.

What’s Next

Despite the considerable power of the tools that Knockout provides, there are still several major difficulties. Because Knockout requires binding to existing HTML, the JavaScript and HTML have to be written separately. This allows the HTML to be more declarative, but it can also become difficult to figure out exactly what JavaScript is in context at a particular point of the HTML. It also means that there is a divide between different parts of the rendering lifecycle; some of the view is rendered in HTML on the server, and some of it is rendered once Knockout binds to the page on the client. There has to be some mechanism for sending data from the server to the JavaScript on the client in order to finish rendering as well. After spending some time working on figuring out some of these problems, we’ve decided to start exploring alternatives to Knockout, including Turbolinks, React, and Redux. Stay tuned for future posts on that process!

engineering