Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is .ATTRS really necessary? #80

Closed
zenorocha opened this issue Apr 6, 2015 · 23 comments
Closed

Is .ATTRS really necessary? #80

zenorocha opened this issue Apr 6, 2015 · 23 comments
Labels

Comments

@zenorocha
Copy link
Contributor

I know we're based in YUI's attribute model but I wonder if having this static variable really adds any value.

@zenorocha zenorocha added the api label Apr 6, 2015
@zenorocha
Copy link
Contributor Author

A suggestion for it would be using ES7 Class Properties which are available on Babel 5.

@yuchi
Copy link
Contributor

yuchi commented Apr 14, 2015

Also everything (at least everything I can think of) could be accomplished by a combo of Class Properties and Decorators.

@eduardolundgren
Copy link
Contributor

@yuchi, could you give an example how we can reach similar attribute definition with properties/decorators?

I would love to get rid of "ATTRS".

Thank you.

@yuchi
Copy link
Contributor

yuchi commented Apr 14, 2015

class Attribute {}

class Component extends Attribute {

  // Simple property with default value
  valueAttribute = 42;

  // Simple property with default `valueFn`
  valueFnAttribute = this.valueFn();

  // Purely dynamic value
  get purelyDynamicValue() {
    return this._storedValue;
  }
  set purelyDynamicValue(v) {
    this._storedValue;
  }

  // Lazy valueFn behaviour
  @memoize
  get lazyValue() {
    console.log('Fires once');
    return 32;
  }

  // Write once
  @writeOnce
  get stickyValue() {
    return this._stickyValue;
  }
  set stickyValue(v) {
    this._stickyValue = v;
  }

  // Methods

  valueFn() {
    return 42 * 2;
  }
}

const c = new Component();

// Will log "Fires once"… well… once!
assert(c.lazyValue === 32);
assert(c.lazyValue === 32);
assert(c.lazyValue === 32);

// Will log "Writes once"… well, you got it.
c.stickyValue = 42;
c.stickyValue = 32;
assert(c.stickyValue === 42);

// Decorators

function writeOnce(host, name, descriptor) {
  const { get, set, enumerable, configurable } = descriptor;
  const result = { ...descriptor };
  const run = Symbol(name + '-has-run');

  if (set) {
    result.set = function writeOnce(...args) {
      if (this[ run ] === true) return;
      this[ run ] = true;
      set.apply(this, args);
    };
  }

  return result;
}

function memoize(host, name, descriptor) {
  const { get, set, enumerable, configurable } = descriptor;
  const result = { ...descriptor };

  if (get) {
    result.get = function memoizedGet(...args) {
      const value = get.apply(this, args);
      Object.defineProperty(this, name, { value, enumerable, configurable });
      return value;
    }
  }

  return result;
}

function assert(a) {
  if (!a) throw new Error("Assertion failed");
}

@yuchi
Copy link
Contributor

yuchi commented Apr 14, 2015

(Very off the envelope examples, took exactly 9 minutes to write that down)

@yuchi
Copy link
Contributor

yuchi commented Apr 14, 2015

(Run it in the Babel REPL with this huuuuuge link: http://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&playground=false&code=class%20Attribute%20%7B%7D%0A%0Aclass%20Component%20extends%20Attribute%20%7B%0A%0A%20%20%2F%2F%20Simple%20property%20with%20default%20value%0A%20%20valueAttribute%20%3D%2042%3B%0A%0A%20%20%2F%2F%20Simple%20property%20with%20default%20%60valueFn%60%0A%20%20valueFnAttribute%20%3D%20this.valueFn()%3B%0A%0A%20%20%2F%2F%20Purely%20dynamic%20value%0A%20%20get%20purelyDynamicValue()%20%7B%0A%20%20%20%20return%20this._storedValue%3B%0A%20%20%7D%0A%20%20set%20purelyDynamicValue(v)%20%7B%0A%20%20%20%20this._storedValue%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20Lazy%20valueFn%20behaviour%0A%20%20%40memoize%0A%20%20get%20lazyValue()%20%7B%0A%20%20%20%20console.log('Fires%20once')%3B%0A%20%20%20%20return%2032%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20Write%20once%0A%20%20%40writeOnce%0A%20%20get%20stickyValue()%20%7B%0A%20%20%20%20return%20this._stickyValue%3B%0A%20%20%7D%0A%20%20set%20stickyValue(v)%20%7B%0A%20%20%20%20this._stickyValue%20%3D%20v%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20Methods%0A%20%20%0A%20%20valueFn()%20%7B%0A%20%20%20%20return%2042%20*%202%3B%0A%20%20%7D%0A%7D%0A%0Aconst%20c%20%3D%20new%20Component()%3B%0A%0A%2F%2F%20Will%20log%20%22Fires%20once%22%E2%80%A6%20well%E2%80%A6%20once!%0Aassert(c.lazyValue%20%3D%3D%3D%2032)%3B%0Aassert(c.lazyValue%20%3D%3D%3D%2032)%3B%0Aassert(c.lazyValue%20%3D%3D%3D%2032)%3B%0A%0A%2F%2F%20Will%20log%20%22Writes%20once%22%E2%80%A6%20well%2C%20you%20got%20it.%0Ac.stickyValue%20%3D%2042%3B%0Ac.stickyValue%20%3D%2032%3B%0Aassert(c.stickyValue%20%3D%3D%3D%2042)%3B%0A%0A%2F%2F%20Decorators%0A%0Afunction%20writeOnce(host%2C%20name%2C%20descriptor)%20%7B%0A%20%20const%20%7B%20get%2C%20set%2C%20enumerable%2C%20configurable%20%7D%20%3D%20descriptor%3B%0A%20%20const%20result%20%3D%20%7B%20...descriptor%20%7D%3B%0A%20%20const%20run%20%3D%20Symbol(name%20%2B%20'-has-run')%3B%0A%0A%20%20if%20(set)%20%7B%0A%20%20%20%20result.set%20%3D%20function%20writeOnce(...args)%20%7B%0A%20%20%20%20%20%20if%20(this%5B%20run%20%5D%20%3D%3D%3D%20true)%20return%3B%0A%20%20%20%20%20%20this%5B%20run%20%5D%20%3D%20true%3B%0A%20%20%20%20%20%20set.apply(this%2C%20args)%3B%0A%20%20%20%20%7D%3B%0A%20%20%7D%0A%0A%20%20return%20result%3B%0A%7D%0A%0Afunction%20memoize(host%2C%20name%2C%20descriptor)%20%7B%0A%20%20const%20%7B%20get%2C%20set%2C%20enumerable%2C%20configurable%20%7D%20%3D%20descriptor%3B%0A%20%20const%20result%20%3D%20%7B%20...descriptor%20%7D%3B%0A%0A%20%20if%20(get)%20%7B%0A%20%20%20%20result.get%20%3D%20function%20memoizedGet(...args)%20%7B%0A%20%20%20%20%20%20const%20value%20%3D%20get.apply(this%2C%20args)%3B%0A%20%20%20%20%20%20Object.defineProperty(this%2C%20name%2C%20%7B%20value%2C%20enumerable%2C%20configurable%20%7D)%3B%0A%20%20%20%20%20%20return%20value%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20return%20result%3B%0A%7D%0A%0Afunction%20assert(a)%20%7B%0A%20%20if%20(!a)%20throw%20new%20Error(%22Assertion%20failed%22)%3B%0A%7D )

@yuchi
Copy link
Contributor

yuchi commented Apr 14, 2015

By the way the return-based setters and parameter-based getters are not shown.

An (untested) implementation could be the following one:

const MY_ENUM = [ 'simple', 'complex', 'idiotic' ];
class Component {
  @attribute
  set myAttribute(newValue, oldValue) { return MY_ENUM.indexOf(newValue); }
  get myAttribute(value) { return MY_ENUM[ value ] || 'idiotic' }
}

function attribute(host, name, descriptor) {
  const { get, set, enumerable, configurable } = descriptor;
  const result = { ...descriptor };
  const storage = Symbol( name + '-storage' );

  result.get = function attributeGet() {
    return get.call(this, this[ storage ]);
  };
  result.set = function attributeSet(v) {
    this[ storage ] = set.call(this, v, this[ storage ]);
  };

  return result;
}

@yuchi
Copy link
Contributor

yuchi commented Apr 15, 2015

Tested it, setter can have only one parameter (why?!) so:

const MY_ENUM = [ 'simple', 'complex', 'idiotic' ];
class Component {
  @attribute
  set myAttribute({ newValue, oldValue }) { return MY_ENUM.indexOf(newValue); }
  get myAttribute(value) { return MY_ENUM[ value ] || 'idiotic' }
}

function attribute(host, name, descriptor) {
  const { get, set, enumerable, configurable } = descriptor;
  const result = { ...descriptor };
  const storage = Symbol( name + '-storage' );

  result.get = function attributeGet() {
    return get.call(this, this[ storage ]);
  };
  result.set = function attributeSet(v) {
    this[ storage ] = set.call(this, { newValue: v, oldValue: this[ storage ] });
  };

  return result;
}

var c = new Component();

console.log(c.myAttribute);
console.log(c.myAttribute = 'lol');
console.log(c.myAttribute);
console.log(c.myAttribute = 'complex');
console.log(c.myAttribute);

@eduardolundgren
Copy link
Contributor

LGTM. We are going to analyze how we can fit that into the API.
Thank you @yuchi.
/cc @mairatma

@natecavanaugh
Copy link

I think my only concerns with decorators (which I do think would be able to solve most of the use cases) would be around sharing those functions, or using them as a reference from either a static method on the Attribute class, or as imported functions.

Overall, though, I think it could work to use them. It seems like you should be able to use any arbitrary reference as a decorator, but do you know for sure?

@yuchi
Copy link
Contributor

yuchi commented Apr 15, 2015

Decorators is just sugar over property descriptor manipulation. The ideas I showed are almost all get/set oriented.

Decorators become great when used in class properties though, so we can have @validators and such.

@yuchi
Copy link
Contributor

yuchi commented Apr 15, 2015

Also, small note to future self:

import validator from 'attribute';

class Component {
  @validator.string // currently works perfectly on babel
  myAwesomeStringOnlyProp = '';
}

@ipeychev
Copy link
Contributor

Hey @yuchi,

Could you please explain how exactly you was going to implement the validator above?

Also, did you consider the case when validator's function might be part of the class's instance? For example:

import validator from 'attribute';

class Component {
 validateMyAwesomeStringOnlyProp(val) {
    return typeof val === 'string';
 }
  @validator // how can you use validateMyAwesomeStringOnlyProp as validator?
  myAwesomeStringOnlyProp = '';
}

@yuchi
Copy link
Contributor

yuchi commented May 18, 2015

Probably pseudo code:

// attribute.js

export default function validator(fn) {
  return (target, name, { value, initializer, enumerable, configurable }) => {
    return {
      enumerable, configurable,
      get() {
        return retrieve(this, name, initializer, value);
      },
      set(newValue) {
        if (fn(newValue)) store(this, name, newValue);
      }
    };
  };
}

validator.string = validator(newValue => typeof newValue === 'string');
validator.number = validator(newValue => typeof newValue === 'number');
// etc…

function store(instance, name, value) {
  instance[ '_' + name ] = value;
}

function retrieve(instance, name, initializer, defValue) {
  const key = '_' + name;
  if (key in instance) return instance[ key ];
  else if (initializer) return initializer();
  else return defValue;
}

// example.js

import validator from 'validator';

class Example {
  @validator(value => value % 2 === 0)
  prop = 42;
}

const example = new Example();

console.log(example.prop); // logs 42

example.prop = 44;

console.log(example.prop); // logs 44

example.prop = 41;

console.log(example.prop); // logs 44

@ipeychev
Copy link
Contributor

Okay, thanks! Yesterday I played with this and I couldn't imagine a way to achieve validator without an internal property. As I see, you did the same, so we are on the same page.

Just FYI - we are trying to find a way to share code among modules. Class annotations are one option. If we can combine this with getting rid of ATTRS, it would be awesome, wouldn't it?

@ipeychev
Copy link
Contributor

Okay, so I have a working prototype of these. It seems the are at least two ways to achieve this, so the question is, which one would you guys prefer? Please vote for:

Option 1)

import Attribute from 'attribute';
import attr from 'attribute/attr';

class Test extends Attribute {
    @attr({
        validator: core.isString,
        writeOnce: true
    })
    prop = 42;
}

or

Option 2)

import Attribute from 'attribute';
import validator from 'attribute/Validator';
import writeOnce from 'attribute/WriteOnce';

class Test extends Attribute {
    @validator(core.isString)
    @writeOnce
    prop = 42;
}

@yuchi @natecavanaugh @mairatma @zenorocha @eduardolundgren

@yuchi
Copy link
Contributor

yuchi commented May 19, 2015

I’m in favor of Option 2, for the following reasons:

  • possible extraction as a standalone module (outside of Metal)

  • adherence to the language

  • composability, see example:

    class Test {
      @validator.string
      @validator.maxLength(42)
      @sanifier.maxLength(42)
      prop = 'hi'
    }

@yuchi
Copy link
Contributor

yuchi commented May 19, 2015

Can you share your version of writeOnce?

@ipeychev
Copy link
Contributor

This is top secret - if I tell you, I will have to kill you.

(will commit soon, wait a bit)

ipeychev added a commit to ipeychev/metal.js that referenced this issue May 19, 2015
@mairatma
Copy link
Contributor

Me and @ipeychev were talking today and we realized that we also need some kind of a @attr annotation just for cases where no other attribute annotation will be added but we want to mark the variable as an attribute. For example, if we want an attribute that doesn't have validator or writeOnce. It could look something like this:

class Test extends Attribute {
    @attr
    prop = 42;
}

But if any attribute annotation is already used, we don't need the @attr annotation to be required. So the previous example would still work. What do you guys think?

@yuchi
Copy link
Contributor

yuchi commented May 20, 2015

We shooting to different stars here. While I still advocate for annotations to define pieces of the ATTRS configuration, what I really meant was to ditch it completely and use dynamic (get/set) properties.

So the annotations would just be a standard library for JS instead of configuration sugar for this specific framework.

@ipeychev
Copy link
Contributor

On theory, we could go in this direction. However, this will be a lot of work, we should rewrite the Attribute class, to change the EventEmitter and so on.
Moreover, to rely only and entirely on class properties right now is risky. They are not part of the language, they are just Stage 0 proposal and exist only in Babel (and please keep in mind they didn't exist at all when we started with the framework).

That's why we thought that we can create some kind of sugar on top of what we already have.
When you create a component, you will extend from Component and use the annotations to specify which of the variables are attributes (for them you will receive change events and so on). You will not add ATTRS at all in this case.

@mairatma
Copy link
Contributor

mairatma commented Jul 5, 2016

We'll keep this notation for now (which is STATE now instead of ATTRS). Many people are using it now and not finding it a problem. When working with soy templates you almost never need to specify this btw, since we create state attributes automatically from the template params.

I'll close this for now, but feel free to reopen if you think we should still discuss it. (just make sure to reopen on metal/metal instead, we've moved there.

@mairatma mairatma closed this as completed Jul 5, 2016
jbalsas added a commit that referenced this issue Jan 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants