81

I have this code in js bin:

var validator = {
  set (target, key, value) {
    console.log(target);
    console.log(key);
    console.log(value);
    if(isObject(target[key])){

    }
    return true
  }
}


var person = {
      firstName: "alfred",
      lastName: "john",
      inner: {
        salary: 8250,
        Proffesion: ".NET Developer"
      }
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'

if i do proxy.inner.salary = 555; it does not work.

However if i do proxy.firstName = "Anne", then it works great.

I do not understand why it does not work Recursively.

http://jsbin.com/dinerotiwe/edit?html,js,console

1
  • 2
    Nested means "multiple objects", which means that you need multiple proxies to detect all property accesses on every object not only the root one. Commented Dec 23, 2016 at 11:09

5 Answers 5

107

You can add a get trap and return a new proxy with validator as a handler:

var validator = {
  get(target, key) {
    if (typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], validator)
    } else {
      return target[key];
    }
  },
  set (target, key, value) {
    console.log(target);
    console.log(key);
    console.log(value);
    return true
  }
}


var person = {
      firstName: "alfred",
      lastName: "john",
      inner: {
        salary: 8250,
        Proffesion: ".NET Developer"
      }
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'

Sign up to request clarification or add additional context in comments.

7 Comments

Thanks, What if target[key] is an Array of objects? I guess we can map validator?
@robertking Array is an object too, so it's an object inside an object, and this code should work with deeply nested objects.
Dates and Arrays didn't quite work for me, perhaps because angular ngFor and datePipes use the prototype properties. I've posted my modified solution below which seems to work ok
Thanks. But this way each time it returns a new Proxy instance. Is there anyway to return the same Proxy instance if it's created?
would this work if i did const inner = person.inner; inner.salary = 1000; ? (edit: oh i see.. the proxy would be returned from the first get now, so inner would be a proxy. cool. Are there any other drawbacks or loopholes to this or can it be expected to work 100% with any amount of deeply nested objects?)
|
32

A slight modification on the example by Michał Perłakowski with the benefit of this approach being that the nested proxy is only created once rather than every time a value is accessed.

If the property of the proxy being accessed is an object or array, the value of the property is replaced with another proxy. The isProxy property in the getter is used to detect whether the currently accessed object is a proxy or not. You may want to change the name of isProxy to avoid naming collisions with properties of stored objects.

Note: the nested proxy is defined in the getter rather than the setter so it is only created if the data is actually used somewhere. This may or may not suit your use-case.

const handler = {
  get(target, key) {
    if (key == 'isProxy')
      return true;

    const prop = target[key];

    // return if property not found
    if (typeof prop == 'undefined')
      return;

    // set value as proxy if object
    if (!prop.isProxy && typeof prop === 'object')
      target[key] = new Proxy(prop, handler);

    return target[key];
  },
  set(target, key, value) {
    console.log('Setting', target, `.${key} to equal`, value);

    // todo : call callback

    target[key] = value;
    return true;
  }
};

const test = {
  string: "data",
  number: 231321,
  object: {
    string: "data",
    number: 32434
  },
  array: [
    1, 2, 3, 4, 5
  ],
};

const proxy = new Proxy(test, handler);

console.log(proxy);
console.log(proxy.string); // "data"

proxy.string = "Hello";

console.log(proxy.string); // "Hello"

console.log(proxy.object); // { "string": "data", "number": 32434 }

proxy.object.string = "World";

console.log(proxy.object.string); // "World"

5 Comments

i believe .isBindingProxy should be ,isProxy ?
If you're using Node v10+, you can also use util.types.isProxy instead of manually "setting" isProxy
For browser approach, A suggestion: declare: const isProxy = Symbol("isProxy"). Then use key === isProxy instead.
an explanation as to how isProxy is set would be useful, I think
@RichardHunter isProxy is never explicitly set on any of the root proxy's nested objects. If an object is not yet a proxy, it's assumed that it will not have a isProxy property and execution will move to line 14 where target[key] = new Proxy(...). If an object is a proxy, when the isProxy property is accessed - like in the if (!prop.isProxy ...) check on line 13 - execution will find its way to that proxy's get() trap as defined in handler, which returns true when key == isProxy. Line 13 then fails and execution moves to line 16 which returns target[key] - the proxy.
17

I published a library on GitHub that does this as well. It will also report to a callback function what modifications have taken place along with their full path.

Michal's answer is good, but it creates a new Proxy every time a nested object is accessed. Depending on your usage, that could lead to a very large memory overhead.

3 Comments

This is something I spotted too, Proxy's are of course Objects, and there is no way to tell if an Object is a Proxy,.. The way I've got around this is keep track of the proxies inside a WeakMap..
I tried to access proxy.inner.salary 100 Million times and didn't see any Memory rising. I think this answer is just not true and puts bad reputation on Michal's answer. Garbage collection seems to work in this case.
@KilianHertel Take a look at the first if statement in Michal's answer. It creates a new Proxy if the accessed property is a not null object. So of course, depending on your usage, creating a bunch of new Proxy objects could very well result in increased memory usage. Your mileage will vary depending on garbage collection. The other answer provided by James also addresses that very issue. Why does my answer "put bad reputation" on Michal's answer? I said his answer is good and I even upvoted it myself...
4

I have also created a library type function for observing updates on deeply nested proxy objects (I created it for use as a one-way bound data model). Compared to Elliot's library it's slightly easier to understand at < 100 lines. Moreover, I think Elliot's worry about new Proxy objects being made is a premature optimisation, so I kept that feature to make it simpler to reason about the function of the code.

observable-model.js

let ObservableModel = (function () {
    /*
    * observableValidation: This is a validation handler for the observable model construct.
    * It allows objects to be created with deeply nested object hierarchies, each of which
    * is a proxy implementing the observable validator. It uses markers to track the path an update to the object takes
    *   <path> is an array of values representing the breadcrumb trail of object properties up until the final get/set action
    *   <rootTarget> the earliest property in this <path> which contained an observers array    *
    */
    let observableValidation = {
        get(target, prop) {
            this.updateMarkers(target, prop);
            if (target[prop] && typeof target[prop] === 'object') {
                target[prop] = new Proxy(target[prop], observableValidation);
                return new Proxy(target[prop], observableValidation);
            } else {
                return target[prop];
            }
        },
        set(target, prop, value) {
            this.updateMarkers(target, prop);
            // user is attempting to update an entire observable field
            // so maintain the observers array
            target[prop] = this.path.length === 1 && prop !== 'length'
                ? Object.assign(value, { observers: target[prop].observers })
                : value;
            // don't send events on observer changes / magic length changes
            if(!this.path.includes('observers') && prop !== 'length') {
                this.rootTarget.observers.forEach(o => o.onEvent(this.path, value));
            }
            // reset the markers
            this.rootTarget = undefined;
            this.path.length = 0;
            return true;
        },
        updateMarkers(target, prop) {
            this.path.push(prop);
            this.rootTarget = this.path.length === 1 && prop !== 'length'
                ? target[prop]
                : target;
        },
        path: [],
        set rootTarget(target) {
            if(typeof target === 'undefined') {
                this._rootTarget = undefined;
            }
            else if(!this._rootTarget && target.hasOwnProperty('observers')) {
                this._rootTarget = Object.assign({}, target);
            }
        },
        get rootTarget() {
            return this._rootTarget;
        }
    };

    /*
    * create: Creates an object with keys governed by the fields array
    * The value at each key is an object with an observers array
    */
    function create(fields) {
        let observableModel = {};
        fields.forEach(f => observableModel[f] = { observers: [] });
        return new Proxy(observableModel, observableValidation);
    }

    return {create: create};
})();

It's then trivial to create an observable model and register observers:

app.js

// give the create function a list of fields to convert into observables
let model = ObservableModel.create([
    'profile',
    'availableGames'
]);

// define the observer handler. it must have an onEvent function
// to handle events sent by the model
let profileObserver = {
    onEvent(field, newValue) {
        console.log(
            'handling profile event: \n\tfield: %s\n\tnewValue: %s',
            JSON.stringify(field),
            JSON.stringify(newValue));
    }
};

// register the observer on the profile field of the model
model.profile.observers.push(profileObserver);

// make a change to profile - the observer prints:
// handling profile event:
//        field: ["profile"]
//        newValue: {"name":{"first":"foo","last":"bar"},"observers":[{}
// ]}
model.profile = {name: {first: 'foo', last: 'bar'}};

// make a change to available games - no listeners are registered, so all
// it does is change the model, nothing else
model.availableGames['1234'] = {players: []};

Hope this is useful!

Comments

2

I wrote a function based on Michał Perłakowski code. I added access to the path of property in the set/get functions. Also, I added types.

    const createHander = <T>(path: string[] = []) => ({
        get: (target: T, key: keyof T): any => {
            if (key == 'isProxy') return true;
            if (typeof target[key] === 'object' && target[key] != null)
                return new Proxy(
                    target[key],
                    createHander<any>([...path, key as string])
                );
            return target[key];
        },
        set: (target: T, key: keyof T, value: any) =>  {
            console.log(`Setting ${[...path, key]} to: `, value);
            target[key] = value;
            return true;
        }
    });
    
    const proxy = new Proxy(obj ,createHander<ObjectType>());

1 Comment

Where's the callback that attached to the Proxy?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.