9

I would like to observe all changes on an object properties.
In the following example, i would like to be notified by the personChanged observer if firstname or lastname is changed.
BUT I would like to have something generic applied on all object properties (using Ember.keys() ??) How to replace 'firstname', 'lastname' with something more generic ?

In my example:

personChanged is called when firstname or lastname is changed:

 App.MyObject: Ember.Object.create({
   firstname: 'first',
   lastname: 'last',
   personChanged: Ember.observer(function() {
            console.log("person changed");
   }, 'firstname', 'lastname') 
 })
musically_ut
  • 34,028
  • 8
  • 94
  • 106
fvisticot
  • 7,936
  • 14
  • 49
  • 79
  • would this http://stackoverflow.com/questions/11623645/ember-js-observer-for-all-values/11679850#11679850 work for you ? – sly7_7 Dec 30 '12 at 21:01

2 Answers2

8

This is possible using an ObjectProxy in two flavours, depending on your requirements. Both approaches differ only in when and how many times is the observer called and both of them rely on Ember.keys.

The HTML for both the solutions is the same.

HTML

<script type="text/x-handlebars" data-template-name="app">
    Name: {{App.MyObject.firstname}} {{App.MyObject.lastname}}

    <ul>
    {{#each App.List}}
        <li>{{this}}</li>
    {{/each}}
    </ul>
</script>

Solution 1

JsFiddle: http://jsfiddle.net/2zxSq/

Javascript

App = Em.Application.create();

App.List = [];

App.MyObject = Em.ObjectProxy.create({
    // Your Original object, must be defined before 'init' is called, however.
    content: Em.Object.create({
        firstname: 'first',
        lastname:  'last'
    }),


    // These following two functions can be abstracted out to a Mixin
    init: function () {
        var self = this;
        Em.keys(this.get('content')).forEach(function (k) {
            Em.addObserver(self.get('content'), k, self, 'personChanged')
        });
    },

    // Manually removing the observers is necessary.
    willDestroy: function () {
        var self = this;
        Em.keys(this.get('content')).forEach(function (k) {
            Em.removeObserver(self.get('content'), k, self, 'personChanged');
        });
    },

    // The counter is for illustrative purpose only
    counter: 0,
    // This is the function which is called.
    personChanged: function () {
        // This function MUST be idempotent.
        this.incrementProperty('counter');
        App.List.pushObject(this.get('counter'));
        console.log('person changed');
    }
});

App.ApplicationView = Em.View.extend({
    templateName: 'app'
});

// Test driving the implementation.
App.MyObject.set('firstname', 'second');
App.MyObject.set('lastname',  'last-but-one');

App.MyObject.setProperties({
    'firstname': 'third',
    'lastname' : 'last-but-two'
});

While initialising MyObject, all properties which already exist on the content object are observed, and the function personChanged is called each time any of the property changes. However, since the observers are fired eagerly [1], the function personChanged should be idempotent, which the function in example is not. The next solution fixes this by making the observer lazy.

Solution 2

JsFiddle: http://jsfiddle.net/2zxSq/1/

Javascript

App.MyObject = Em.ObjectProxy.create({
    content: Em.Object.create({
        firstname: 'first',
        lastname:  'last'
    }),


    init: function () {
        var self = this;
        Em.keys(this.get('content')).forEach(function (k) {
            Em.addObserver(self, k, self, 'personChanged')
        });
    },

    willDestroy: function () {
        var self = this;
        Em.keys(this.get('content')).forEach(function (k) {
            Em.removeObserver(self, k, self, 'personChanged');
        });
    },

    // Changes from here 
    counter: 0,
    _personChanged: function () {
        this.incrementProperty('counter');
        App.List.pushObject(this.get('counter'));
        console.log('person changed');
    },

    // The Actual function is called via Em.run.once
    personChanged: function () {
        Em.run.once(this, '_personChanged');
    }
});

The only change here is that the actual observer function is now called only at the end of the Ember Run loop, which might be the behaviour you are looking for.

Other notes

These solutions use ObjectProxy instead of defining the observers on the object itself to avoid setting spurious observers (on properties such as init, willDestroy, etc.) or an explicit list of properties to observe.

This solution can be extended to start observing dynamic properties by overriding the setUnknownProperty on the proxy to add an observer every time a key is added to content. The willDestroy will remain the same.

Reference

[1] This might get changed soon thanks to Asyn Observers

Community
  • 1
  • 1
musically_ut
  • 34,028
  • 8
  • 94
  • 106
2

What you have there is right using inline observers, but you just have some syntax errors. However it is even simpler to do with the key word observes. So I have tweaked your example a little below.

App.MyObject: Ember.Object.create({
    firstname: 'first',
    lastname: 'last',
    personChanged: function() {
        //firstname or lastname changed
    }.observes('firstname','lastname')
});

Note: I put quotes around the properties

Source: http://emberjs.com/guides/object-model/observers/

JustKash
  • 687
  • 2
  • 13
  • 28
  • 1
    Tx but my question was to get a generic solution to observe all object attributes (without writing the list of attributes in the observe method) – fvisticot Dec 29 '12 at 08:46
  • Sorry but there is no way to do that unless you store all your properties in an array or one object and observe that and then do some manual calculations to retrieve the values. – JustKash Dec 29 '12 at 20:38