What are we trying to observe? Raw object data.
// Objects
var obj = { id: 2 };
obj.id = 3; // obj == { id: 3 }
// Arrays
var arr = ['foo', 'bar'];
arr.splice(1, 1, 'baz'); // arr == ['foo', 'baz'];
Observe, mutate and change:
// A model can be a simple vanilla object
var todoModel = {
label: 'Default',
completed: false
};
// We then specify a callback for whenever mutations
// are made to the object
function observer(changeRecords){
changeRecords.forEach(function(change){
console.log({
affectedPropertyName: change.name,
valueBeforeChange: change.oldValue,
changeType: change.type,
affectedObject: change.object
});
});
}
// Which we then observe
Object.observe(todoModel, observer);
// Let's play!
/*
todoModel.label = 'Buy some more milk';
todoModel.completeBy = '01/01/2014';
delete todoModel.completed;
Object.unobserve(todoModel, observer);
*/
Notifier.notify() - what can be observed?:
function Circle(radius) {
Object.defineOwnProperty(this, 'radius', {
get: function () {
return radius;
},
set: function (newRadius) {
if (radius == newRadius)
return;
var notify = Object.getNotifier(this);
// notify(changeRecord)
notifier.notify({
type: 'updated',
// deleted, new, reconfigured etc.
// you can also just custom type (e.g 'foo')
name: 'radius',
oldValue: radius
});
radius: newRadius;
}
});
}
/*
notifier.notify({
type: 'reconfigured',
name: 'radius',
oldValue: 'circumference'
});
*/
Attribute binding:
<h1>Bind To Attributes</h1>
<ul>
<template id="colors" repeat="{{ colors }}">
<li style="color: {{ color }}">The style attribute of this list item is bound</li>
</template>
</ul>
<button id="rotateText">Rotate</button>
<script>
document.addEventListener('DOMContentLoaded', function() {
var t = document.getElementById('colors');
t.model = {
colors: [
{ color: 'red' },
{ color: 'blue' },
{ color: 'green' },
{ color: 'pink' }
]
};
// Needed to detect model changes if Object.observe
// is not available in the JS VM.
Platform.performMicrotaskCheckpoint();
var b = document.getElementById('rotateText');
b.addEventListener('click', function() {
t.model.colors.push(t.model.colors.shift());
Platform.performMicrotaskCheckpoint();
});
});
</script>
Text binding:
<h1>Bind To Text</h1>
<ul>
<template id="text" repeat="{{ text }}">
<li>Text is bound here: {{ value }}</li>
</template>
</ul>
<button id="rotateText">Rotate</button>
<script>
document.addEventListener('DOMContentLoaded', function() {
var t = document.getElementById('text');
t.model = {
text: [
{ value: 'Fee' },
{ value: 'Fi' },
{ value: 'Fo' },
{ value: 'Fum' }
]
};
// Needed to detect model changes if Object.observe
// is not available in the JS VM.
Platform.performMicrotaskCheckpoint();
var b = document.getElementById('rotateText');
b.addEventListener('click', function() {
t.model.text.push(t.model.text.shift());
Platform.performMicrotaskCheckpoint();
});
});
</script
Nested templates:
<h1>Nested Template</h1>
Managers:
<ul>
<template id="example" repeat="{{ managers }}">
<li>{{ name }}, Employees:
<ul>
<template repeat="{{ employees }}">
<li>{{ name }}</li>
</template>
</ul>
</li>
</template>
</ul>
<script>
document.addEventListener('DOMContentLoaded', function() {
var t = document.getElementById('example');
t.model = { managers: [
{
name: 'Bob',
employees: [{ name: 'Sally' }, { name: 'Tim' }, { name: 'Joe' }]
},
{
name: 'Janet',
employees: [{ name: 'Eric' }, { name: 'Jack' }, { name: 'Laura' }]
},
{
name: 'Suzie',
employees: [{ name: 'John' }, { name: 'Lucy' }, { name: 'Fred' }]
},
]};
// Needed to detect model changes if Object.observe
// is not available in the JS VM.
Platform.performMicrotaskCheckpoint();
});
</script>
Object.observe() with an acceptList:
// A model can be a simple vanilla object
var todoModel = {
label: 'Default',
completed: false
};
// We then specify a callback for whenever mutations
// are made to the object
function observer(changes){
changes.forEach(function(change, i){
console.log(change);
})
};
// Which we then observe
// Note the third argument
Object.observe(todoModel, observer, ['deleted']);
// without this third option, defaults to intrinsic types
todoModel.label = 'Buy some milk'; // note that no changes were reported
// delete todoModel.label;
// this change was reported
Defaults to intrinsic object change types:
// A model can be a simple vanilla object
var todoModel = {
label: 'Default',
completed: false
};
Object.observe(todoModel, function(changeRecords){
changeRecords.forEach(function(change){
console.log({
changeType: change.type,
affectedObject: change.object,
affectedPropertyName: change.name,
valueBeforeChange: change.oldValue
});
})
});
todoModel.label = 'Buy some bread';
delete todoModel.label;
Notify accessors:
var model = {
a: {}
};
var _b = 2;
Object.defineProperty(model.a, 'b', {
get: function() { return _b; },
set: function(b) {
Object.getNotifier(this).notify({
type: 'updated',
name: 'b',
oldValue: _b
});
console.log('set', b);
_b = b;
}
});
function observer(changes){
changes.forEach(function(change, i){
console.log(change);
})
}
Object.observe(model.a, observer);
//model.a.b = 4; // will be observed.
Perform a large change:
function Thingy(a, b, c) {
this.a = a;
this.b = b;
}
Thingy.MULTIPLY = 'multiply';
Thingy.INCREMENT = 'increment';
Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';
var myObserver = {
records: undefined,
callbackCount: 0,
reset: function() {
this.records = undefined;
this.callbackCount = 0;
},
};
var myObserver2 = {
records: undefined,
callbackCount: 0,
reset: function() {
this.records = undefined;
this.callbackCount = 0;
},
};
Thingy.prototype = {
increment: function(amount) {
var notifier = Object.getNotifier(this);
// Tell the system that a collection of work
// compromises a given changeType
notifier.performChange(Thingy.INCREMENT, function() {
this.a += amount;
this.b += amount;
}, this);
notifier.notify({
object: this,
type: Thingy.INCREMENT,
incremented: amount
});
},
multiply: function(amount) {
var notifier = Object.getNotifier(this);
notifier.performChange(Thingy.MULTIPLY, function() {
this.a *= amount;
this.b *= amount;
}, this);
notifier.notify({
object: this,
type: Thingy.MULTIPLY,
multiplied: amount
});
},
incrementAndMultiply: function(incAmount, multAmount) {
var notifier = Object.getNotifier(this);
notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
this.increment(incAmount);
this.multiply(multAmount);
}, this);
notifier.notify({
object: this,
type: Thingy.INCREMENT_AND_MULTIPLY,
incremented: incAmount,
multiplied: multAmount
});
}
}
myObserver2.callback = function(r){
r.forEach(function(change){
console.log('Observer 2', change);
})
}
myObserver.callback = function(changeRecords) {
changeRecords.forEach(function(change){
console.log({
changeType: change.type,
affectedObject: change.object,
affectedPropertyName: change.name,
valueBeforeChange: change.oldValue
});
});
myObserver.records = changeRecords;
myObserver.callbackCount++;
};
Thingy.observe = function(thingy, callback) {
// Object.observe(obj, callback, opt_acceptList)
Object.observe(thingy, callback, [Thingy.INCREMENT,
Thingy.MULTIPLY,
Thingy.INCREMENT_AND_MULTIPLY,
'updated']);
}
var thingy = new Thingy(2, 4);
Object.observe(thingy, myObserver.callback);
Thingy.observe(thingy, myObserver2.callback);
/*
thingy.increment(3); // { a: 5, b: 7 }
thingy.b++; // { a: 5, b: 8 }
thingy.multiply(2); // { a: 10, b: 16 }
thingy.a++; // { a: 11, b: 16 }
thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }
*/
Array splicing:
var model = ['Buy some milk', 'Learn to code', 'Wear some plaid'];
var count = 0;
Array.observe(model, function(changeRecords) {
count++;
changeRecords.forEach(function(change){
console.log({
changeType: change.type,
affectedObject: change.object,
affectedPropertyName: change.name,
valueBeforeChange: change.oldValue
});
}, count);
});
/*
model[0] = 'Skip this step';
model[1] = 'Paul Irish all the things';
*/
Deck shuffling:
function DeckSuit() {
this.push('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'A', 'Q', 'K');
}
DeckSuit.SHUFFLE = 'shuffle';
DeckSuit.prototype = {
__proto__: Array.prototype,
shuffle: function() {
var notifier = Object.getNotifier(this);
notifier.performChange(DeckSuit.SHUFFLE, function() {
this.reverse();
this.sort(function() { return Math.random()* 2 - 1; });
var cut = this.splice(0, 6);
Array.prototype.push.apply(this, cut);
this.reverse();
this.sort(function() { return Math.random()* 2 - 1; });
var cut = this.splice(0, 6);
Array.prototype.push.apply(this, cut);
this.reverse();
this.sort(function() { return Math.random()* 2 - 1; });
}, this);
notifier.notify({
object: this,
type: DeckSuit.SHUFFLE
});
},
}
DeckSuit.observe = function(thingy, callback) {
Object.observe(thingy, callback, [DeckSuit.SHUFFLE]);
}
DeckSuit.unobserve = function(thingy, callback) {
Object.unobserve(thingy);
}
function observer2(changes){
changes.forEach(function(change, i){
console.log(change);
/*
what property changed? change.name
how did it change? change.type
whats the current value? change.object[change.name]
*/
})
}
var deck = new DeckSuit;
DeckSuit.observe(deck, observer2);
deck.shuffle();
Circle with computed properties:
<h1>The world's simplest constraint-solver</h1>
<script src="weakmap.js"></script>
<script src="constrain.js"></script>
<script>
function Circle(radius) {
// circumference = 2*PI*radius
constrain(this, {
radius: function() { return this.circumference / (2*Math.PI); },
circumference: function() { return 2 * Math.PI * this.radius; }
});
// area = PI*r^2'
constrain(this, {
area: function() { return Math.PI * Math.pow(this.radius, 2); },
radius: function() { return Math.sqrt(this.area / Math.PI); }
});
if (radius)
this.radius = radius;
}
</script>
Circle with constraint solver:
<script src="weakmap.js"></script>
<script src="constrain.js"></script>
<!--<script src="persist.js"></script>-->
<script src="polymer.min.js"></script>
<h1>Circles</h1>
<template repeat="{{circles}}">
<div style="border: 1px solid black; margin: 8px">
<table>
<tr><td>radius:</td><td><input type="number" value="{{ radius }}"></td></tr>
<tr><td>area:</td><td><input type="number" value="{{ area }}"></td></tr>
<tr><td>circumference:</td><td><input type="number" value="{{ circumference }}"></td></tr>
</table>
<button onclick="deleteCircle()">Delete</button>
</div>
</template>
<button onclick="addCircle()">New</button>
<script>
var tmpl = document.querySelector('template');
var newBtn = document.getElementById("newCircle");
tmpl.model = { circles: [] };
function Circle(radius) {
// circumference = 2*PI*radius
constrain(this, {
radius: function() { return this.circumference / (2*Math.PI); },
circumference: function() { return 2 * Math.PI * this.radius; }
});
// area = PI*r^2'
constrain(this, {
area: function() { return Math.PI * Math.pow(this.radius, 2); },
radius: function() { return Math.sqrt(this.area / Math.PI); }
});
if (radius)
this.radius = radius;
}
function CircleController(elm) {
this.circles = elm.model.circles;
}
CircleController.prototype = {
delete: function(circle) {
var index = this.circles.indexOf(circle);
this.circles.splice(index, 1);
},
add: function() {
this.circles.push(new Circle());
}
}
var controller = new CircleController(tmpl);
function addCircle(){
controller.add();
}
function deleteCircle() {
controller.delete();
}
/*
var tmpl = document.querySelector('template');
tmpl.model.circles.forEach(function(c){
c.radius = c.radius * 2;
});
*/
</script>