From 4fd2faa352dd06dbd3726b3783ff147ba6212d46 Mon Sep 17 00:00:00 2001 From: Joel Kang Date: Thu, 28 Apr 2016 06:36:44 -0400 Subject: [PATCH] [WIP][Glimmer2] Add basic mut helper --- .../lib/components/curly-component.js | 47 ++++++----- .../lib/ember-views/attrs-support.js | 31 ++++++++ .../lib/ember-views/component.js | 2 + packages/ember-glimmer/lib/environment.js | 2 + packages/ember-glimmer/lib/helpers/mut.js | 77 +++++++++++++++++++ .../ember-glimmer/lib/utils/references.js | 50 ++++++++++++ .../components/curly-components-test.js | 37 +++++++++ 7 files changed, 221 insertions(+), 25 deletions(-) create mode 100644 packages/ember-glimmer/lib/ember-views/attrs-support.js create mode 100644 packages/ember-glimmer/lib/helpers/mut.js diff --git a/packages/ember-glimmer/lib/components/curly-component.js b/packages/ember-glimmer/lib/components/curly-component.js index 68bf85dd6b0..39e24770755 100644 --- a/packages/ember-glimmer/lib/components/curly-component.js +++ b/packages/ember-glimmer/lib/components/curly-component.js @@ -1,7 +1,7 @@ -import { StatementSyntax, ValueReference } from 'glimmer-runtime'; +import { StatementSyntax, ValueReference, EvaluatedArgs } from 'glimmer-runtime'; import { AttributeBindingReference, RootReference, applyClassNameBinding } from '../utils/references'; import { DIRTY_TAG } from '../ember-views/component'; -import EmptyObject from 'ember-metal/empty_object'; +import { COMPONENT_ARGS } from '../ember-views/attrs-support'; export class CurlyComponentSyntax extends StatementSyntax { constructor({ args, definition, templates }) { @@ -17,21 +17,6 @@ export class CurlyComponentSyntax extends StatementSyntax { } } -function attrsToProps(keys, attrs) { - let merged = new EmptyObject(); - - merged.attrs = attrs; - - for (let i = 0, l = keys.length; i < l; i++) { - let name = keys[i]; - let value = attrs[name]; - - merged[name] = value; - } - - return merged; -} - class ComponentStateBucket { constructor(component, args) { this.component = component; @@ -45,9 +30,10 @@ class CurlyComponentManager { let parentView = dynamicScope.view; let klass = definition.ComponentClass; - let attrs = args.named.value(); - let props = attrsToProps(args.named.keys, attrs); + let attrs = args.named.value(); + let props = new EvaluatedArgs(); + props[COMPONENT_ARGS] = args; props.renderer = parentView.renderer; let component = klass.create(props); @@ -119,13 +105,24 @@ class CurlyComponentManager { if (!args.tag.validate(argsRevision)) { bucket.argsRevision = args.tag.value(); - let attrs = args.named.value(); - let props = attrsToProps(args.named.keys, attrs); - - let oldAttrs = component.attrs; - let newAttrs = attrs; + let oldAttrs = new EvaluatedArgs(); + component[COMPONENT_ARGS].named.forEach((argName, arg) => { + oldAttrs[argName] = arg.lastValue(); + }); - component.setProperties(props); + let newAttrs = component[COMPONENT_ARGS].named.value(); + // Seems like this is a good place to notify all + // non-glimmer dependents that this value has changed + // but we're essentially trying to integrate the + // pull-based Glimmer reference system with the + // push-based system of the rest of Ember. + + // I don't actually know how to reconcile those things + // in my existing mental model of the world. + // I think we somehow need to establish a watcher on + // the references during create and notify it + // on mutableReference.compute()? + //component.notifyPropertyChange(COMPONENT_ARGS); component.trigger('didUpdateAttrs', { oldAttrs, newAttrs }); component.trigger('didReceiveAttrs', { oldAttrs, newAttrs }); diff --git a/packages/ember-glimmer/lib/ember-views/attrs-support.js b/packages/ember-glimmer/lib/ember-views/attrs-support.js new file mode 100644 index 00000000000..68a091fef22 --- /dev/null +++ b/packages/ember-glimmer/lib/ember-views/attrs-support.js @@ -0,0 +1,31 @@ +/** +@module ember +@submodule ember-views +*/ +import { Mixin } from 'ember-metal/mixin'; +import symbol from 'ember-metal/symbol'; + +export const COMPONENT_ARGS = symbol('ARGS'); + +export default Mixin.create({ + init() { + this._super(...arguments); + }, + + // Potentially do something about bound positionalArgs? *handwave* + setUnknownProperty(keyName, value) { + if (this[COMPONENT_ARGS] && this[COMPONENT_ARGS].named.has(keyName)) { + return this[COMPONENT_ARGS].named.get(keyName).update(value); + } else { + return this[keyName] = value; + } + }, + + unknownProperty(keyName) { + if (this[COMPONENT_ARGS] && this[COMPONENT_ARGS].named.has(keyName)) { + return this[COMPONENT_ARGS].named.get(keyName).value(); + } else { + return this[keyName]; + } + } +}); diff --git a/packages/ember-glimmer/lib/ember-views/component.js b/packages/ember-glimmer/lib/ember-views/component.js index 2766d0458e4..f3822fee244 100644 --- a/packages/ember-glimmer/lib/ember-views/component.js +++ b/packages/ember-glimmer/lib/ember-views/component.js @@ -1,6 +1,7 @@ import CoreView from 'ember-views/views/core_view'; import ChildViewsSupport from './child-views-support'; import ClassNamesSupport from './class-names-support'; +import AttrsSupport from './attrs-support'; import ViewStateSupport from 'ember-views/mixins/view_state_support'; import InstrumentationSupport from 'ember-views/mixins/instrumentation_support'; import AriaRoleSupport from 'ember-views/mixins/aria_role_support'; @@ -17,6 +18,7 @@ export default CoreView.extend( ClassNamesSupport, InstrumentationSupport, AriaRoleSupport, + AttrsSupport, ViewMixin, { isComponent: true, template: null, diff --git a/packages/ember-glimmer/lib/environment.js b/packages/ember-glimmer/lib/environment.js index 57508277265..9d95ff35023 100644 --- a/packages/ember-glimmer/lib/environment.js +++ b/packages/ember-glimmer/lib/environment.js @@ -29,6 +29,7 @@ import { default as log } from './helpers/log'; import { default as readonly } from './helpers/readonly'; import { default as unbound } from './helpers/unbound'; import { default as classHelper } from './helpers/-class'; +import { default as mut } from './helpers/mut'; import { OWNER } from 'container/owner'; const builtInHelpers = { @@ -40,6 +41,7 @@ const builtInHelpers = { loc, log, readonly, + mut, unbound, '-class': classHelper }; diff --git a/packages/ember-glimmer/lib/helpers/mut.js b/packages/ember-glimmer/lib/helpers/mut.js new file mode 100644 index 00000000000..4ffd0aa5e93 --- /dev/null +++ b/packages/ember-glimmer/lib/helpers/mut.js @@ -0,0 +1,77 @@ +import { MutableReference } from '../utils/references'; +import { assert } from 'ember-metal/debug'; +import { isConst } from 'glimmer-reference'; + +/** + The `mut` helper lets you __clearly specify__ that a child `Component` can update the + (mutable) value passed to it, which will __change the value of the parent component__. + + This is very helpful for passing mutable values to a `Component` of any size, but + critical to understanding the logic of a large/complex `Component`. + + To specify that a parameter is mutable, when invoking the child `Component`: + + ```handlebars + {{my-child childClickCount=(mut totalClicks)}} + ``` + + The child `Component` can then modify the parent's value as needed: + + ```javascript + // my-child.js + export default Component.extend({ + click() { + this.get('childClickCount').update(this.get('childClickCount').value + 1); + } + }); + ``` + + Additionally, the `mut` helper can be combined with the `action` helper to + mutate a value. For example: + + ```handlebars + {{my-child childClickCount=totalClicks click-count-change=(action (mut "totalClicks"))}} + ``` + + The child `Component` would invoke the action with the new click value: + + ```javascript + // my-child.js + export default Component.extend({ + click() { + this.get('clickCountChange')(this.get('childClickCount') + 1); + } + }); + ``` + + The `mut` helper changes the `totalClicks` value to what was provided as the action argument. + + See a [2.0 blog post](http://emberjs.com/blog/2015/05/10/run-up-to-two-oh.html#toc_the-code-mut-code-helper) for + additional information on using `{{mut}}`. + + @public + @method mut + @param {Object} [attr] the "two-way" attribute that can be modified. + @for Ember.Templates.helpers + @public +*/ + +export default { + isInternalHelper: true, + toReference(args) { + assert( + 'mut helper cannot be called with multiple params or hash params', + args.positional.values.length === 1 && !args.named.map + ); + + let source = args.positional.at(0); + + // isConst is probably not what we want, more like, isNotReference. + assert( + 'You can only pass in references to the mut helper, not primitive values', + !isConst(source) + ); + + return new MutableReference(source); + } +}; diff --git a/packages/ember-glimmer/lib/utils/references.js b/packages/ember-glimmer/lib/utils/references.js index dd1b684d638..2eae4d75434 100644 --- a/packages/ember-glimmer/lib/utils/references.js +++ b/packages/ember-glimmer/lib/utils/references.js @@ -1,3 +1,4 @@ +import { set } from 'ember-metal/property_set'; import { get } from 'ember-metal/property_get'; import { tagFor } from 'ember-metal/tags'; import { CURRENT_TAG, CONSTANT_TAG, VOLATILE_TAG, ConstReference, DirtyableTag, UpdatableTag, combine, isConst, referenceFromParts } from 'glimmer-reference'; @@ -5,6 +6,7 @@ import { ConditionalReference as GlimmerConditionalReference } from 'glimmer-run import emberToBool from './to-bool'; import { RECOMPUTE_TAG } from '../helper'; import { dasherize } from 'ember-runtime/system/string'; +import EmberError from 'ember-metal/error'; // FIXME: fix tests that uses a "fake" proxy (i.e. a POJOs that "happen" to // have an `isTruthy` property on them). This is not actually supported – @@ -61,6 +63,10 @@ class CachedReference extends EmberPathReference { this._lastRevision = null; } + lastValue() { + return this._lastValue; + } + // @abstract compute() } @@ -101,6 +107,11 @@ class PropertyReference extends CachedReference { // jshint ignore:line get(propertyKey) { return new PropertyReference(this, propertyKey); } + + update() { + let { _parentReference, _propertyKey } = this; + throw new EmberError(`The '${_propertyKey}' property of ${_parentReference.value()} is not mutable.`); + } } export class UpdatableReference extends EmberPathReference { @@ -121,6 +132,45 @@ export class UpdatableReference extends EmberPathReference { } } +// @implements PathReference, CachedReference +export class MutableReference extends PropertyReference { + constructor(sourceReference) { + super(sourceReference._parentReference, sourceReference._propertyKey); + } + + get(propertyKey) { + return new MutableReference(this._parentReference.get(propertyKey)); + } + + update(value) { + let { _parentReference, _propertyKey, _parentObjectTag } = this; + let parentValue = _parentReference.value(); + + // I actually don't know what Im doing here. + // Do we dirty the parent reference or the property reference (or both?) + + // What does this magical tagFor thing accomplish? + // If references are objects with values and tags, what is a tagFor a value? + let propertyTag = tagFor(this.get(_propertyKey)); + let returnVal = set(parentValue, _propertyKey, value); + propertyTag.dirty(); + _parentObjectTag.update(tagFor(parentValue)); + + // This part is the most dubious. It seems like I'm proxying fully + // the source reference that there's no real way of understanding + // the state of `this` mutable reference. Or does it not matter since + // its state _should_ be the state of the source reference. + + // If so, how else would I get the old value of a mutableReference? + // For example in didUpdateAttrs when we want to set the old value, + // if references are by definition lazily evaluated, does this concept + // even make sense? + this._lastRevision = propertyTag.value(); + + return returnVal; + } +} + // @implements PathReference export class GetHelperReference extends CachedReference { constructor(sourceReference, pathReference) { diff --git a/packages/ember-glimmer/tests/integration/components/curly-components-test.js b/packages/ember-glimmer/tests/integration/components/curly-components-test.js index 51af164b55e..f0767cb6501 100644 --- a/packages/ember-glimmer/tests/integration/components/curly-components-test.js +++ b/packages/ember-glimmer/tests/integration/components/curly-components-test.js @@ -1,4 +1,5 @@ /* globals EmberDev */ +import { get } from 'ember-metal/property_get'; import { set } from 'ember-metal/property_set'; import { observer } from 'ember-metal/mixin'; import { Component, compile } from '../../utils/helpers'; @@ -2359,4 +2360,40 @@ moduleFor('Components test: curly components', class extends RenderingTest { this.assertText('initial value'); } + + // TODO: Move to mutable-bindings-test or a better place + ['@test a simple mutable binding using `mut` inserts into the DOM'](assert) { + let component; + let FooBarComponent = Component.extend({ + init() { + this._super(...arguments); + component = this; + } + }); + + this.registerComponent('foo-bar', { + ComponentClass: FooBarComponent, + template: 'mut:{{mutFoo}} bound:{{boundFoo}} get:{{getFoo}}' + }); + + this.render('{{foo-bar mutFoo=(mut bar.baz) boundFoo=bar.baz getFoo=(get bar "baz")}}', { + bar: { + baz: 'hola' + } + }); + + this.assertText('mut:hola bound:hola get:hola'); + + this.assertStableRerender(); + + this.runTask(() => component.set('mutFoo', 'mutadios')); + + this.assertText('mut:mutadios bound:mutadios get:mutadios'); + assert.equal(get(this.context, 'bar.baz'), 'mutadios'); + + this.runTask(() => set(this.context, 'bar.baz', 'hola')); + + this.assertText('mut:hola bound:hola get:hola'); + assert.equal(get(component, 'mutFoo'), 'hola'); + } });