diff --git a/packages/@ember/-internals/glimmer/index.ts b/packages/@ember/-internals/glimmer/index.ts index 8cacb65eff4..ef302aa1900 100644 --- a/packages/@ember/-internals/glimmer/index.ts +++ b/packages/@ember/-internals/glimmer/index.ts @@ -265,8 +265,8 @@ export { default as RootTemplate } from './lib/templates/root'; export { default as template } from './lib/template'; export { default as Checkbox } from './lib/components/checkbox'; -export { default as TextField } from './lib/components/text_field'; -export { default as TextArea } from './lib/components/text_area'; +export { default as TextField } from './lib/components/text-field'; +export { default as TextArea } from './lib/components/textarea'; export { default as LinkComponent } from './lib/components/link-to'; export { default as Component, ROOT_REF } from './lib/component'; export { default as Helper, helper } from './lib/helper'; diff --git a/packages/@ember/-internals/glimmer/lib/components/input.ts b/packages/@ember/-internals/glimmer/lib/components/input.ts new file mode 100644 index 00000000000..f89dca5cb84 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/components/input.ts @@ -0,0 +1,172 @@ +/** +@module @ember/component +*/ +import { computed } from '@ember/-internals/metal'; +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; +import Component from '../component'; + +/** + The `{{input}}` helper lets you create an HTML `` component. + It causes a `TextField` component to be rendered. For more info, + see the [TextField](/api/ember/release/classes/TextField) docs and + the [templates guide](https://guides.emberjs.com/release/templates/input-helpers/). + + ```handlebars + {{input value="987"}} + ``` + + renders as: + + ```HTML + + ``` + + ### Text field + + If no `type` option is specified, a default of type 'text' is used. + Many of the standard HTML attributes may be passed to this helper. + + + + + + + + + + + +
`readonly``required``autofocus`
`value``placeholder``disabled`
`size``tabindex``maxlength`
`name``min``max`
`pattern``accept``autocomplete`
`autosave``formaction``formenctype`
`formmethod``formnovalidate``formtarget`
`height``inputmode``multiple`
`step``width``form`
`selectionDirection``spellcheck` 
+ When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + A very common use of this helper is to bind the `value` of an input to an Object's attribute: + + ```handlebars + Search: + {{input value=searchWord}} + ``` + + In this example, the initial value in the `` will be set to the value of `searchWord`. + If the user changes the text, the value of `searchWord` will also be updated. + + ### Actions + + The helper can send multiple actions based on user events. + The action property defines the action which is sent when + the user presses the return key. + + ```handlebars + {{input action="submit"}} + ``` + + The helper allows some user events to send actions. + + * `enter` + * `insert-newline` + * `escape-press` + * `focus-in` + * `focus-out` + * `key-press` + * `key-up` + + For example, if you desire an action to be sent when the input is blurred, + you only need to setup the action name to the event name property. + + ```handlebars + {{input focus-out="alertMessage"}} + ``` + See more about [Text Support Actions](/api/ember/release/classes/TextField) + + ### Extending `TextField` + + Internally, `{{input type="text"}}` creates an instance of `TextField`, passing + arguments from the helper to `TextField`'s `create` method. You can extend the + capabilities of text inputs in your applications by reopening this class. For example, + if you are building a Bootstrap project where `data-*` attributes are used, you + can add one to the `TextField`'s `attributeBindings` property: + + ```javascript + import TextField from '@ember/component/text-field'; + TextField.reopen({ + attributeBindings: ['data-error'] + }); + ``` + + Keep in mind when writing `TextField` subclasses that `TextField` + itself extends `Component`. Expect isolated component semantics, not + legacy 1.x view semantics (like `controller` being present). + See more about [Ember components](/api/ember/release/classes/Component) + + ### Checkbox + + Checkboxes are special forms of the `{{input}}` helper. To create a ``: + + ```handlebars + Emberize Everything: + {{input type="checkbox" name="isEmberized" checked=isEmberized}} + ``` + + This will bind checked state of this checkbox to the value of `isEmberized` -- if either one changes, + it will be reflected in the other. + + The following HTML attributes can be set via the helper: + + * `checked` + * `disabled` + * `tabindex` + * `indeterminate` + * `name` + * `autofocus` + * `form` + + ### Extending `Checkbox` + + Internally, `{{input type="checkbox"}}` creates an instance of `Checkbox`, passing + arguments from the helper to `Checkbox`'s `create` method. You can extend the + capablilties of checkbox inputs in your applications by reopening this class. For example, + if you wanted to add a css class to all checkboxes in your application: + + ```javascript + import Checkbox from '@ember/component/checkbox'; + + Checkbox.reopen({ + classNames: ['my-app-checkbox'] + }); + ``` + + @method input + @for Ember.Templates.helpers + @param {Hash} options + @public +*/ +const Input = Component.extend({ + tagName: '', + + isCheckbox: computed('type', function(this: { type?: unknown }) { + return this.type === 'checkbox'; + }) +}); + +Input.toString = () => '@ember/component/input'; + +if (DEBUG) { + const UNSET = {}; + + Input.reopen({ + value: UNSET, + + didReceiveAttrs() { + this._super(); + + assert( + "`` is not supported; " + + "please use `` instead.", + !(this.type === 'checkbox' && this.value !== UNSET) + ); + } + }); +} + +export default Input; diff --git a/packages/@ember/-internals/glimmer/lib/components/text_field.ts b/packages/@ember/-internals/glimmer/lib/components/text-field.ts similarity index 100% rename from packages/@ember/-internals/glimmer/lib/components/text_field.ts rename to packages/@ember/-internals/glimmer/lib/components/text-field.ts diff --git a/packages/@ember/-internals/glimmer/lib/components/text_area.ts b/packages/@ember/-internals/glimmer/lib/components/textarea.ts similarity index 100% rename from packages/@ember/-internals/glimmer/lib/components/text_area.ts rename to packages/@ember/-internals/glimmer/lib/components/textarea.ts diff --git a/packages/@ember/-internals/glimmer/lib/resolver.ts b/packages/@ember/-internals/glimmer/lib/resolver.ts index 00b91f9c100..225fb452ae5 100644 --- a/packages/@ember/-internals/glimmer/lib/resolver.ts +++ b/packages/@ember/-internals/glimmer/lib/resolver.ts @@ -319,7 +319,7 @@ export default class RuntimeResolver implements IRuntimeResolver -) { - let definition = builder.compiler['resolver'].lookupComponentDefinition(type, builder.referrer); - builder.component.static(definition!, [params, hashToArgs(hash), null, null]); - return true; -} - -/** - The `{{input}}` helper lets you create an HTML `` component. - It causes a `TextField` component to be rendered. For more info, - see the [TextField](/api/ember/release/classes/TextField) docs and - the [templates guide](https://guides.emberjs.com/release/templates/input-helpers/). - - ```handlebars - {{input value="987"}} - ``` - - renders as: - - ```HTML - - ``` - - ### Text field - - If no `type` option is specified, a default of type 'text' is used. - Many of the standard HTML attributes may be passed to this helper. - - - - - - - - - - - -
`readonly``required``autofocus`
`value``placeholder``disabled`
`size``tabindex``maxlength`
`name``min``max`
`pattern``accept``autocomplete`
`autosave``formaction``formenctype`
`formmethod``formnovalidate``formtarget`
`height``inputmode``multiple`
`step``width``form`
`selectionDirection``spellcheck` 
- When set to a quoted string, these values will be directly applied to the HTML - element. When left unquoted, these values will be bound to a property on the - template's current rendering context (most typically a controller instance). - A very common use of this helper is to bind the `value` of an input to an Object's attribute: - - ```handlebars - Search: - {{input value=searchWord}} - ``` - - In this example, the initial value in the `` will be set to the value of `searchWord`. - If the user changes the text, the value of `searchWord` will also be updated. - - ### Actions - - The helper can send multiple actions based on user events. - The action property defines the action which is sent when - the user presses the return key. - - ```handlebars - {{input action="submit"}} - ``` - - The helper allows some user events to send actions. - - * `enter` - * `insert-newline` - * `escape-press` - * `focus-in` - * `focus-out` - * `key-press` - * `key-up` - - For example, if you desire an action to be sent when the input is blurred, - you only need to setup the action name to the event name property. - - ```handlebars - {{input focus-out="alertMessage"}} - ``` - See more about [Text Support Actions](/api/ember/release/classes/TextField) - - ### Extending `TextField` - - Internally, `{{input type="text"}}` creates an instance of `TextField`, passing - arguments from the helper to `TextField`'s `create` method. You can extend the - capabilities of text inputs in your applications by reopening this class. For example, - if you are building a Bootstrap project where `data-*` attributes are used, you - can add one to the `TextField`'s `attributeBindings` property: - - ```javascript - import TextField from '@ember/component/text-field'; - TextField.reopen({ - attributeBindings: ['data-error'] - }); - ``` - - Keep in mind when writing `TextField` subclasses that `TextField` - itself extends `Component`. Expect isolated component semantics, not - legacy 1.x view semantics (like `controller` being present). - See more about [Ember components](/api/ember/release/classes/Component) - - ### Checkbox - - Checkboxes are special forms of the `{{input}}` helper. To create a ``: - - ```handlebars - Emberize Everything: - {{input type="checkbox" name="isEmberized" checked=isEmberized}} - ``` - - This will bind checked state of this checkbox to the value of `isEmberized` -- if either one changes, - it will be reflected in the other. - - The following HTML attributes can be set via the helper: - - * `checked` - * `disabled` - * `tabindex` - * `indeterminate` - * `name` - * `autofocus` - * `form` - - ### Extending `Checkbox` - - Internally, `{{input type="checkbox"}}` creates an instance of `Checkbox`, passing - arguments from the helper to `Checkbox`'s `create` method. You can extend the - capablilties of checkbox inputs in your applications by reopening this class. For example, - if you wanted to add a css class to all checkboxes in your application: - - ```javascript - import Checkbox from '@ember/component/checkbox'; - - Checkbox.reopen({ - classNames: ['my-app-checkbox'] - }); - ``` - - @method input - @for Ember.Templates.helpers - @param {Hash} options - @public -*/ - -export function inputMacro( - _name: string, +export let inputMacro: ( + name: string, params: Option, hash: Option, builder: OpcodeBuilder -) { - if (params === null) { - params = []; +) => boolean; + +if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + if (DEBUG) { + inputMacro = () => { + throw unreachable(); + } + } +} else { + function buildSyntax( + type: string, + params: any[], + hash: any, + builder: OpcodeBuilder + ) { + let definition = builder.compiler['resolver'].lookupComponentDefinition(type, builder.referrer); + builder.component.static(definition!, [params, hashToArgs(hash), null, null]); + return true; } - if (hash !== null) { - let keys = hash[0]; - let values = hash[1]; - let typeIndex = keys.indexOf('type'); - if (typeIndex > -1) { - let typeArg = values[typeIndex]; - if (Array.isArray(typeArg)) { - // there is an AST plugin that converts this to an expression - // it really should just compile in the component call too. - let inputTypeExpr = params[0] as WireFormat.Expression; - builder.dynamicComponent(inputTypeExpr, null, params.slice(1), hash, true, null, null); - return true; - } - if (typeArg === 'checkbox') { - assert( - "{{input type='checkbox'}} does not support setting `value=someBooleanValue`; " + - 'you must use `checked=someBooleanValue` instead.', - keys.indexOf('value') === -1 - ); - wrapComponentClassAttribute(hash); - return buildSyntax('-checkbox', params, hash, builder); + inputMacro = function inputMacro( + _name: string, + params: Option, + hash: Option, + builder: OpcodeBuilder + ) { + if (params === null) { + params = []; + } + if (hash !== null) { + let keys = hash[0]; + let values = hash[1]; + let typeIndex = keys.indexOf('type'); + + if (typeIndex > -1) { + let typeArg = values[typeIndex]; + if (Array.isArray(typeArg)) { + // there is an AST plugin that converts this to an expression + // it really should just compile in the component call too. + let inputTypeExpr = params[0] as WireFormat.Expression; + builder.dynamicComponent(inputTypeExpr, null, params.slice(1), hash, true, null, null); + return true; + } + if (typeArg === 'checkbox') { + assert( + "`{{input type='checkbox' value=...}}` is not supported; " + + "please use `{{input type='checkbox' checked=...}}` instead.", + keys.indexOf('value') === -1 + ); + wrapComponentClassAttribute(hash); + return buildSyntax('-checkbox', params, hash, builder); + } } } + return buildSyntax('-text-field', params, hash, builder); } - return buildSyntax('-text-field', params, hash, builder); } diff --git a/packages/@ember/-internals/glimmer/lib/templates/input.d.ts b/packages/@ember/-internals/glimmer/lib/templates/input.d.ts new file mode 100644 index 00000000000..786bb5e91a8 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/templates/input.d.ts @@ -0,0 +1,3 @@ +import { Factory } from '../template'; +declare const TEMPLATE: Factory; +export default TEMPLATE; diff --git a/packages/@ember/-internals/glimmer/lib/templates/input.hbs b/packages/@ember/-internals/glimmer/lib/templates/input.hbs new file mode 100644 index 00000000000..3c66f23252f --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/templates/input.hbs @@ -0,0 +1,70 @@ +{{~#if this.isCheckbox~}} + +{{~else~}} + +{{~/if~}} + diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js index 8dbbc40513d..fa1b777c336 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js @@ -1217,12 +1217,38 @@ moduleFor( this.assertText('ab'); } - ['@test GH#14632 give useful warning when calling contextual components with input as a name']() { + ['@feature(!ember-glimmer-angle-bracket-built-ins) GH#14632 give useful warning when calling contextual components with input as a name']() { expectAssertion(() => { this.render('{{component (component "input" type="text")}}'); }, 'Invoking `{{input}}` using angle bracket syntax or `component` helper is not yet supported.'); } + ['@feature(ember-glimmer-angle-bracket-built-ins) it can invoke input component']() { + this.render('{{component (component "input" type="text" value=value)}}', { + value: 'foo', + }); + + this.assertComponentElement(this.firstChild, { + tagName: 'input', + attrs: { + class: 'ember-text-field ember-view', + type: 'text', + }, + }); + + this.assert.strictEqual('foo', this.firstChild.value); + + this.assertStableRerender(); + + runTask(() => this.context.set('value', 'bar')); + + this.assert.strictEqual('bar', this.firstChild.value); + + runTask(() => this.context.set('value', 'foo')); + + this.assert.strictEqual('foo', this.firstChild.value); + } + ['@feature(!ember-glimmer-angle-bracket-built-ins) GH#14632 give useful warning when calling contextual components with textarea as a name']() { expectAssertion(() => { this.render('{{component (component "textarea")}}'); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/input-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/input-angle-test.js new file mode 100644 index 00000000000..61aa01c5e9a --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/components/input-angle-test.js @@ -0,0 +1,1105 @@ +import { RenderingTestCase, moduleFor, runDestroy, runTask } from 'internal-test-helpers'; + +import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features'; +import { assign } from '@ember/polyfills'; +import { set } from '@ember/-internals/metal'; +import { jQuery } from '@ember/-internals/views'; + +import { Component } from '../../utils/helpers'; + +if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + class InputRenderingTest extends RenderingTestCase { + $input() { + return this.$('input'); + } + + inputID() { + return this.$input().prop('id'); + } + + assertDisabled() { + this.assert.ok(this.$('input').prop('disabled'), 'The input is disabled'); + } + + assertNotDisabled() { + this.assert.ok(this.$('input').is(':not(:disabled)'), 'The input is not disabled'); + } + + assertInputId(expectedId) { + this.assert.equal(this.inputID(), expectedId, 'the input id should be `expectedId`'); + } + + assertSingleInput() { + this.assert.equal(this.$('input').length, 1, 'A single text field was inserted'); + } + + assertSingleCheckbox() { + this.assert.equal(this.$('input[type=checkbox]').length, 1, 'A single checkbox is added'); + } + + assertCheckboxIsChecked() { + this.assert.equal(this.$input().prop('checked'), true, 'the checkbox is checked'); + } + + assertCheckboxIsNotChecked() { + this.assert.equal(this.$input().prop('checked'), false, 'the checkbox is not checked'); + } + + assertValue(expected) { + this.assert.equal(this.$input().val(), expected, `the input value should be ${expected}`); + } + + assertAttr(name, expected) { + this.assert.equal( + this.$input().attr(name), + expected, + `the input ${name} attribute has the value '${expected}'` + ); + } + + assertAllAttrs(names, expected) { + names.forEach(name => this.assertAttr(name, expected)); + } + + assertSelectionRange(start, end) { + let input = this.$input()[0]; + this.assert.equal( + input.selectionStart, + start, + `the cursor start position should be ${start}` + ); + this.assert.equal(input.selectionEnd, end, `the cursor end position should be ${end}`); + } + + triggerEvent(type, options) { + let event = document.createEvent('Events'); + event.initEvent(type, true, true); + assign(event, options); + + let element = this.$input()[0]; + runTask(() => { + element.dispatchEvent(event); + }); + } + } + + moduleFor( + 'Components test: ', + class extends InputRenderingTest { + ['@test a single text field is inserted into the DOM']() { + this.render(``, { value: 'hello' }); + + let id = this.inputID(); + + this.assertValue('hello'); + this.assertSingleInput(); + + runTask(() => this.rerender()); + + this.assertValue('hello'); + this.assertSingleInput(); + this.assertInputId(id); + + runTask(() => set(this.context, 'value', 'goodbye')); + + this.assertValue('goodbye'); + this.assertSingleInput(); + this.assertInputId(id); + + runTask(() => set(this.context, 'value', 'hello')); + + this.assertValue('hello'); + this.assertSingleInput(); + this.assertInputId(id); + } + + ['@test default type']() { + this.render(``); + + this.assertAttr('type', 'text'); + + runTask(() => this.rerender()); + + this.assertAttr('type', 'text'); + } + + ['@test dynamic attributes (HTML attribute)']() { + this.render( + ` + `, + { + value: 'Original value', + disabled: false, + placeholder: 'Original placeholder', + name: 'original-name', + maxlength: 10, + minlength: 5, + size: 20, + tabindex: 30, + } + ); + + this.assertNotDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + + runTask(() => this.rerender()); + + this.assertNotDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + + runTask(() => { + set(this.context, 'value', 'Updated value'); + set(this.context, 'disabled', true); + set(this.context, 'placeholder', 'Updated placeholder'); + set(this.context, 'name', 'updated-name'); + set(this.context, 'maxlength', 11); + set(this.context, 'minlength', 6); + // set(this.context, 'size', 21); //NOTE: failing in IE (TEST_SUITE=sauce) + // set(this.context, 'tabindex', 31); //NOTE: failing in IE (TEST_SUITE=sauce) + }); + + this.assertDisabled(); + this.assertValue('Updated value'); + this.assertAttr('placeholder', 'Updated placeholder'); + this.assertAttr('name', 'updated-name'); + this.assertAttr('maxlength', '11'); + this.assertAttr('minlength', '6'); + // this.assertAttr('size', '21'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '31'); //NOTE: failing in IE (TEST_SUITE=sauce) + + runTask(() => { + set(this.context, 'value', 'Original value'); + set(this.context, 'disabled', false); + set(this.context, 'placeholder', 'Original placeholder'); + set(this.context, 'name', 'original-name'); + set(this.context, 'maxlength', 10); + set(this.context, 'minlength', 5); + // set(this.context, 'size', 20); //NOTE: failing in IE (TEST_SUITE=sauce) + // set(this.context, 'tabindex', 30); //NOTE: failing in IE (TEST_SUITE=sauce) + }); + + this.assertNotDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + } + + ['@test dynamic attributes (named argument)']() { + this.render( + ` + `, + { + value: 'Original value', + disabled: false, + placeholder: 'Original placeholder', + name: 'original-name', + maxlength: 10, + minlength: 5, + size: 20, + tabindex: 30, + } + ); + + this.assertNotDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + + runTask(() => this.rerender()); + + this.assertNotDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + + runTask(() => { + set(this.context, 'value', 'Updated value'); + set(this.context, 'disabled', true); + set(this.context, 'placeholder', 'Updated placeholder'); + set(this.context, 'name', 'updated-name'); + set(this.context, 'maxlength', 11); + set(this.context, 'minlength', 6); + // set(this.context, 'size', 21); //NOTE: failing in IE (TEST_SUITE=sauce) + // set(this.context, 'tabindex', 31); //NOTE: failing in IE (TEST_SUITE=sauce) + }); + + this.assertDisabled(); + this.assertValue('Updated value'); + this.assertAttr('placeholder', 'Updated placeholder'); + this.assertAttr('name', 'updated-name'); + this.assertAttr('maxlength', '11'); + this.assertAttr('minlength', '6'); + // this.assertAttr('size', '21'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '31'); //NOTE: failing in IE (TEST_SUITE=sauce) + + runTask(() => { + set(this.context, 'value', 'Original value'); + set(this.context, 'disabled', false); + set(this.context, 'placeholder', 'Original placeholder'); + set(this.context, 'name', 'original-name'); + set(this.context, 'maxlength', 10); + set(this.context, 'minlength', 5); + // set(this.context, 'size', 20); //NOTE: failing in IE (TEST_SUITE=sauce) + // set(this.context, 'tabindex', 30); //NOTE: failing in IE (TEST_SUITE=sauce) + }); + + this.assertNotDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + } + + ['@test static attributes (HTML attribute)']() { + this.render(` + `); + + this.assertDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + + runTask(() => this.rerender()); + + this.assertDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + } + + ['@test static attributes (named argument)']() { + this.render(` + `); + + this.assertDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + + runTask(() => this.rerender()); + + this.assertDisabled(); + this.assertValue('Original value'); + this.assertAttr('placeholder', 'Original placeholder'); + this.assertAttr('name', 'original-name'); + this.assertAttr('maxlength', '10'); + this.assertAttr('minlength', '5'); + // this.assertAttr('size', '20'); //NOTE: failing in IE (TEST_SUITE=sauce) + // this.assertAttr('tabindex', '30'); //NOTE: failing in IE (TEST_SUITE=sauce) + } + + ['@test cursor selection range']() { + // Modifying input.selectionStart, which is utilized in the cursor tests, + // causes an event in Safari. + runDestroy(this.owner.lookup('event_dispatcher:main')); + + this.render(``, { value: 'original' }); + + let input = this.$input()[0]; + + // See https://ember-twiddle.com/33e506329f8176ae874422644d4cc08c?openFiles=components.input-component.js%2Ctemplates.components.input-component.hbs + // this.assertSelectionRange(8, 8); //NOTE: this is (0, 0) on Firefox (TEST_SUITE=sauce) + + runTask(() => this.rerender()); + + // this.assertSelectionRange(8, 8); //NOTE: this is (0, 0) on Firefox (TEST_SUITE=sauce) + + runTask(() => { + input.selectionStart = 2; + input.selectionEnd = 4; + }); + + this.assertSelectionRange(2, 4); + + runTask(() => this.rerender()); + + this.assertSelectionRange(2, 4); + + // runTask(() => set(this.context, 'value', 'updated')); + // + // this.assertSelectionRange(7, 7); //NOTE: this fails in IE, the range is 0 -> 0 (TEST_SUITE=sauce) + // + // runTask(() => set(this.context, 'value', 'original')); + // + // this.assertSelectionRange(8, 8); //NOTE: this fails in IE, the range is 0 -> 0 (TEST_SUITE=sauce) + } + + ['@test [DEPRECATED] sends an action with `` when is pressed']( + assert + ) { + assert.expect(4); + + expectDeprecation(() => { + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed.'); + }, + }, + }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``). (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 13 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } + + ['@test sends an action with `` when is pressed']( + assert + ) { + assert.expect(2); + + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + + this.triggerEvent('keyup', { + keyCode: 13, + }); + } + + ['@test [DEPRECATED] sends an action with `` is pressed'](assert) { + assert.expect(4); + + expectDeprecation(() => { + this.render(``, { + value: 'initial', + + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``). (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keypress', { keyCode: 65 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } + + ['@test sends an action with `` is pressed'](assert) { + assert.expect(2); + + this.render(``, { + value: 'initial', + + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + + this.triggerEvent('keypress', { keyCode: 65 }); + } + + ['@test sends an action to the parent level when `bubbles=true` is provided'](assert) { + assert.expect(1); + + let ParentComponent = Component.extend({ + change() { + assert.ok(true, 'bubbled upwards'); + }, + }); + + this.registerComponent('parent', { + ComponentClass: ParentComponent, + template: ``, + }); + this.render(``); + + this.triggerEvent('change'); + } + + ['@test triggers `focus-in` when focused'](assert) { + let wasFocused = false; + + this.render(``, { + actions: { + foo() { + wasFocused = true; + }, + }, + }); + + runTask(() => { + this.$input().focus(); + }); + + assert.ok(wasFocused, 'action was triggered'); + } + + ['@test sends `insert-newline` when is pressed'](assert) { + assert.expect(2); + + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + + this.triggerEvent('keyup', { + keyCode: 13, + }); + } + + ['@test [DEPRECATED] sends an action with `` when is pressed']( + assert + ) { + assert.expect(4); + + expectDeprecation(() => { + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``). (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 27 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } + + ['@test sends an action with `` when is pressed']( + assert + ) { + assert.expect(2); + + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + + this.triggerEvent('keyup', { keyCode: 27 }); + } + + ['@test [DEPRECATED] sends an action with `` when a key is pressed']( + assert + ) { + assert.expect(4); + + expectDeprecation(() => { + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``). (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keydown', { keyCode: 65 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } + + ['@test sends an action with `` when a key is pressed']( + assert + ) { + assert.expect(2); + + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + + this.triggerEvent('keydown', { keyCode: 65 }); + } + + ['@test [DEPRECATED] sends an action with `` when a key is pressed']( + assert + ) { + assert.expect(4); + + expectDeprecation(() => { + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``). (\'-top-level\' @ L1:C0) '); + + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 65 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } + + ['@test sends an action with `` when a key is pressed']( + assert + ) { + assert.expect(2); + + this.render(``, { + actions: { + foo(value, event) { + assert.ok(true, 'action was triggered'); + assert.ok(event instanceof jQuery.Event, 'jQuery event was passed'); + }, + }, + }); + this.triggerEvent('keyup', { keyCode: 65 }); + } + + ['@test GH#14727 can render a file input after having had render an input of other type']() { + this.render(``); + + this.assert.equal(this.$input()[0].type, 'text'); + this.assert.equal(this.$input()[1].type, 'file'); + } + } + ); + + moduleFor( + 'Components test: with dynamic type', + class extends InputRenderingTest { + ['@test a bound property can be used to determine type']() { + this.render(``, { type: 'password' }); + + this.assertAttr('type', 'password'); + + runTask(() => this.rerender()); + + this.assertAttr('type', 'password'); + + runTask(() => set(this.context, 'type', 'text')); + + this.assertAttr('type', 'text'); + + runTask(() => set(this.context, 'type', 'password')); + + this.assertAttr('type', 'password'); + } + + ['@test a subexpression can be used to determine type']() { + this.render(``, { + isTruthy: true, + trueType: 'text', + falseType: 'password', + }); + + this.assertAttr('type', 'text'); + + runTask(() => this.rerender()); + + this.assertAttr('type', 'text'); + + runTask(() => set(this.context, 'isTruthy', false)); + + this.assertAttr('type', 'password'); + + runTask(() => set(this.context, 'isTruthy', true)); + + this.assertAttr('type', 'text'); + } + + ['@test GH16256 input macro does not modify params in place']() { + this.registerComponent('my-input', { + template: ``, + }); + + this.render(``, { + firstType: 'password', + secondType: 'email', + }); + + let inputs = this.element.querySelectorAll('input'); + this.assert.equal(inputs.length, 2, 'there are two inputs'); + this.assert.equal(inputs[0].getAttribute('type'), 'password'); + this.assert.equal(inputs[1].getAttribute('type'), 'email'); + } + } + ); + + moduleFor( + `Components test: `, + class extends InputRenderingTest { + ['@test dynamic attributes (HTML attribute)']() { + this.render( + ``, + { + disabled: false, + name: 'original-name', + checked: false, + tabindex: 10, + } + ); + + this.assertSingleCheckbox(); + this.assertNotDisabled(); + this.assertAttr('name', 'original-name'); + this.assertAttr('tabindex', '10'); + + runTask(() => this.rerender()); + + this.assertSingleCheckbox(); + this.assertNotDisabled(); + this.assertAttr('name', 'original-name'); + this.assertAttr('tabindex', '10'); + + runTask(() => { + set(this.context, 'disabled', true); + set(this.context, 'name', 'updated-name'); + set(this.context, 'tabindex', 11); + }); + + this.assertSingleCheckbox(); + this.assertDisabled(); + this.assertAttr('name', 'updated-name'); + this.assertAttr('tabindex', '11'); + + runTask(() => { + set(this.context, 'disabled', false); + set(this.context, 'name', 'original-name'); + set(this.context, 'tabindex', 10); + }); + + this.assertSingleCheckbox(); + this.assertNotDisabled(); + this.assertAttr('name', 'original-name'); + this.assertAttr('tabindex', '10'); + } + + ['@test dynamic attributes (named argument)']() { + this.render( + ``, + { + disabled: false, + name: 'original-name', + checked: false, + tabindex: 10, + } + ); + + this.assertSingleCheckbox(); + this.assertNotDisabled(); + this.assertAttr('name', 'original-name'); + this.assertAttr('tabindex', '10'); + + runTask(() => this.rerender()); + + this.assertSingleCheckbox(); + this.assertNotDisabled(); + this.assertAttr('name', 'original-name'); + this.assertAttr('tabindex', '10'); + + runTask(() => { + set(this.context, 'disabled', true); + set(this.context, 'name', 'updated-name'); + set(this.context, 'tabindex', 11); + }); + + this.assertSingleCheckbox(); + this.assertDisabled(); + this.assertAttr('name', 'updated-name'); + this.assertAttr('tabindex', '11'); + + runTask(() => { + set(this.context, 'disabled', false); + set(this.context, 'name', 'original-name'); + set(this.context, 'tabindex', 10); + }); + + this.assertSingleCheckbox(); + this.assertNotDisabled(); + this.assertAttr('name', 'original-name'); + this.assertAttr('tabindex', '10'); + } + + ['@test `value` property assertion']() { + expectAssertion(() => { + this.render(``, { + value: 'value', + }); + }, /checkbox.+@value.+not supported.+use.+@checked.+instead/); + } + + ['@test with a bound type']() { + this.render(``, { + inputType: 'checkbox', + isChecked: true, + }); + + this.assertSingleCheckbox(); + this.assertCheckboxIsChecked(); + + runTask(() => this.rerender()); + + this.assertCheckboxIsChecked(); + + runTask(() => set(this.context, 'isChecked', false)); + + this.assertCheckboxIsNotChecked(); + + runTask(() => set(this.context, 'isChecked', true)); + + this.assertCheckboxIsChecked(); + } + + ['@test native click changes check property']() { + this.render(``); + + this.assertSingleCheckbox(); + this.assertCheckboxIsNotChecked(); + this.$input()[0].click(); + this.assertCheckboxIsChecked(); + this.$input()[0].click(); + this.assertCheckboxIsNotChecked(); + } + + ['@test with static values (HTML attribute)']() { + this.render( + `` + ); + + this.assertSingleCheckbox(); + this.assertCheckboxIsNotChecked(); + this.assertNotDisabled(); + this.assertAttr('tabindex', '10'); + this.assertAttr('name', 'original-name'); + + runTask(() => this.rerender()); + + this.assertSingleCheckbox(); + this.assertCheckboxIsNotChecked(); + this.assertNotDisabled(); + this.assertAttr('tabindex', '10'); + this.assertAttr('name', 'original-name'); + } + + ['@test with static values (named argument)']() { + this.render( + `` + ); + + this.assertSingleCheckbox(); + this.assertCheckboxIsNotChecked(); + this.assertNotDisabled(); + this.assertAttr('tabindex', '10'); + this.assertAttr('name', 'original-name'); + + runTask(() => this.rerender()); + + this.assertSingleCheckbox(); + this.assertCheckboxIsNotChecked(); + this.assertNotDisabled(); + this.assertAttr('tabindex', '10'); + this.assertAttr('name', 'original-name'); + } + } + ); + + moduleFor( + `Components test: `, + class extends InputRenderingTest { + ['@test null values (HTML attribute)']() { + let attributes = ['disabled', 'placeholder', 'name', 'maxlength', 'size', 'tabindex']; + + this.render( + ` + `, + { + value: null, + disabled: null, + placeholder: null, + name: null, + maxlength: null, + size: null, + tabindex: null, + } + ); + + this.assertValue(''); + this.assertAllAttrs(attributes, undefined); + + runTask(() => this.rerender()); + + this.assertValue(''); + this.assertAllAttrs(attributes, undefined); + + runTask(() => { + set(this.context, 'disabled', true); + set(this.context, 'value', 'Updated value'); + set(this.context, 'placeholder', 'Updated placeholder'); + set(this.context, 'name', 'updated-name'); + set(this.context, 'maxlength', 11); + set(this.context, 'size', 21); + set(this.context, 'tabindex', 31); + }); + + this.assertDisabled(); + this.assertValue('Updated value'); + this.assertAttr('placeholder', 'Updated placeholder'); + this.assertAttr('name', 'updated-name'); + this.assertAttr('maxlength', '11'); + this.assertAttr('size', '21'); + this.assertAttr('tabindex', '31'); + + runTask(() => { + set(this.context, 'disabled', null); + set(this.context, 'value', null); + set(this.context, 'placeholder', null); + set(this.context, 'name', null); + set(this.context, 'maxlength', null); + // set(this.context, 'size', null); //NOTE: this fails with `Error: Failed to set the 'size' property on 'HTMLInputElement': The value provided is 0, which is an invalid size.` (TEST_SUITE=sauce) + set(this.context, 'tabindex', null); + }); + + this.assertAttr('disabled', undefined); + this.assertValue(''); + // this.assertAttr('placeholder', undefined); //NOTE: this fails with a value of "null" (TEST_SUITE=sauce) + // this.assertAttr('name', undefined); //NOTE: this fails with a value of "null" (TEST_SUITE=sauce) + this.assertAttr('maxlength', undefined); + // this.assertAttr('size', undefined); //NOTE: re-enable once `size` bug above has been addressed + this.assertAttr('tabindex', undefined); + } + + ['@test null values (named argument)']() { + let attributes = ['disabled', 'placeholder', 'name', 'maxlength', 'size', 'tabindex']; + + this.render( + ` + `, + { + value: null, + disabled: null, + placeholder: null, + name: null, + maxlength: null, + size: null, + tabindex: null, + } + ); + + this.assertValue(''); + this.assertAllAttrs(attributes, undefined); + + runTask(() => this.rerender()); + + this.assertValue(''); + this.assertAllAttrs(attributes, undefined); + + runTask(() => { + set(this.context, 'disabled', true); + set(this.context, 'value', 'Updated value'); + set(this.context, 'placeholder', 'Updated placeholder'); + set(this.context, 'name', 'updated-name'); + set(this.context, 'maxlength', 11); + set(this.context, 'size', 21); + set(this.context, 'tabindex', 31); + }); + + this.assertDisabled(); + this.assertValue('Updated value'); + this.assertAttr('placeholder', 'Updated placeholder'); + this.assertAttr('name', 'updated-name'); + this.assertAttr('maxlength', '11'); + this.assertAttr('size', '21'); + this.assertAttr('tabindex', '31'); + + runTask(() => { + set(this.context, 'disabled', null); + set(this.context, 'value', null); + set(this.context, 'placeholder', null); + set(this.context, 'name', null); + set(this.context, 'maxlength', null); + // set(this.context, 'size', null); //NOTE: this fails with `Error: Failed to set the 'size' property on 'HTMLInputElement': The value provided is 0, which is an invalid size.` (TEST_SUITE=sauce) + set(this.context, 'tabindex', null); + }); + + this.assertAttr('disabled', undefined); + this.assertValue(''); + // this.assertAttr('placeholder', undefined); //NOTE: this fails with a value of "null" (TEST_SUITE=sauce) + // this.assertAttr('name', undefined); //NOTE: this fails with a value of "null" (TEST_SUITE=sauce) + this.assertAttr('maxlength', undefined); + // this.assertAttr('size', undefined); //NOTE: re-enable once `size` bug above has been addressed + this.assertAttr('tabindex', undefined); + } + } + ); + + // These are the permutations of the set: + // ['type="range"', 'min="-5" max="50"', value="%x"'] + [ + // HTML attribute + '@type="range" min="-5" max="50" @value="%x"', + '@type="range" @value="%x" min="-5" max="50"', + 'min="-5" max="50" @type="range" @value="%x"', + 'min="-5" max="50" @value="%x" @type="range"', + '@value="%x" min="-5" max="50" @type="range"', + '@value="%x" @type="range" min="-5" max="50"', + + // Named argument + '@type="range" @min="-5" @max="50" @value="%x"', + '@type="range" @value="%x" @min="-5" @max="50"', + '@min="-5" @max="50" @type="range" @value="%x"', + '@min="-5" @max="50" @value="%x" @type="range"', + '@value="%x" @min="-5" @max="50" @type="range"', + '@value="%x" @type="range" @min="-5" @max="50"', + ].forEach(attrs => { + moduleFor( + `[GH#15675] Components test: `, + class extends InputRenderingTest { + renderInput(value = 25) { + this.render(``); + } + + ['@test value over default max but below set max is kept']() { + this.renderInput('25'); + this.assertValue('25'); + } + + ['@test value below default min but above set min is kept']() { + this.renderInput('-2'); + this.assertValue('-2'); + } + + ['@test in the valid default range is kept']() { + this.renderInput('5'); + this.assertValue('5'); + } + + ['@test value above max is reset to max']() { + this.renderInput('55'); + this.assertValue('50'); + } + + ['@test value below min is reset to min']() { + this.renderInput('-10'); + this.assertValue('-5'); + } + } + ); + }); +} else { + moduleFor( + 'Components test: ', + class extends RenderingTestCase { + ['@test it is not allowed']() { + expectAssertion(() => { + this.render(``); + }, 'Invoking `{{input}}` using angle bracket syntax or `component` helper is not yet supported.'); + } + } + ); +} diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/input-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/input-curly-test.js similarity index 83% rename from packages/@ember/-internals/glimmer/tests/integration/helpers/input-test.js rename to packages/@ember/-internals/glimmer/tests/integration/components/input-curly-test.js index 4e799d1749c..e58971339bc 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/input-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/input-curly-test.js @@ -1,5 +1,6 @@ import { RenderingTestCase, moduleFor, runDestroy, runTask } from 'internal-test-helpers'; +import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features'; import { assign } from '@ember/polyfills'; import { set } from '@ember/-internals/metal'; import { jQuery } from '@ember/-internals/views'; @@ -78,14 +79,8 @@ class InputRenderingTest extends RenderingTestCase { } moduleFor( - 'Helpers test: {{input}}', + 'Components test: {{input}}', class extends InputRenderingTest { - ['@test should not allow angle bracket invocation']() { - expectAssertion(() => { - this.render(''); - }, 'Invoking `{{input}}` using angle bracket syntax or `component` helper is not yet supported.'); - } - ['@test a single text field is inserted into the DOM']() { this.render(`{{input type="text" value=value}}`, { value: 'hello' }); @@ -293,16 +288,24 @@ moduleFor( }, }, }); - }, '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")}})'); + }, 'Passing actions to components as strings (like `{{input enter="foo"}}`) is deprecated. Please use closure actions instead (`{{input enter=(action "foo")}}`). (\'-top-level\' @ L1:C0) '); + + if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 13 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } else { + 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")}}`).'); + } } ['@test sends an action with `{{input enter=(action "foo")}}` when is pressed']( assert ) { assert.expect(2); + this.render(`{{input enter=(action 'foo')}}`, { actions: { foo(value, event) { @@ -331,11 +334,17 @@ moduleFor( }, }, }); - }, '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")}})'); + }, 'Passing actions to components as strings (like `{{input key-press="foo"}}`) is deprecated. Please use closure actions instead (`{{input key-press=(action "foo")}}`). (\'-top-level\' @ L1:C0) '); + + if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + expectDeprecation(() => { + this.triggerEvent('keypress', { keyCode: 65 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } else { + 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) { @@ -422,11 +431,17 @@ moduleFor( }, }, }); - }, '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")}})'); + }, 'Passing actions to components as strings (like `{{input escape-press="foo"}}`) is deprecated. Please use closure actions instead (`{{input escape-press=(action "foo")}}`). (\'-top-level\' @ L1:C0) '); + + if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 27 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } else { + 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']( @@ -460,11 +475,17 @@ moduleFor( }, }, }); - }, '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")}})'); + }, 'Passing actions to components as strings (like `{{input key-down="foo"}}`) is deprecated. Please use closure actions instead (`{{input key-down=(action "foo")}}`). (\'-top-level\' @ L1:C0) '); + + if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + expectDeprecation(() => { + this.triggerEvent('keydown', { keyCode: 65 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } else { + 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']( @@ -498,16 +519,20 @@ moduleFor( }, }, }); - }, '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")}})'); + }, 'Passing actions to components as strings (like `{{input key-up="foo"}}`) is deprecated. Please use closure actions instead (`{{input key-up=(action "foo")}}`). (\'-top-level\' @ L1:C0) '); + + if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + expectDeprecation(() => { + this.triggerEvent('keyup', { keyCode: 65 }); + }, 'Passing actions to components as strings (like ``) is deprecated. Please use closure actions instead (``).'); + } else { + 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 - ) { + ['@test sends an action with `{{input key-up=(action "foo")}}` when a key is pressed'](assert) { assert.expect(2); this.render(`{{input key-up=(action 'foo')}}`, { @@ -531,7 +556,7 @@ moduleFor( ); moduleFor( - 'Helpers test: {{input}} with dynamic type', + 'Components test: {{input}} with dynamic type', class extends InputRenderingTest { ['@test a bound property can be used to determine type']() { this.render(`{{input type=type}}`, { type: 'password' }); @@ -592,7 +617,7 @@ moduleFor( ); moduleFor( - `Helpers test: {{input type='checkbox'}}`, + `Components test: {{input type='checkbox'}}`, class extends InputRenderingTest { ['@test dynamic attributes']() { this.render( @@ -651,7 +676,7 @@ moduleFor( this.render(`{{input type="checkbox" value=value}}`, { value: 'value', }); - }, /you must use `checked=/); + }, /checkbox.+value.+not supported.+use.+checked.+instead/); } ['@test with a bound type']() { @@ -710,7 +735,7 @@ moduleFor( ); moduleFor( - `Helpers test: {{input type='text'}}`, + `Components test: {{input type='text'}}`, class extends InputRenderingTest { ['@test null values']() { let attributes = ['disabled', 'placeholder', 'name', 'maxlength', 'size', 'tabindex']; @@ -795,7 +820,7 @@ moduleFor( 'value="%x" type="range" min="-5" max="50"', ].forEach(attrs => { moduleFor( - `[GH#15675] Helpers test: {{input ${attrs}}}`, + `[GH#15675] Components test: {{input ${attrs}}}`, class extends InputRenderingTest { renderInput(value = 25) { this.render(`{{input ${attrs.replace('%x', value)}}}`); diff --git a/packages/@ember/-internals/views/lib/mixins/text_support.js b/packages/@ember/-internals/views/lib/mixins/text_support.js index 055908669b2..aa5173bd5c9 100644 --- a/packages/@ember/-internals/views/lib/mixins/text_support.js +++ b/packages/@ember/-internals/views/lib/mixins/text_support.js @@ -4,6 +4,7 @@ import { get, set, Mixin } from '@ember/-internals/metal'; import { TargetActionSupport } from '@ember/-internals/runtime'; +import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features'; import { deprecate } from '@ember/debug'; import { SEND_ACTION } from '@ember/deprecated-features'; @@ -310,15 +311,16 @@ function sendAction(eventName, view, event) { let value = get(view, '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', - } - ); + let message = EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS + ? `Passing actions to components as strings (like \`\`) is deprecated. Please use closure actions instead (\`\`).` + : `Passing actions to components as strings (like \`{{input ${eventName}="${actionName}"}}\`) is deprecated. Please use closure actions instead (\`{{input ${eventName}=(action "${actionName}")}}\`).`; + + deprecate(message, 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, event], diff --git a/packages/ember-template-compiler/lib/plugins/deprecate-send-action.ts b/packages/ember-template-compiler/lib/plugins/deprecate-send-action.ts index e06bda6120a..7745d818ab4 100644 --- a/packages/ember-template-compiler/lib/plugins/deprecate-send-action.ts +++ b/packages/ember-template-compiler/lib/plugins/deprecate-send-action.ts @@ -1,3 +1,4 @@ +import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features'; import { deprecate } from '@ember/debug'; import { SEND_ACTION } from '@ember/deprecated-features'; import { AST, ASTPlugin, ASTPluginEnvironment } from '@glimmer/syntax'; @@ -18,15 +19,48 @@ export default function deprecateSendAction(env: ASTPluginEnvironment): ASTPlugi if (SEND_ACTION) { let { moduleName } = env.meta; - let deprecationMessage = (node: AST.MustacheStatement, evName: string, action: string) => { + let deprecationMessage = (node: AST.Node, eventName: string, actionName: string) => { let sourceInformation = calculateLocationDisplay(moduleName, node.loc); - return `Please refactor \`{{input ${evName}="${action}"}}\` to \`{{input ${evName}=(action "${action}")}}\. ${sourceInformation}`; + + if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS && node.type === 'ElementNode') { + return `Passing actions to components as strings (like \`\`) is deprecated. Please use closure actions instead (\`\`). ${sourceInformation}`; + } else { + return `Passing actions to components as strings (like \`{{input ${eventName}="${actionName}"}}\`) is deprecated. Please use closure actions instead (\`{{input ${eventName}=(action "${actionName}")}}\`). ${sourceInformation}`; + } }; return { name: 'deprecate-send-action', visitor: { + ElementNode(node: AST.ElementNode) { + if (!EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS || node.tag !== 'Input') { + return; + } + + node.attributes.forEach(({ name, value }) => { + if (name.charAt(0) === '@') { + let eventName = name.substring(1); + + if (EVENTS.indexOf(eventName) > -1) { + if (value.type === 'TextNode') { + deprecate(deprecationMessage(node, eventName, value.chars), false, { + id: 'ember-component.send-action', + until: '4.0.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_ember-component-send-action', + }); + } else if (value.type === 'MustacheStatement' && value.path.type === 'StringLiteral') { + deprecate(deprecationMessage(node, eventName, value.path.original), false, { + id: 'ember-component.send-action', + until: '4.0.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_ember-component-send-action', + }); + } + } + } + }); + }, + MustacheStatement(node: AST.MustacheStatement) { if (node.path.original !== 'input') { return; diff --git a/packages/ember-template-compiler/lib/plugins/index.ts b/packages/ember-template-compiler/lib/plugins/index.ts index 2089090076f..3f407a518ac 100644 --- a/packages/ember-template-compiler/lib/plugins/index.ts +++ b/packages/ember-template-compiler/lib/plugins/index.ts @@ -17,6 +17,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 { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features'; import { SEND_ACTION } from '@ember/deprecated-features'; import { ASTPlugin, ASTPluginEnvironment } from '@glimmer/syntax'; @@ -31,7 +32,6 @@ const transforms: Array = [ TransformQuotedBindingsIntoJustBindings, AssertReservedNamedArguments, TransformActionSyntax, - TransformInputTypeSyntax, TransformAttrsIntoArgs, TransformEachInIntoEach, TransformHasBlockSyntax, @@ -42,6 +42,10 @@ const transforms: Array = [ AssertSplattributeExpressions, ]; +if (!EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + transforms.push(TransformInputTypeSyntax); +} + if (SEND_ACTION) { transforms.push(DeprecateSendAction); } diff --git a/packages/ember-template-compiler/lib/plugins/transform-input-type-syntax.ts b/packages/ember-template-compiler/lib/plugins/transform-input-type-syntax.ts index 3f7b67d48ad..b6d879b83b9 100644 --- a/packages/ember-template-compiler/lib/plugins/transform-input-type-syntax.ts +++ b/packages/ember-template-compiler/lib/plugins/transform-input-type-syntax.ts @@ -1,4 +1,6 @@ +import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features'; import { AST, ASTPlugin, ASTPluginEnvironment } from '@glimmer/syntax'; +import { unreachable } from '@glimmer/util'; import { Builders } from '../types'; /** @@ -26,36 +28,46 @@ import { Builders } from '../types'; @class TransformInputTypeSyntax */ -export default function transformInputTypeSyntax(env: ASTPluginEnvironment): ASTPlugin { - let b = env.syntax.builders; +let transformInputTypeSyntax: (env: ASTPluginEnvironment) => ASTPlugin; - return { - name: 'transform-input-type-syntax', +if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { + transformInputTypeSyntax = () => { + throw unreachable(); + }; +} else { + transformInputTypeSyntax = function transformInputTypeSyntax(env: ASTPluginEnvironment): ASTPlugin { + let b = env.syntax.builders; + + return { + name: 'transform-input-type-syntax', - visitor: { - MustacheStatement(node: AST.MustacheStatement) { - if (isInput(node)) { - insertTypeHelperParameter(node, b); - } + visitor: { + MustacheStatement(node: AST.MustacheStatement) { + if (isInput(node)) { + insertTypeHelperParameter(node, b); + } + }, }, - }, - }; -} + }; + } -function isInput(node: AST.MustacheStatement) { - return node.path.original === 'input'; -} + function isInput(node: AST.MustacheStatement) { + return node.path.original === 'input'; + } -function insertTypeHelperParameter(node: AST.MustacheStatement, builders: Builders) { - let pairs = node.hash.pairs; - let pair = null; - for (let i = 0; i < pairs.length; i++) { - if (pairs[i].key === 'type') { - pair = pairs[i]; - break; + function insertTypeHelperParameter(node: AST.MustacheStatement, builders: Builders) { + let pairs = node.hash.pairs; + let pair = null; + for (let i = 0; i < pairs.length; i++) { + if (pairs[i].key === 'type') { + pair = pairs[i]; + break; + } + } + if (pair && pair.value.type !== 'StringLiteral') { + node.params.unshift(builders.sexpr('-input-type', [pair.value], undefined, pair.loc)); } - } - if (pair && pair.value.type !== 'StringLiteral') { - node.params.unshift(builders.sexpr('-input-type', [pair.value], undefined, pair.loc)); } } + +export default transformInputTypeSyntax; 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 index a5925fa41e0..9f7316f3b56 100644 --- a/packages/ember-template-compiler/tests/plugins/deprecate-send-action-test.js +++ b/packages/ember-template-compiler/tests/plugins/deprecate-send-action-test.js @@ -18,7 +18,7 @@ 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) `; + let expectedMessage = `Passing actions to components as strings (like \`{{input ${e}="foo-bar"}}\`) is deprecated. Please use closure actions instead (\`{{input ${e}=(action "foo-bar")}}\`)\. ('baz/foo-bar' @ L1:C0) `; expectDeprecation(() => { compile(`{{input ${e}="foo-bar"}}`, { moduleName: 'baz/foo-bar' });