Last active

Embed URL

HTTPS clone URL

SSH clone URL

You can clone with HTTPS or SSH.

Download Gist

Object.observe() examples from my talk

View examples.md

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
Something went wrong with that request. Please try again.