diff --git a/packages/ember-htmlbars/tests/helpers/closure_component_test.js b/packages/ember-htmlbars/tests/helpers/closure_component_test.js index d6d5da4f0d9..04807e63259 100644 --- a/packages/ember-htmlbars/tests/helpers/closure_component_test.js +++ b/packages/ember-htmlbars/tests/helpers/closure_component_test.js @@ -1,6 +1,7 @@ import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import ComponentLookup from 'ember-views/component_lookup'; import Component from 'ember-views/components/component'; +import EventDispatcher from 'ember-views/system/event_dispatcher'; import compile from 'ember-template-compiler/system/compile'; import run from 'ember-metal/run_loop'; import isEmpty from 'ember-metal/is_empty'; @@ -614,3 +615,156 @@ QUnit.test('adding parameters to a closure component\'s instance does not add it runAppend(component); equal(component.$().text(), 'Foo', 'there is only one Foo'); }); + +QUnit.test('parameters in a closure are mutable when closure is a param', function(assert) { + let dispatcher = EventDispatcher.create(); + dispatcher.setup(); + + let ChangeButton = Component.extend().reopenClass({ + positionalParams: ['val'] + }); + + owner.register( + 'component:change-button', + ChangeButton + ); + owner.register( + 'template:components/change-button', + compile('') + ); + + let template = compile('{{component (component "change-button" val2)}}{{val2}}'); + + component = Component.extend({ + [OWNER]: owner, + template + }).create({ + val2: 8 + }); + + runAppend(component); + + assert.equal(component.$('.value').text(), '8', 'initial state is right'); + + run(() => component.$('.my-button').click()); + + assert.equal(component.$('.value').text(), '10', 'Value gets updated'); + + runDestroy(dispatcher); +}); + +QUnit.test('parameters in a closure are mutable when closure is in a nested param', function(assert) { + let dispatcher = EventDispatcher.create(); + dispatcher.setup(); + + let ChangeButton = Component.extend().reopenClass({ + positionalParams: ['val'] + }); + + owner.register( + 'component:change-button', + ChangeButton + ); + owner.register( + 'template:components/change-button', + compile('') + ); + + owner.register( + 'component:my-comp', + Component.extend().reopenClass({ + positionalParams: ['components'] + }) + ); + owner.register( + 'template:components/my-comp', + compile('{{component components.comp}}') + ); + + let template = compile('{{my-comp (hash comp=(component "change-button" val2))}}{{val2}}'); + + component = Component.extend({ + [OWNER]: owner, + template + }).create({ + val2: 8 + }); + + runAppend(component); + + assert.equal(component.$('.value').text(), '8', 'initial state is right'); + + run(() => component.$('.my-button').click()); + + assert.equal(component.$('.value').text(), '10', 'Value gets updated'); + + runDestroy(dispatcher); +}); + +QUnit.test('parameters in a closure are mutable when closure is a hash value', function(assert) { + let dispatcher = EventDispatcher.create(); + dispatcher.setup(); + + owner.register( + 'template:components/change-button', + compile('') + ); + + owner.register( + 'template:components/my-comp', + compile('{{component component}}') + ); + + let template = compile('{{my-comp component=(component "change-button" val=val2)}}{{val2}}'); + + component = Component.extend({ + [OWNER]: owner, + template + }).create({ + val2: 8 + }); + + runAppend(component); + + assert.equal(component.$('.value').text(), '8', 'initial state is right'); + + run(() => component.$('.my-button').click()); + + assert.equal(component.$('.value').text(), '10', 'Value gets updated'); + + runDestroy(dispatcher); +}); + +QUnit.test('parameters in a closure are mutable when closure is a nested hash value', function(assert) { + let dispatcher = EventDispatcher.create(); + dispatcher.setup(); + + owner.register( + 'template:components/change-button', + compile('') + ); + + owner.register( + 'template:components/my-comp', + compile('{{component components.button}}') + ); + + let template = compile('{{my-comp components=(hash button=(component "change-button" val=val2))}}{{val2}}'); + + component = Component.extend({ + [OWNER]: owner, + template + }).create({ + val2: 8 + }); + + runAppend(component); + + assert.equal(component.$('.value').text(), '8', 'initial state is right'); + + run(() => component.$('.my-button').click()); + + assert.equal(component.$('.value').text(), '10', 'Value gets updated'); + + runDestroy(dispatcher); +}); diff --git a/packages/ember-template-compiler/lib/index.js b/packages/ember-template-compiler/lib/index.js index 10eb52a36d2..8aeeced6376 100644 --- a/packages/ember-template-compiler/lib/index.js +++ b/packages/ember-template-compiler/lib/index.js @@ -7,6 +7,7 @@ import { registerPlugin } from 'ember-template-compiler/plugins'; import TransformOldBindingSyntax from 'ember-template-compiler/plugins/transform-old-binding-syntax'; import TransformOldClassBindingSyntax from 'ember-template-compiler/plugins/transform-old-class-binding-syntax'; import TransformItemClass from 'ember-template-compiler/plugins/transform-item-class'; +import TransformClosureComponentAttrsIntoMut from 'ember-template-compiler/plugins/transform-closure-component-attrs-into-mut'; import TransformComponentAttrsIntoMut from 'ember-template-compiler/plugins/transform-component-attrs-into-mut'; import TransformComponentCurlyToReadonly from 'ember-template-compiler/plugins/transform-component-curly-to-readonly'; import TransformAngleBracketComponents from 'ember-template-compiler/plugins/transform-angle-bracket-components'; @@ -23,6 +24,7 @@ import 'ember-template-compiler/compat'; registerPlugin('ast', TransformOldBindingSyntax); registerPlugin('ast', TransformOldClassBindingSyntax); registerPlugin('ast', TransformItemClass); +registerPlugin('ast', TransformClosureComponentAttrsIntoMut); registerPlugin('ast', TransformComponentAttrsIntoMut); registerPlugin('ast', TransformComponentCurlyToReadonly); registerPlugin('ast', TransformAngleBracketComponents); diff --git a/packages/ember-template-compiler/lib/plugins/transform-closure-component-attrs-into-mut.js b/packages/ember-template-compiler/lib/plugins/transform-closure-component-attrs-into-mut.js new file mode 100644 index 00000000000..79381e66b5b --- /dev/null +++ b/packages/ember-template-compiler/lib/plugins/transform-closure-component-attrs-into-mut.js @@ -0,0 +1,78 @@ +function TransformClosureComponentAttrsIntoMut() { + // set later within HTMLBars to the syntax package + this.syntax = null; +} + +/** + @private + @method transform + @param {AST} ast The AST to be transformed. +*/ +TransformClosureComponentAttrsIntoMut.prototype.transform = function TransformClosureComponentAttrsIntoMut_transform(ast) { + let b = this.syntax.builders; + let walker = new this.syntax.Walker(); + + walker.visit(ast, function(node) { + if (validate(node)) { + processExpression(b, node); + } + }); + + return ast; +}; + +function processExpression(builder, node) { + processSubExpressionsInNode(builder, node); + + if (isComponentClosure(node)) { + mutParameters(builder, node); + } +} + +function processSubExpressionsInNode(builder, node) { + for (let i = 0; i < node.params.length; i++) { + if (node.params[i].type === 'SubExpression') { + processExpression(builder, node.params[i]); + } + } + + each(node.hash.pairs, function(pair) { + let { value } = pair; + + if (value.type === 'SubExpression') { + processExpression(builder, value); + } + }); +} + +function isComponentClosure(node) { + return node.type === 'SubExpression' && node.path.original === 'component'; +} + +function mutParameters(builder, node) { + for (let i = 1; i < node.params.length; i++) { + if (node.params[i].type === 'PathExpression') { + node.params[i] = builder.sexpr(builder.path('@mut'), [node.params[i]]); + } + } + + each(node.hash.pairs, function(pair) { + let { value } = pair; + + if (value.type === 'PathExpression') { + pair.value = builder.sexpr(builder.path('@mut'), [pair.value]); + } + }); +} + +function validate(node) { + return node.type === 'BlockStatement' || node.type === 'MustacheStatement'; +} + +function each(list, callback) { + for (var i = 0, l = list.length; i < l; i++) { + callback(list[i]); + } +} + +export default TransformClosureComponentAttrsIntoMut;