Skip to content

Instantly share code, notes, and snippets.

@addyosmani
Last active February 23, 2016 18:22
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save addyosmani/6951740 to your computer and use it in GitHub Desktop.
Save addyosmani/6951740 to your computer and use it in GitHub Desktop.
Object.observe() examples from my talk

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment