Data Binding Best Practices – Brendan McLoughlin – Angular



Data Binding Best Practices – Brendan McLoughlin – Angular

0 2


data-binding-best-practices


On Github bmac / data-binding-best-practices

Data Binding Best Practices

Brendan McLoughlin

Web Unleashed 2013

https://bmac.github.io/data-binding-best-practices

Bocoup

@Bocoup

Oh hey, these are some notes. They'll be hidden in your presentation, but you can see them if you open the speaker notes window (hit 's' on your keyboard).

I Love Data Binding

  • Great for applications that have complex UI requirements
  • Enterprise apps (Validation, Workflows)
  • Dashboards (Real-time)
  • WebRTC (Heavy use of browser APIs)
  • Complex DOM manipulation in response to data changes
https://lh3.ggpht.com/-7ntFFNpikqM/Tx67iYPZTpI/AAAAAAAABaQ/lk1n55zhFls/s1600/i_love_you_cupcake.jpg

What is Data Binding?

  • Link data to the UI
  • Changes to the data or the DOM are kept in sync
  • Relationships are described with a declarative syntax
[ { "name": "Economy", "price": 199.95 }, { "name": "Business", "price": 449.22 }, { "name": "First Class", "price": 1199.99 } ]

Choose a ticket class:

You have chosen ($)

  • Data is some JavaScript state object
  • In this case the UI is the DOM
  • reduces boilerplate keeping html up to date
  • a way to write dynamic HTML using HTML
  • Not the only way but all the examples here use a template or the dom to describe the relationship between data and the dom

Data Binding is Not a New Concept

  • Microsoft's Windows Presentation Foundation
  • Adobe's Flex
  • Apple's Key Value Observation
  • PowerBuilder's DataWindow
  • Others
  • Most of these are Application Frameworks
  • Desktop or in Plugins
  • The web is now ready for the complex interactions these frameworks use to provide
  • We can take the good ideas from them and use them on the web

JavaScript's Data Binding Landscape

  • Angular (late 2009)
  • Knockout (2010)
  • Ember (2011)
    • Sproutcore (2007)
  • Meteor (2011)
  • Ractive (2012)
  • Polymer (2012)
  • Relatively recent
  • We've had some time to shake out the bugs
  • Browsers are getting faster
  • Network requests are still slow
  • Browsers can do more

Example Time

  • Angular
  • Ember
  • Knockout
    • Take a look at binding some data
    • Looks at a simple example of how the frameworks do binding under the hood
    • Identify common patterns

Presentation Model

Source of Truth for Application State

  • $scope
  • Controller
  • View Model
  • Should contain all the state of the application
  • Not just the state that should be persisted

Template

Describes the Relationship Between State and the DOM

  • HTML
  • Handlebars
  • HTML
  • Presentation Model doesn't know about the bindings
  • Bindings don't know about the presentation model

Angular

function Controller($scope) { $scope.name = 'foo'; $scope.length = function() { return $scope.name.length; }; // this.length(); => 3 $scope.reset = function() { $scope.name = 'foo'; }; };
<input ng-model="name" value="foo"/> <button ng-click="reset()">Reset</button> <div>Name: {{name}} Length: {{length()}} </div>

Reset

Name: foo     Length: 3

Angular

$scope.price = 100; //... myModule.directive('numberInput', function () { return { template: '<input type="text"/>', restrict: 'E', require: 'ngModel', link: function($scope, element, attrs, modelCtrl) { element.on('blur', function() { $scope.$apply(function() { var num = parseInt(element.value.replace(/,/g, ''), 10); modelCtrl.$modelValue = num; }); }); modelCtrl.$render = function () { element.value = modelCtrl.$modelValue.toLocaleString(); }; // missing closing braces...
<label> Price: <number-input ng-model="price"></number-input></label>
Price:

Ember

var ExampleController = Ember.ObjectController.extend({ name: 'foo', length: function() { return this.get('name').length; }.property('name'), // this.get('length'); => 3 actions: { reset: function() { this.set('name', 'foo'); } } };
{{input value=name }} <button {{action 'reset'}}>Reset</button> <div>Name: {{ name }} Length: {{ length }}</div>
Reset
Name: foo     Length: 3

Ember

controller.set('price', 100); App.NumberInputComponent = Ember.Component.extend({ setUp: function() { this.$('input').on('blur', this.updateValue.bind(this)); this.setValue(); }.on('didInsertElement'), updateValue: function() { var $input = this.$('input'); var number = parseInt($input.val().replace(/,/g), 10); this.set('value', number); }, setValue: function() { var $input = this.$('input'); $input.val(this.get('value').toLocaleString()); }.observes('value') };
<script type="text/x-handlebars" id="components/number-input"> <input type="text" /> </script> {{number-input value=price }}

Knockout

var ViewModel = function() { this.name = ko.observable('foo'); this.length = ko.computed(function() { return this.name().length; }, this); // this.length(); => 3 this.reset = function() { this.name('foo'); }; };
<input data-bind="value: name, valueUpdate: 'afterkeydown'" value="foo"/> <button data-bind="click: reset">Reset</button> <div>Name: <span data-bind="text: name"></span> Length: <span data-bind="text: length"></span></div>

Reset

Name: foo     Length: 3

The Presentation Model is for State

  • Good
    • State Values
    • Computed Values
    • Simple Handlers
  • Bad
    • DOM Manipulation
    • Business Logic
    • Ajax/Network Logic
      • State Lives in JavaScript not the DOM
      • When getting started people often try to put everything here
      • Usually works with the model
      • All about state, responding to domain events, Do not put business logic here

Good

App.StateController = Ember.ObjectController.extend({
  actions: {
    toggleEdit: function() { // Simple Event Handler
      this.toggleProperty('editing')
    },
    save: function() { // Delegate complex logic to services
      localStorageService.save(this.get('model'));
    }
  },
  editing: false,
  author: {}, // State
  authorName: function() { // Computed State
    var firstName = this.get('author.firstName');
    var lastName = this.get('author.lastName');
    return firstName + ' ' + lastName;
  }.property('author.firstName', 'author.lastName')
});

Bad

var viewModel = {
  toggleEdit: function() {
    $('.edit-controls').toggleClass('hide'); // DOM manupulation
    $('.view-panel').toggleClass('hide');
  },
  save: function() {
    var model = ko.toJS(this); // Complex logic
    localStorage.setItem(this.key, JSON.stringify(model));
  },
  load: function() {
    var self = this; // AJAX
    $.ajax(this.url).then(function() {
      self.author(data.author);
      self.title(data.title);
    });
  }
};
  • Many times your arragement of state is specific to the screen
  • In general Presentation models are not very reusable
  • You want to move reusable logic out of the presentation model

Business Logic Belongs in Services

  • Work with JavaScript Objects not the DOM
  • Reusable
  • Testable

Hints that your Code Belongs in a Service

  • Ajax
  • Transformation of Data
  • Iteration or Underscore Methods
  • Managing Promises
  • Facade for 3rd party APIs

Logicless Templates

http://www.calwatchdog.com/wp-content/uploads/2013/05/Spock-logic
  • Templates are about describing the relationship between data and the DOM
  • You really want to keep logic in the presentation model
  • If you find yourself wanting to add logic to the template you probably want to use a computed property

Bad

<label ng-app>Batting Average: {{hits / atBats}}</label>
<span data-bind="if: onsale || superCheap">
  Buy Me!
</span>

Unacceptable

<button ng-click="count = count + 1" ng-init="count=0">
  Increment
</button>
  • Logic in templates is evil
  • ember doesn't let you do this/ ember gets this 100% right

If/Else :)

{{#if inStock}}
  <button>Buy It Now!</button>
{{else}}
  <span>Sold Out!</span>
{{/if}}

Passing Arguments :)

<button {{action "buyItNow" item}}>✓</button>
<button ng-click="buyItNow($event, item)">
 Increment
</button>

Bad

<button ng-click="buyItNow($event, item || 'default')">
 Increment
</button>

Unless You are Using Knockout :(

<button data-bind="click: function(vm) {vm.something(foo); }">
  Do Something
</button>

Keep Presentation Logic Close to the DOM

<label>Order Date: {{purchaseOrder.createdAt | date:'MM/dd/yyyy'}}</label>
<label>Order Date: {{date purchaseOrder.createdAt 'MM/DD/YYYY'}}</label>
<label>Order Date: <span data-bind="text: fmt.date(purchaseOrder.createdAt, 'MM/DD/YYYY')"></span> </label>
  • Angular Filters
  • Handlebars Helpers
  • functions
  • Keep Data in a Form that is easy to manipulate
  • Bindings are for Responding to a Browser Events

Binding

Update the DOM to Reflect the Presentation Model

Translates DOM Events into Application Actions

  • Directives
  • Components
  • Binding

Knockout

viewModel.price = ko.observable(100); ko.bindingHandlers.number = { init: function(element, valueAccessor) { var observable = valueAccessor(); element.addEventListener('blur', function() { var number = parseInt(element.value.replace(/,/g, ''), 10); observable(number); }); }, update: function(element, valueAccessor) { var number = ko.unwrap(valueAccessor()); element.value = number.toLocaleString(); }};
<label> Price: <input type="text" data-bind="number: price" /></label>
Price:

Angular Directive

$scope.price = 100;
<number-input ng-model="price"></number-input>

Ember Component

controller.set('price', 100);
{{number-input value=price}}

Bindings

  • Respond to Observable Changes and Update the DOM
  • Respond to Browser Events and Update the Observables

Wrap Existing UI Widgets in Binding

  • Do not initialize widgets from a Presentation Model
  • Think about what state you may need to expose
  • Look for open canonical open source implementations
  • When transition an old application to a data binding libarary
  • Or just reusing an existing open source widget
  • Most of the work is prolly done for you so google

Work With the Element You are Given

Good

init: function(element, valueAccessor) { $(element).datepicker(); }

Bad

init: function(element, valueAccessor) {
  element.addEventListener('focus', function() {
    $('label').addClass('hot-pink');
  });
  element.addEventListener('blur', function() {
    $('label').removeClass('hot-pink');
  });
}

Expose State as an Observable

var VM = function() { this.name = ko.observable(''); this.inputHasFocus = ko.observable(false); this.highlight = ko.computed(function () { return !(this.name() || this.inputHasFocus()); // true }, this); }; ko.bindingHandlers['hasFocus'] = { init: function (element, valueAccessor) { var observable = valueAccessor(); $(element).on('focus', function () { observable(true); }); $(element).on('blur', function () { observable(false); }); // ...
<label data-bind="css: {'hot-pink': highlight}">Favorite Fruit:</label> <input id="fruit" data-bind="value: name, hasFocus: inputHasFocus"/>

Favorite Fruit:

  • jQuery widgets
  • Modals
  • I want perform this action only when x is shown/hidden

Use a Module System

  • Apps are Complex
  • Separate Responsibilities Calls for Separate Modules

https://upload.wikimedia.org/wikipedia/commons/4/43/Apollo_10_Command_Module_1.jpg

  • Its really easy to create circular dependencies if you are using all gobals
  • Angular gets props for encouraging modules with its dependency injection
  • Require js
  • ES6
  • Browserfy

Unit Test

  • Test Separately

http://unbounce.com/photos/3-ab-testing-variations.png

Conclusion

  • Presentation Model is for Data
  • Services for Business Logic
  • Templates Describe the Relationship Between DOM and Data
  • Bindings for DOM Manipulation and Event Handling

Fin