diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index 2e76976be4a..426c2f15528 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -23,6 +23,7 @@ export const ROOT_REF = symbol('ROOT_REF'); export const IS_DISPATCHING_ATTRS = symbol('IS_DISPATCHING_ATTRS'); export const HAS_BLOCK = symbol('HAS_BLOCK'); export const BOUNDS = symbol('BOUNDS'); +export const DISABLE_TAGLESS_EVENT_CHECK = symbol('DISABLE_TAGLESS_EVENT_CHECK'); /** @module @ember/component @@ -650,7 +651,8 @@ const Component = CoreView.extend( assert( // tslint:disable-next-line:max-line-length `You can not define a function that handles DOM events in the \`${this}\` tagless component since it doesn't have any DOM element.`, - this.tagName !== '' || + this[DISABLE_TAGLESS_EVENT_CHECK] || + this.tagName !== '' || !this.renderer._destinedForDOM || !(() => { let eventDispatcher = getOwner(this).lookup('event_dispatcher:main'); diff --git a/packages/@ember/-internals/glimmer/lib/components/checkbox.ts b/packages/@ember/-internals/glimmer/lib/components/checkbox.ts index 2026f210624..3c7eb676b53 100644 --- a/packages/@ember/-internals/glimmer/lib/components/checkbox.ts +++ b/packages/@ember/-internals/glimmer/lib/components/checkbox.ts @@ -59,6 +59,15 @@ const Checkbox = EmberComponent.extend({ change() { set(this, 'checked', this.element.checked); }, + + __sourceInput: null, + + init() { + if (this.__sourceInput) { + this.__sourceInput.__injectEvents(this); + } + this._super(...arguments); + } }); Checkbox.toString = () => '@ember/component/checkbox'; diff --git a/packages/@ember/-internals/glimmer/lib/components/input.ts b/packages/@ember/-internals/glimmer/lib/components/input.ts index fbccb87a825..c5a54eaddd6 100644 --- a/packages/@ember/-internals/glimmer/lib/components/input.ts +++ b/packages/@ember/-internals/glimmer/lib/components/input.ts @@ -2,10 +2,12 @@ @module @ember/component */ import { computed } from '@ember/-internals/metal'; +import { getOwner } from '@ember/-internals/owner'; import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import Component from '../component'; +import { Dict } from '@glimmer/interfaces'; +import Component, { DISABLE_TAGLESS_EVENT_CHECK } from '../component'; let Input: any; @@ -148,9 +150,24 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { Input = Component.extend({ tagName: '', + init() { + this[DISABLE_TAGLESS_EVENT_CHECK] = true; + this._super(...arguments); + }, + isCheckbox: computed('type', function(this: { type?: unknown }) { return this.type === 'checkbox'; }), + + __injectEvents(target: any) { + let eventDispatcher = getOwner(this).lookup('event_dispatcher:main'); + let events: Dict = (eventDispatcher && eventDispatcher._finalEvents) || {}; + Object.values(events).forEach(key => { + if (this[key]) { + target[key] = this[key]; + } + }); + } }); Input.toString = () => '@ember/component/input'; diff --git a/packages/@ember/-internals/glimmer/lib/components/text-field.ts b/packages/@ember/-internals/glimmer/lib/components/text-field.ts index 31d409ef20b..c7746c62cfe 100644 --- a/packages/@ember/-internals/glimmer/lib/components/text-field.ts +++ b/packages/@ember/-internals/glimmer/lib/components/text-field.ts @@ -153,6 +153,15 @@ const TextField = Component.extend(TextSupport, { @public */ max: null, + + __sourceInput: null, + + init() { + if (this.__sourceInput) { + this.__sourceInput.__injectEvents(this); + } + this._super(...arguments); + } }); TextField.toString = () => '@ember/component/text-field'; diff --git a/packages/@ember/-internals/glimmer/lib/templates/input.hbs b/packages/@ember/-internals/glimmer/lib/templates/input.hbs index 3c66f23252f..92fd18de8f4 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/input.hbs +++ b/packages/@ember/-internals/glimmer/lib/templates/input.hbs @@ -11,6 +11,9 @@ @autofocus={{@autofocus}} @required={{@required}} @form={{@form}} + + @__sourceInput={{this}} + ...attributes /> {{~else~}} @@ -64,6 +67,9 @@ @type={{@type}} @value={{@value}} @width={{@width}} + + @__sourceInput={{this}} + ...attributes /> {{~/if~}} 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 index 61aa01c5e9a..d01842568fe 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/input-angle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/input-angle-test.js @@ -81,6 +81,59 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { element.dispatchEvent(event); }); } + + assertTriggersNativeDOMEvents(type) { + // Defaults from EventDispatcher + let events = { + touchstart: 'touchStart', + touchmove: 'touchMove', + touchend: 'touchEnd', + touchcancel: 'touchCancel', + keydown: 'keyDown', + keyup: 'keyUp', + keypress: 'keyPress', + mousedown: 'mouseDown', + mouseup: 'mouseUp', + contextmenu: 'contextMenu', + click: 'click', + dblclick: 'doubleClick', + mousemove: 'mouseMove', + // These four are specially handled elsewhere + // focusin: 'focusIn', + // focusout: 'focusOut', + // mouseenter: 'mouseEnter', + // mouseleave: 'mouseLeave', + submit: 'submit', + input: 'input', + change: 'change', + dragstart: 'dragStart', + drag: 'drag', + dragenter: 'dragEnter', + dragleave: 'dragLeave', + dragover: 'dragOver', + drop: 'drop', + dragend: 'dragEnd', + }; + + let triggeredEvents = []; + + let typeAttr = type ? `type="${type}" ` : ''; + let actionAttrs = Object.keys(events).map(evt => `@${events[evt]}={{action 'run_${evt}'}}`); + let template = ``; + + let actions = {}; + Object.keys(events).forEach(evt => { + actions[`run_${evt}`] = function() { + triggeredEvents.push(evt); + }; + }); + + this.render(template, { actions }); + + Object.keys(events).forEach(evt => this.triggerEvent(evt)); + + this.assert.deepEqual(triggeredEvents, Object.keys(events), 'called for all events'); + } } moduleFor( @@ -645,6 +698,10 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { this.assert.equal(this.$input()[0].type, 'text'); this.assert.equal(this.$input()[1].type, 'file'); } + + ['@test sends an action with `` for native DOM events']() { + this.assertTriggersNativeDOMEvents(); + } } ); @@ -892,6 +949,10 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { this.assertAttr('tabindex', '10'); this.assertAttr('name', 'original-name'); } + + ['@test sends an action with `` for native DOM events']() { + this.assertTriggersNativeDOMEvents('checkbox'); + } } ); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/input-curly-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/input-curly-test.js index e58971339bc..9c6cd00f02f 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/input-curly-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/input-curly-test.js @@ -76,6 +76,59 @@ class InputRenderingTest extends RenderingTestCase { element.dispatchEvent(event); }); } + + assertTriggersNativeDOMEvents(type) { + // Defaults from EventDispatcher + let events = { + touchstart: 'touchStart', + touchmove: 'touchMove', + touchend: 'touchEnd', + touchcancel: 'touchCancel', + keydown: 'keyDown', + keyup: 'keyUp', + keypress: 'keyPress', + mousedown: 'mouseDown', + mouseup: 'mouseUp', + contextmenu: 'contextMenu', + click: 'click', + dblclick: 'doubleClick', + mousemove: 'mouseMove', + // These four are specially handled elsewhere + // focusin: 'focusIn', + // focusout: 'focusOut', + // mouseenter: 'mouseEnter', + // mouseleave: 'mouseLeave', + submit: 'submit', + input: 'input', + change: 'change', + dragstart: 'dragStart', + drag: 'drag', + dragenter: 'dragEnter', + dragleave: 'dragLeave', + dragover: 'dragOver', + drop: 'drop', + dragend: 'dragEnd', + }; + + let triggeredEvents = []; + + let typeAttr = type ? `type="${type}" ` : ''; + let actionAttrs = Object.keys(events).map(evt => `${events[evt]}=(action 'run_${evt}')`); + let template = `{{input ${typeAttr}${actionAttrs.join(' ')}}}`; + + let actions = {}; + Object.keys(events).forEach(evt => { + actions[`run_${evt}`] = function() { + triggeredEvents.push(evt); + }; + }); + + this.render(template, { actions }); + + Object.keys(events).forEach(evt => this.triggerEvent(evt)); + + this.assert.deepEqual(triggeredEvents, Object.keys(events), 'called for all events'); + } } moduleFor( @@ -552,6 +605,10 @@ moduleFor( this.assert.equal(this.$input()[0].type, 'text'); this.assert.equal(this.$input()[1].type, 'file'); } + + ['@test sends an action with `{{input EVENT=(action "foo")}}` for native DOM events']() { + this.assertTriggersNativeDOMEvents(); + } } ); @@ -731,6 +788,10 @@ moduleFor( this.assertAttr('tabindex', '10'); this.assertAttr('name', 'original-name'); } + + ['@test sends an action with `{{input EVENT=(action "foo")}}` for native DOM events']() { + this.assertTriggersNativeDOMEvents('checkbox'); + } } );