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