segmentation tool, which allows our customers to create highly granular subsets of contacts within their account. We recently updated this tool to add a number of new front-end features, all built on top of Backbone.js.

Getting Started

First, we looked at how to structure our underlying code. We knew we had the following elements to work with:{C}
  • A Segment which was an all-encompassing wrapper for everything else we were building.
  • Rules that could be added to a segment and would contain a limited number of properties, a few of which would be user-editable.
  • Criteria that could be added to a rule and would encompass the bulk of the complexity of the application. There would be many possible types of criteria. Each criterion would have a variety of dynamic fields that would need to change based on user input/selections.
Each of the above elements would have a model. We opted to use a single base model for both Rules and the Segment, but have unique Criteria models that were based on the criteria type and extended from a common parent model. Because Backbone.js doesn't support nested models, we opted to keep all our models sitting at the top level alongside each other and to take advantage of Collections where we could. Since we'd never have more than one segment per page, the segment model is the only one that isn't part of a collection. We created a Rules Collection, and a Criteria Collection to house all the Rule and Criteria model instances.

View and Template Structure

We created single views for the segment and rules, and then setup specialized criteria views that all inherited from a shared base. For some groups of criteria, the rendering logic is identical. For those child views, we'd just call a function in the parent criteria view to handle rendering. We also found that we needed to add custom events to some child views. Since we only needed to do this for about 20% of the views, we added an optional function that we'd call from the parent view if it existed. At a basic level (ignoring child views and models), our segment structure looks something like this: segment-arch-for-blog Once we had our basic structure mapped out for a segment, we still had one major piece left to figure out: the inner templates, or "fields" within each criterion. Say, for example, that a user selected an "email_address" criterion. This particular criterion type requires that we show the user two additional fields within the criteria view: a drop down with a bunch of options and then as many text input fields as a user might want. email_address_crit After looking at all the fields we were going to be using, we realized that the behavior for a lot of these were going to be shared across multiple criteria. For example, a text input field should behave the same way no matter what criterion it lives in. Because there was so much shared behavior across the inner criterion fields, we opted to create generic templates that could be shared across criteria. We created a rule template for the rule view and a criteria template for the criteria view. After that, we created a collection of smaller 'field' templates to be used within the criteria templates. Breaking down the render() method in the criteria view, we first render out our base criteria template and then delegate out work down to the child view to do any needed criterion-specific rendering. In the child views, we'll wind up eventually calling back up to the parent and passing through a function to render templates(called renderTemplate()) for as many fields as we have to render. The renderTemplate() function accepts keys as params that match to specific config options. Handling the inner fields this way allowed us to simplify our logic so that we use just 10 different field templates across all 40-plus types of criteria. Based on a CSS class that we pass down to the templates, the criteria view binds different events handlers. For example, changing some types of fields will simply update the model with the new values, while changing other types will also render out new field templates based on the value selected.

Problem Solving

While working on this project, we ran into some hurdles, some larger than others. In general, we tried to do things the "Backbone way" before attempting other solutions. If there was already a tried and true workaround out there, we used it. However, as with most engineering projects, we found ourselves in unmapped territory more often than not. Here are some of the issues we ran into and how we worked through them:
  • Backbone.js doesn't support for nested objects within models. While Backbone does allow you to store nested objects in a model, you lose all the useful event handling that comes along with model objects, such as listening for changes to a specific property, etc. Normally, model properties are simple to read and write to:
    Model.set({     property: value }); 
    In the above example, "value" can be anything you want. The problem comes into play when you want "value" to be an object. We can store this object just fine. But when we want to change it, we need to replace the ENTIRE object or Backbone won't pick up the changes to it. To work around this, everything was broken out into top-level properties. When those properties were objects or arrays, we simply replaced the model values when we needed to change them. Our approach of replacing model values wound up being beneficial to us when it came time to handle multiple values for user-entered input. We store these values in an array mapped to a model property. This starts to get tricky when we look at filtering out duplicate values and handling removals. Say we have an array that looks like this:
    ['gmail', 'aol', 'hotmail', 'yahoo'] 
    Now, let's say the user removes "aol". We'd need to filter through the existing array, remove the value, and then store the new array. Instead, we just look at what's present in the UI, and store that. This is handy because the same function can then be called for both adding and removing items. Each time a user adds an item, simply look at what's in the UI and replace. This also helps with filtering out duplicates, as our logic can live in the function that looks into the DOM to grab values.
  • Using Model.clone() was not such a good idea for us after all. This is not to say that Model.clone() isn't useful; it just wasn't the right solution for our architecture. We initially implemented our copy/clone feature using Backbone.js's built-in cloning function for models. This backfired on us for a few reasons:
    • The values we sent to the server/read back from the server are not the same values we ultimately use within the Backbone.js portion of the segment builder. Because the server has its own requirements and limitations for what it will/won't accept, we were transforming data before we sent it over and then decoding it after we got it back from the server. Using .clone() bypasses this translation layer completely by just giving us a new model that already had the values we wanted. This worked at first, but as our translation layer became more and more complex we found that this introduced bugs.
    • Because the cloned models were bypassing the translation layers, we found ourselves introducing little hacks here and there to work around the fact that we'd built two distinct code paths for models to pass through. Whenever possible, build a single code path where your logic lives. It became twice as hard to diagnose and solve problems because we had to look in two places when issues cropped up.
    We finally stripped out .clone() completely and went the route of creating a new model. We then set the properties using the same packaging function we used to send criteria over to the server and manually calling initialize() again on the model.

Conclusion

Overall, we were really happy with Backbone.js and have continued using it in other projects at Bronto since we released the segment builder. The builder has been out of beta and in production for over a year now and we're finding that it's not too tricky to get in and modify the code when we need to address issues, or add new features. builder]]>