jeudi 30 avril 2020

Nested JS decorator get/set's, how to properly chain them?

The ember framework has adopted decorators aggressively. In order to utilize data binding now i have to decorate my properties with @tracked which gets me all my nice UI updates anytime i change a property.

     @tracked username = 'dave';

This works well, but i'm encountering some serious problems if i need to add a custom decorator on top of the tracked decorator.

    @typed(StateTrackMap)
    @tracked
    mapConfigsArray = [create(StateTrackMap)];

I'm able to get this to work by having my @typed decorator check to see if it is above another decorator or not.

export default function typed(classType) {
    let weak = new WeakMap();
    return function(object, property, descriptor) {
        return {
            get() {
                // Check if there is another decorator attached below us in the chain
                // i.e. "tracked"
                if (typeof descriptor.get == 'function') {
                    return descriptor.get.call(this);
                }
                // If we haven't initialized before but there is one ready, return that
                if (!weak.has(this) && typeof descriptor.initializer == 'function') {
                    weak.set(this, descriptor.initializer.call(this));
                }
                return weak.get(this);
            },
            set(value) {
                // my set code which does the type checking/converting this descriptor is for

                                // Apply the converted value to the lower level object
                // This may be the object itself, or it may be another setter in the chain
                if (typeof descriptor.set == 'function') {
                    descriptor.set.call(this, typedValue);
                } else {
                    return weak.set(this, typedValue);
                }
            }
        }
    }
}

But this feels, weird... and doesn't look like any of the usages of descriptors i've seen. Mostly because if i change the order of the decorators things explode

    @tracked
    @typed(StateTrackMap)
    mapConfigsArray = [create(StateTrackMap)];
index.js:172 Uncaught Error: Assertion Failed: The options object passed to tracked() may only contain a 'value' or 'initializer' property, not both.

So i guess my question is, what is the proper way to chain decorators that have get & set? It seems to me that the order of the decorators determines if i can go up/down the chain or not. Also it seems to me that this chaining logic has to be baked into every decorator or else it doesn't work. Is there some generic way i can pass decorators to other decorators?

I've seen some examples where i return the descriptor reference but that doesn't appear to help the problem here either as i am not quite sure how i can still inject my get/set on it without erasing the property property chain or getting into the same boat as above where my code has to be designed to work with other descriptors specifically.

export default function typed(classType) {
    return function(object, property, descriptor) {
        const set = descriptor.set;
        const get = descriptor.get;
        const weak = new WeakMap();

        descriptor.get = function() {
            if (typeof get == 'function') {
                return get.call(this);
            }
            // If we haven't initialized before but there is one ready, return that
            if (!weak.has(this) && typeof descriptor.initializer == 'function') {
                weak.set(this, descriptor.initializer.call(this));
            }
            return weak.get(this);
        }
        descriptor.set = function(value) {
            // My type checking / conversion code

            // Apply the converted value to the lower level object
            // This may be the object itself, or it may be another setter in the chain
            if (typeof set == 'function') {
                set.call(this, typedValue);
            } else {
                return weak.set(this, typedValue);
            }
        }
        return descriptor;
    }
}

BTW this method gives a different explosion.

Assertion Failed: You attempted to use @tracked on mapConfigsArray, but that element is not a class field.



Aucun commentaire:

Enregistrer un commentaire