Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 22 additions & 25 deletions packages/ember-glimmer/lib/components/curly-component.js
Original file line number Diff line number Diff line change
@@ -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 }) {
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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 });
Expand Down
31 changes: 31 additions & 0 deletions packages/ember-glimmer/lib/ember-views/attrs-support.js
Original file line number Diff line number Diff line change
@@ -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];
}
}
});
2 changes: 2 additions & 0 deletions packages/ember-glimmer/lib/ember-views/component.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,6 +18,7 @@ export default CoreView.extend(
ClassNamesSupport,
InstrumentationSupport,
AriaRoleSupport,
AttrsSupport,
ViewMixin, {
isComponent: true,
template: null,
Expand Down
2 changes: 2 additions & 0 deletions packages/ember-glimmer/lib/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -40,6 +41,7 @@ const builtInHelpers = {
loc,
log,
readonly,
mut,
unbound,
'-class': classHelper
};
Expand Down
77 changes: 77 additions & 0 deletions packages/ember-glimmer/lib/helpers/mut.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
50 changes: 50 additions & 0 deletions packages/ember-glimmer/lib/utils/references.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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';
import { ConditionalReference as GlimmerConditionalReference } from 'glimmer-runtime';
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 –
Expand Down Expand Up @@ -61,6 +63,10 @@ class CachedReference extends EmberPathReference {
this._lastRevision = null;
}

lastValue() {
return this._lastValue;
}

// @abstract compute()
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
}
});