diff --git a/packages/@ember/deprecated-features/index.ts b/packages/@ember/deprecated-features/index.ts index e896a671ab2..f2afea2fd45 100644 --- a/packages/@ember/deprecated-features/index.ts +++ b/packages/@ember/deprecated-features/index.ts @@ -1,3 +1,4 @@ +export const SEND_ACTION = !!'3.4.0'; export const PROPERTY_BASED_DESCRIPTORS = !!'3.2.0'; export const EMBER_EXTEND_PROTOTYPES = !!'3.2.0-beta.5'; export const DEPRECATE_OPTIONS_MISSING = !!'2.1.0-beta.1'; diff --git a/packages/ember-glimmer/tests/integration/components/dynamic-components-test.js b/packages/ember-glimmer/tests/integration/components/dynamic-components-test.js index d46211f26e9..239166e47d7 100644 --- a/packages/ember-glimmer/tests/integration/components/dynamic-components-test.js +++ b/packages/ember-glimmer/tests/integration/components/dynamic-components-test.js @@ -435,7 +435,11 @@ moduleFor( classNames: 'inner-component', didInsertElement() { // trigger action on click in absence of app's EventDispatcher - let sendAction = (this.eventHandler = () => this.sendAction('somethingClicked')); + let sendAction = (this.eventHandler = () => { + if (this.somethingClicked) { + this.somethingClicked(); + } + }); this.element.addEventListener('click', sendAction); }, willDestroyElement() { @@ -447,7 +451,7 @@ moduleFor( let actionTriggered = 0; this.registerComponent('outer-component', { template: - '{{#component componentName somethingClicked="mappedAction"}}arepas!{{/component}}', + '{{#component componentName somethingClicked=(action "mappedAction")}}arepas!{{/component}}', ComponentClass: Component.extend({ classNames: 'outer-component', componentName: 'inner-component', diff --git a/packages/ember-glimmer/tests/integration/components/target-action-test.js b/packages/ember-glimmer/tests/integration/components/target-action-test.js index 2080ea199ca..568a9f6cae0 100644 --- a/packages/ember-glimmer/tests/integration/components/target-action-test.js +++ b/packages/ember-glimmer/tests/integration/components/target-action-test.js @@ -7,6 +7,13 @@ import Controller from '@ember/controller'; import { Object as EmberObject } from 'ember-runtime'; import { Route } from 'ember-routing'; +function expectSendActionDeprecation(fn) { + expectDeprecation( + fn, + /You called (.*).sendAction\((.*)\) but Component#sendAction is deprecated. Please use closure actions instead./ + ); +} + moduleFor( 'Components test: sendAction', class extends RenderingTest { @@ -61,7 +68,9 @@ moduleFor( ['@test Calling sendAction on a component without an action defined does nothing']() { this.renderDelegate(); - this.runTask(() => this.delegate.sendAction()); + expectSendActionDeprecation(() => { + this.runTask(() => this.delegate.sendAction()); + }); this.assertSendCount(0); } @@ -69,9 +78,11 @@ moduleFor( ['@test Calling sendAction on a component with an action defined calls send on the controller']() { this.renderDelegate(); - this.runTask(() => { - set(this.delegate, 'action', 'addItem'); - this.delegate.sendAction(); + expectSendActionDeprecation(() => { + this.runTask(() => { + set(this.delegate, 'action', 'addItem'); + this.delegate.sendAction(); + }); }); this.assertSendCount(1); @@ -79,27 +90,30 @@ moduleFor( } ['@test Calling sendAction on a component with a function calls the function']() { - this.assert.expect(1); + this.assert.expect(2); this.renderDelegate(); - this.runTask(() => { - set(this.delegate, 'action', () => this.assert.ok(true, 'function is called')); - this.delegate.sendAction(); + expectSendActionDeprecation(() => { + this.runTask(() => { + set(this.delegate, 'action', () => this.assert.ok(true, 'function is called')); + this.delegate.sendAction(); + }); }); } ['@test Calling sendAction on a component with a function calls the function with arguments']() { - this.assert.expect(1); + this.assert.expect(2); let argument = {}; this.renderDelegate(); - - this.runTask(() => { - set(this.delegate, 'action', actualArgument => { - this.assert.deepEqual(argument, actualArgument, 'argument is passed'); + expectSendActionDeprecation(() => { + this.runTask(() => { + set(this.delegate, 'action', actualArgument => { + this.assert.deepEqual(argument, actualArgument, 'argument is passed'); + }); + this.delegate.sendAction('action', argument); }); - this.delegate.sendAction('action', argument); }); } @@ -109,16 +123,19 @@ moduleFor( playing: null, }); - this.runTask(() => this.delegate.sendAction()); + expectSendActionDeprecation(() => { + this.runTask(() => this.delegate.sendAction()); + }); this.assertSendCount(0); this.runTask(() => { set(this.context, 'playing', 'didStartPlaying'); }); - - this.runTask(() => { - this.delegate.sendAction('playing'); + expectSendActionDeprecation(() => { + this.runTask(() => { + this.delegate.sendAction('playing'); + }); }); this.assertSendCount(1); @@ -130,12 +147,16 @@ moduleFor( playing: null, }); - this.runTask(() => this.delegate.sendAction('playing')); + expectSendActionDeprecation(() => { + this.runTask(() => this.delegate.sendAction('playing')); + }); this.assertSendCount(0); this.runTask(() => this.delegate.attrs.playing.update('didStartPlaying')); - this.runTask(() => this.delegate.sendAction('playing')); + expectSendActionDeprecation(() => { + this.runTask(() => this.delegate.sendAction('playing')); + }); this.assertSendCount(1); this.assertNamedSendCount('didStartPlaying', 1); @@ -145,23 +166,28 @@ moduleFor( this.renderDelegate(); let component = this.delegate; - - this.runTask(() => { - set(this.delegate, 'playing', 'didStartPlaying'); - component.sendAction('playing'); + expectSendActionDeprecation(() => { + this.runTask(() => { + set(this.delegate, 'playing', 'didStartPlaying'); + component.sendAction('playing'); + }); }); this.assertSendCount(1); this.assertNamedSendCount('didStartPlaying', 1); - this.runTask(() => component.sendAction('playing')); + expectSendActionDeprecation(() => { + this.runTask(() => component.sendAction('playing')); + }); this.assertSendCount(2); this.assertNamedSendCount('didStartPlaying', 2); - this.runTask(() => { - set(component, 'action', 'didDoSomeBusiness'); - component.sendAction(); + expectSendActionDeprecation(() => { + this.runTask(() => { + set(component, 'action', 'didDoSomeBusiness'); + component.sendAction(); + }); }); this.assertSendCount(3); @@ -175,9 +201,12 @@ moduleFor( set(this.delegate, 'action', {}); set(this.delegate, 'playing', {}); }); - - expectAssertion(() => this.delegate.sendAction()); - expectAssertion(() => this.delegate.sendAction('playing')); + expectSendActionDeprecation(() => { + expectAssertion(() => this.delegate.sendAction()); + }); + expectSendActionDeprecation(() => { + expectAssertion(() => this.delegate.sendAction('playing')); + }); } ['@test Calling sendAction on a component with contexts']() { @@ -187,17 +216,21 @@ moduleFor( let firstContext = { song: 'She Broke My Ember' }; let secondContext = { song: 'My Achey Breaky Ember' }; - this.runTask(() => { - set(this.delegate, 'playing', 'didStartPlaying'); - this.delegate.sendAction('playing', testContext); + expectSendActionDeprecation(() => { + this.runTask(() => { + set(this.delegate, 'playing', 'didStartPlaying'); + this.delegate.sendAction('playing', testContext); + }); }); this.assertSendCount(1); this.assertNamedSendCount('didStartPlaying', 1); this.assertSentWithArgs([testContext], 'context was sent with the action'); - this.runTask(() => { - this.delegate.sendAction('playing', firstContext, secondContext); + expectSendActionDeprecation(() => { + this.runTask(() => { + this.delegate.sendAction('playing', firstContext, secondContext); + }); }); this.assertSendCount(2); @@ -262,8 +295,9 @@ moduleFor( }); this.renderDelegate(); - - this.runTask(() => innerChild.sendAction('bar', 'something special')); + expectSendActionDeprecation(() => { + this.runTask(() => innerChild.sendAction('bar', 'something special')); + }); } ['@test asserts if called on a destroyed component']() { @@ -303,7 +337,7 @@ moduleFor( ["@test sendAction should trigger an action on the parent component's controller if it exists"]( assert ) { - assert.expect(15); + assert.expect(20); let component; @@ -402,34 +436,46 @@ moduleFor( this.addTemplate('c.e', '{{foo-bar val=".e" poke="poke"}}'); return this.visit('/a') - .then(() => component.sendAction('poke', 'top')) + .then(() => { + expectSendActionDeprecation(() => component.sendAction('poke', 'top')); + }) .then(() => { this.assertText('a'); return this.visit('/b'); }) - .then(() => component.sendAction('poke', 'top no controller')) + .then(() => { + expectSendActionDeprecation(() => component.sendAction('poke', 'top no controller')); + }) .then(() => { this.assertText('b'); return this.visit('/c'); }) - .then(() => component.sendAction('poke', 'top with nested no controller')) + .then(() => { + expectSendActionDeprecation(() => { + component.sendAction('poke', 'top with nested no controller'); + }); + }) .then(() => { this.assertText('c'); return this.visit('/c/d'); }) - .then(() => component.sendAction('poke', 'nested')) + .then(() => { + expectSendActionDeprecation(() => component.sendAction('poke', 'nested')); + }) .then(() => { this.assertText('c.d'); return this.visit('/c/e'); }) - .then(() => component.sendAction('poke', 'nested no controller')) + .then(() => { + expectSendActionDeprecation(() => component.sendAction('poke', 'nested no controller')); + }) .then(() => this.assertText('c.e')); } ["@test sendAction should not trigger an action in an outlet's controller if a parent component handles it"]( assert ) { - assert.expect(1); + assert.expect(2); let component; @@ -463,7 +509,9 @@ moduleFor( }) ); - return this.visit('/').then(() => component.sendAction('poke')); + return this.visit('/').then(() => { + expectSendActionDeprecation(() => component.sendAction('poke')); + }); } } ); @@ -472,7 +520,7 @@ moduleFor( 'Components test: sendAction of a closure action', class extends RenderingTest { ['@test action should be called'](assert) { - assert.expect(1); + assert.expect(2); let component; this.registerComponent('inner-component', { @@ -495,8 +543,9 @@ moduleFor( }); this.render('{{outer-component}}'); - - this.runTask(() => component.sendAction('submitAction')); + expectSendActionDeprecation(() => { + this.runTask(() => component.sendAction('submitAction')); + }); } ['@test contexts passed to sendAction are appended to the bound arguments on a closure action']() { @@ -531,8 +580,9 @@ moduleFor( }); this.render('{{outer-component}}'); - - this.runTask(() => innerComponent.sendAction('innerSubmit', fourth)); + expectSendActionDeprecation(() => { + this.runTask(() => innerComponent.sendAction('innerSubmit', fourth)); + }); this.assert.deepEqual( actualArgs, diff --git a/packages/ember-glimmer/tests/integration/helpers/element-action-test.js b/packages/ember-glimmer/tests/integration/helpers/element-action-test.js index 76d079e76cb..a562152a189 100644 --- a/packages/ember-glimmer/tests/integration/helpers/element-action-test.js +++ b/packages/ember-glimmer/tests/integration/helpers/element-action-test.js @@ -1440,7 +1440,9 @@ moduleFor( let InnerComponent = Component.extend({ click() { innerClickCalled = true; - this.sendAction(); + expectDeprecation(() => { + this.sendAction(); + }, /You called (.*).sendAction\((.*)\) but Component#sendAction is deprecated. Please use closure actions instead./); }, }); diff --git a/packages/ember-glimmer/tests/integration/helpers/input-test.js b/packages/ember-glimmer/tests/integration/helpers/input-test.js index e7c6a328221..66e045265d8 100644 --- a/packages/ember-glimmer/tests/integration/helpers/input-test.js +++ b/packages/ember-glimmer/tests/integration/helpers/input-test.js @@ -271,10 +271,30 @@ moduleFor( // this.assertSelectionRange(8, 8); //NOTE: this fails in IE, the range is 0 -> 0 (TEST_SUITE=sauce) } - ['@test sends an action with `{{input enter="foo"}}` when is pressed'](assert) { - assert.expect(1); + ['@test [DEPRECATED] sends an action with `{{input enter="foo"}}` when is pressed']( + assert + ) { + assert.expect(3); + + expectDeprecation(() => { + this.render(`{{input enter='foo'}}`, { + actions: { + foo() { + assert.ok(true, 'action was triggered'); + }, + }, + }); + }, 'Please refactor `{{input enter="foo"}}` to `{{input enter=(action "foo")}}. (\'-top-level\' @ L1:C0) '); + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 13 }); + }, 'Passing actions to components as strings (like {{input enter="foo"}}) is deprecated. Please use closure actions instead ({{input enter=(action "foo")}})'); + } - this.render(`{{input enter='foo'}}`, { + ['@test sends an action with `{{input enter=(action "foo")}}` when is pressed']( + assert + ) { + assert.expect(1); + this.render(`{{input enter=(action 'foo')}}`, { actions: { foo() { assert.ok(true, 'action was triggered'); @@ -287,10 +307,30 @@ moduleFor( }); } - ['@test sends an action with `{{input key-press="foo"}}` is pressed'](assert) { + ['@test [DEPRECATED] sends an action with `{{input key-press="foo"}}` is pressed'](assert) { + assert.expect(3); + + expectDeprecation(() => { + this.render(`{{input value=value key-press='foo'}}`, { + value: 'initial', + + actions: { + foo() { + assert.ok(true, 'action was triggered'); + }, + }, + }); + }, 'Please refactor `{{input key-press="foo"}}` to `{{input key-press=(action "foo")}}. (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keypress', { keyCode: 65 }); + }, 'Passing actions to components as strings (like {{input key-press="foo"}}) is deprecated. Please use closure actions instead ({{input key-press=(action "foo")}})'); + } + + ['@test sends an action with `{{input key-press=(action "foo")}}` is pressed'](assert) { assert.expect(1); - this.render(`{{input value=value key-press='foo'}}`, { + this.render(`{{input value=value key-press=(action 'foo')}}`, { value: 'initial', actions: { @@ -300,9 +340,7 @@ moduleFor( }, }); - this.triggerEvent('keypress', { - keyCode: 65, - }); + this.triggerEvent('keypress', { keyCode: 65 }); } ['@test sends an action to the parent level when `bubbles=true` is provided'](assert) { @@ -326,7 +364,7 @@ moduleFor( ['@test triggers `focus-in` when focused'](assert) { let wasFocused = false; - this.render(`{{input focus-in='foo'}}`, { + this.render(`{{input focus-in=(action 'foo')}}`, { actions: { foo() { wasFocused = true; @@ -344,7 +382,7 @@ moduleFor( ['@test sends `insert-newline` when is pressed'](assert) { assert.expect(1); - this.render(`{{input insert-newline='foo'}}`, { + this.render(`{{input insert-newline=(action 'foo')}}`, { actions: { foo() { assert.ok(true, 'action was triggered'); @@ -357,10 +395,32 @@ moduleFor( }); } - ['@test sends an action with `{{input escape-press="foo"}}` when is pressed'](assert) { + ['@test [DEPRECATED] sends an action with `{{input escape-press="foo"}}` when is pressed']( + assert + ) { + assert.expect(3); + + expectDeprecation(() => { + this.render(`{{input escape-press='foo'}}`, { + actions: { + foo() { + assert.ok(true, 'action was triggered'); + }, + }, + }); + }, 'Please refactor `{{input escape-press="foo"}}` to `{{input escape-press=(action "foo")}}. (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 27 }); + }, 'Passing actions to components as strings (like {{input escape-press="foo"}}) is deprecated. Please use closure actions instead ({{input escape-press=(action "foo")}})'); + } + + ['@test sends an action with `{{input escape-press=(action "foo")}}` when is pressed']( + assert + ) { assert.expect(1); - this.render(`{{input escape-press='foo'}}`, { + this.render(`{{input escape-press=(action 'foo')}}`, { actions: { foo() { assert.ok(true, 'action was triggered'); @@ -368,15 +428,35 @@ moduleFor( }, }); - this.triggerEvent('keyup', { - keyCode: 27, - }); + this.triggerEvent('keyup', { keyCode: 27 }); } - ['@test sends an action with `{{input key-down="foo"}}` when a key is pressed'](assert) { + ['@test [DEPRECATED] sends an action with `{{input key-down="foo"}}` when a key is pressed']( + assert + ) { + assert.expect(3); + + expectDeprecation(() => { + this.render(`{{input key-down='foo'}}`, { + actions: { + foo() { + assert.ok(true, 'action was triggered'); + }, + }, + }); + }, 'Please refactor `{{input key-down="foo"}}` to `{{input key-down=(action "foo")}}. (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keydown', { keyCode: 65 }); + }, 'Passing actions to components as strings (like {{input key-down="foo"}}) is deprecated. Please use closure actions instead ({{input key-down=(action "foo")}})'); + } + + ['@test sends an action with `{{input key-down=(action "foo")}}` when a key is pressed']( + assert + ) { assert.expect(1); - this.render(`{{input key-down='foo'}}`, { + this.render(`{{input key-down=(action 'foo')}}`, { actions: { foo() { assert.ok(true, 'action was triggered'); @@ -384,25 +464,42 @@ moduleFor( }, }); - this.triggerEvent('keydown', { - keyCode: 65, - }); + this.triggerEvent('keydown', { keyCode: 65 }); } - ['@test sends an action with `{{input key-up="foo"}}` when a key is pressed'](assert) { + ['@test [DEPRECATED] sends an action with `{{input key-up="foo"}}` when a key is pressed']( + assert + ) { + assert.expect(3); + + expectDeprecation(() => { + this.render(`{{input key-up='foo'}}`, { + actions: { + foo() { + assert.ok(true, 'action was triggered'); + }, + }, + }); + }, 'Please refactor `{{input key-up="foo"}}` to `{{input key-up=(action "foo")}}. (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 65 }); + }, 'Passing actions to components as strings (like {{input key-up="foo"}}) is deprecated. Please use closure actions instead ({{input key-up=(action "foo")}})'); + } + + ['@test [DEPRECATED] sends an action with `{{input key-up=(action "foo")}}` when a key is pressed']( + assert + ) { assert.expect(1); - this.render(`{{input key-up='foo'}}`, { + this.render(`{{input key-up=(action 'foo')}}`, { actions: { foo() { assert.ok(true, 'action was triggered'); }, }, }); - - this.triggerEvent('keyup', { - keyCode: 65, - }); + this.triggerEvent('keyup', { keyCode: 65 }); } ['@test GH#14727 can render a file input after having had render an input of other type']() { diff --git a/packages/ember-template-compiler/lib/plugins/deprecate-send-action.ts b/packages/ember-template-compiler/lib/plugins/deprecate-send-action.ts new file mode 100644 index 00000000000..695e14697d6 --- /dev/null +++ b/packages/ember-template-compiler/lib/plugins/deprecate-send-action.ts @@ -0,0 +1,48 @@ +import { deprecate } from '@ember/debug'; +import { AST, ASTPlugin, ASTPluginEnvironment } from '@glimmer/syntax'; +import calculateLocationDisplay from '../system/calculate-location-display'; +import { SEND_ACTION } from '@ember/deprecated-features'; + +const EVENTS = [ + 'insert-newline', + 'enter', + 'escape-press', + 'focus-in', + 'focus-out', + 'key-press', + 'key-up', + 'key-down', +]; + +export default function deprecateSendAction(env: ASTPluginEnvironment): ASTPlugin | undefined { + if (SEND_ACTION) { + let { moduleName } = env.meta; + + let deprecationMessage = (node: AST.MustacheStatement, eventName: string, actionName: string) => { + let sourceInformation = calculateLocationDisplay(moduleName, node.loc); + return `Please refactor \`{{input ${eventName}="${actionName}"}}\` to \`{{input ${eventName}=(action "${actionName}")}}\. ${sourceInformation}`; + }; + + return { + name: 'deprecate-send-action', + + visitor: { + MustacheStatement(node: AST.MustacheStatement) { + if (node.path.original !== 'input') { + return; + } + + node.hash.pairs.forEach(pair => { + if (EVENTS.indexOf(pair.key) > -1 && pair.value.type === 'StringLiteral') { + deprecate(deprecationMessage(node, pair.key, pair.value.original), false, { + id: 'ember-component.send-action', + until: '4.0.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_ember-component-send-action', + }); + } + }); + }, + }, + }; + } +} diff --git a/packages/ember-template-compiler/lib/plugins/index.ts b/packages/ember-template-compiler/lib/plugins/index.ts index 3e0a59d63f7..02cd7e346b4 100644 --- a/packages/ember-template-compiler/lib/plugins/index.ts +++ b/packages/ember-template-compiler/lib/plugins/index.ts @@ -4,6 +4,7 @@ import AssertReservedNamedArguments from './assert-reserved-named-arguments'; import AssertSplattributeExpressions from './assert-splattribute-expression'; import DeprecateRender from './deprecate-render'; import DeprecateRenderModel from './deprecate-render-model'; +import DeprecateSendAction from './deprecate-send-action'; import TransformActionSyntax from './transform-action-syntax'; import TransformAngleBracketComponents from './transform-angle-bracket-components'; import TransformAttrsIntoArgs from './transform-attrs-into-args'; @@ -18,7 +19,7 @@ import TransformOldClassBindingSyntax from './transform-old-class-binding-syntax import TransformQuotedBindingsIntoJustBindings from './transform-quoted-bindings-into-just-bindings'; import TransformTopLevelComponents from './transform-top-level-components'; -import { BINDING_SUPPORT, RENDER_HELPER } from '@ember/deprecated-features'; +import { BINDING_SUPPORT, RENDER_HELPER, SEND_ACTION } from '@ember/deprecated-features'; import { ASTPlugin, ASTPluginEnvironment } from '@glimmer/syntax'; export type APluginFunc = (env: ASTPluginEnvironment) => ASTPlugin | undefined; @@ -51,4 +52,7 @@ if (BINDING_SUPPORT) { transforms.push(TransformOldBindingSyntax); } -export default Object.freeze(transforms); +if (SEND_ACTION) { + transforms.push(DeprecateSendAction); +} + export default Object.freeze(transforms); diff --git a/packages/ember-template-compiler/tests/plugins/deprecate-send-action-test.js b/packages/ember-template-compiler/tests/plugins/deprecate-send-action-test.js new file mode 100644 index 00000000000..a5925fa41e0 --- /dev/null +++ b/packages/ember-template-compiler/tests/plugins/deprecate-send-action-test.js @@ -0,0 +1,29 @@ +import { compile } from '../../index'; +import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; + +const EVENTS = [ + 'insert-newline', + 'enter', + 'escape-press', + 'focus-in', + 'focus-out', + 'key-press', + 'key-up', + 'key-down', +]; + +class DeprecateSendActionTest extends AbstractTestCase {} + +EVENTS.forEach(function(e) { + DeprecateSendActionTest.prototype[ + `@test Using \`{{input ${e}="actionName"}}\` provides a deprecation` + ] = function() { + let expectedMessage = `Please refactor \`{{input ${e}="foo-bar"}}\` to \`{{input ${e}=(action "foo-bar")}}\. ('baz/foo-bar' @ L1:C0) `; + + expectDeprecation(() => { + compile(`{{input ${e}="foo-bar"}}`, { moduleName: 'baz/foo-bar' }); + }, expectedMessage); + }; +}); + +moduleFor('ember-template-compiler: deprecate-send-action', DeprecateSendActionTest); diff --git a/packages/ember-testing/tests/helpers_test.js b/packages/ember-testing/tests/helpers_test.js index 11e9ff25782..ab685b338e7 100644 --- a/packages/ember-testing/tests/helpers_test.js +++ b/packages/ember-testing/tests/helpers_test.js @@ -753,8 +753,8 @@ if (!jQueryDisabled) { let wasFocused = false; this.add( - 'route:application', - Route.extend({ + 'controller:index', + Controller.extend({ actions: { wasFocused() { wasFocused = true; @@ -767,7 +767,7 @@ if (!jQueryDisabled) { 'index', `
- {{input type="text" id="first" focus-in="wasFocused"}} + {{input type="text" id="first" focus-in=(action "wasFocused")}}
' ` ); diff --git a/packages/ember-views/lib/mixins/action_support.js b/packages/ember-views/lib/mixins/action_support.js index 0216017fdfa..05d48d904bf 100644 --- a/packages/ember-views/lib/mixins/action_support.js +++ b/packages/ember-views/lib/mixins/action_support.js @@ -3,30 +3,40 @@ */ import { inspect } from 'ember-utils'; import { Mixin, get } from 'ember-metal'; -import { assert } from '@ember/debug'; +import { assert, deprecate } from '@ember/debug'; import { MUTABLE_CELL } from '../compat/attrs'; +import { SEND_ACTION } from '@ember/deprecated-features'; -function validateAction(component, actionName) { - if (actionName && actionName[MUTABLE_CELL]) { - actionName = actionName.value; - } - - assert( - `The default action was triggered on the component ${component.toString()}, but the action name (${actionName}) was not a string.`, - actionName === null || - actionName === undefined || - typeof actionName === 'string' || - typeof actionName === 'function' - ); - return actionName; -} +const mixinObj = { + send(actionName, ...args) { + assert( + `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, + !this.isDestroying && !this.isDestroyed + ); -/** - @class ActionSupport - @namespace Ember - @private -*/ -export default Mixin.create({ + let action = this.actions && this.actions[actionName]; + + if (action) { + let shouldBubble = action.apply(this, args) === true; + if (!shouldBubble) { + return; + } + } + + let target = get(this, 'target'); + if (target) { + assert( + `The \`target\` for ${this} (${target}) does not have a \`send\` method`, + typeof target.send === 'function' + ); + target.send(...arguments); + } else { + assert(`${inspect(this)} had no action handler for: ${actionName}`, action); + } + }, +}; + +if (SEND_ACTION) { /** Calls an action passed to a component. @@ -110,12 +120,24 @@ export default Mixin.create({ @param [action] {String} the action to call @param [params] {*} arguments for the action @public + @deprecated */ - sendAction(action, ...contexts) { + let sendAction = function sendAction(action, ...contexts) { assert( `Attempted to call .sendAction() with the action '${action}' on the destroyed object '${this}'.`, !this.isDestroying && !this.isDestroyed ); + deprecate( + `You called ${inspect(this)}.sendAction(${ + typeof action === 'string' ? `"${action}"` : '' + }) but Component#sendAction is deprecated. Please use closure actions instead.`, + false, + { + id: 'ember-component.send-action', + until: '4.0.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_ember-component-send-action', + } + ); let actionName; @@ -139,32 +161,29 @@ export default Mixin.create({ actionContext: contexts, }); } - }, + }; + + let validateAction = function validateAction(component, actionName) { + if (actionName && actionName[MUTABLE_CELL]) { + actionName = actionName.value; + } - send(actionName, ...args) { assert( - `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, - !this.isDestroying && !this.isDestroyed + `The default action was triggered on the component ${component.toString()}, but the action name (${actionName}) was not a string.`, + actionName === null || + actionName === undefined || + typeof actionName === 'string' || + typeof actionName === 'function' ); + return actionName; + }; - let action = this.actions && this.actions[actionName]; - - if (action) { - let shouldBubble = action.apply(this, args) === true; - if (!shouldBubble) { - return; - } - } + mixinObj.sendAction = sendAction; +} - let target = get(this, 'target'); - if (target) { - assert( - `The \`target\` for ${this} (${target}) does not have a \`send\` method`, - typeof target.send === 'function' - ); - target.send(...arguments); - } else { - assert(`${inspect(this)} had no action handler for: ${actionName}`, action); - } - }, -}); +/** + @class ActionSupport + @namespace Ember + @private +*/ +export default Mixin.create(mixinObj); diff --git a/packages/ember-views/lib/mixins/text_support.js b/packages/ember-views/lib/mixins/text_support.js index 78aacf315e2..05c8ebd7e86 100644 --- a/packages/ember-views/lib/mixins/text_support.js +++ b/packages/ember-views/lib/mixins/text_support.js @@ -4,6 +4,8 @@ import { get, set, Mixin } from 'ember-metal'; import { TargetActionSupport } from 'ember-runtime'; +import { deprecate } from '@ember/debug'; +import { SEND_ACTION } from '@ember/deprecated-features'; const KEY_EVENTS = { 13: 'insertNewline', @@ -88,7 +90,7 @@ const KEY_EVENTS = { +--------------------+----------------+ | new line inserted | insert-newline | | | | - | enter key pressed | insert-newline | + | enter key pressed | enter | | | | | cancel key pressed | escape-press | | | | @@ -279,8 +281,7 @@ export default Mixin.create(TargetActionSupport, { */ keyUp(event) { this.interpretKeyEvents(event); - - this.sendAction('key-up', get(this, 'value'), event); + sendAction('key-up', this, event); }, /** @@ -297,7 +298,7 @@ export default Mixin.create(TargetActionSupport, { @private */ keyDown(event) { - this.sendAction('key-down', get(this, 'value'), event); + sendAction('key-down', this, event); }, }); @@ -305,12 +306,28 @@ export default Mixin.create(TargetActionSupport, { // sendAction semantics for TextField are different from // the component semantics so this method normalizes them. function sendAction(eventName, view, event) { - let action = get(view, `attrs.${eventName}`) || get(view, eventName); + let actionName = get(view, `attrs.${eventName}`) || get(view, eventName); let value = get(view, 'value'); - view.sendAction(eventName, value); + if (SEND_ACTION && typeof actionName === 'string') { + deprecate( + `Passing actions to components as strings (like {{input ${eventName}="${actionName}"}}) is deprecated. Please use closure actions instead ({{input ${eventName}=(action "${actionName}")}})`, + false, + { + id: 'ember-component.send-action', + until: '4.0.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_ember-component-send-action', + } + ); + view.triggerAction({ + action: actionName, + actionContext: [value], + }); + } else if (typeof actionName === 'function') { + actionName(value); + } - if (action && !get(view, 'bubbles')) { + if (actionName && !get(view, 'bubbles')) { event.stopPropagation(); } } diff --git a/packages/ember/tests/controller_test.js b/packages/ember/tests/controller_test.js index 0d79d83ad17..cfd52aa161d 100644 --- a/packages/ember/tests/controller_test.js +++ b/packages/ember/tests/controller_test.js @@ -29,12 +29,12 @@ moduleFor( ComponentClass: Component.extend({ classNames: ['component-with-action'], click() { - this.sendAction(); + this.action(); }, }), }); - this.addTemplate('index', '{{component-with-action action="componentAction"}}'); + this.addTemplate('index', '{{component-with-action action=(action "componentAction")}}'); return this.visit('/').then(() => { this.runTask(() => this.$('.component-with-action').click());