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..8524c5180db 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..23af6e59e15 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,26 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { Input = Component.extend({ tagName: '', + init() { + if (DEBUG) { + 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..dc1206f1717 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..fbb5406662f 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 @@ -71,16 +71,81 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { this.assert.equal(input.selectionEnd, end, `the cursor end position should be ${end}`); } - triggerEvent(type, options) { + triggerEvent(type, options, selector) { let event = document.createEvent('Events'); event.initEvent(type, true, true); assign(event, options); - let element = this.$input()[0]; + let element = this.$(selector || 'input')[0]; runTask(() => { 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', + 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 TestComponent = Component.extend({ tagName: 'input' }); + this.registerComponent('test-component', { ComponentClass: TestComponent }); + + let triggeredEvents = []; + let actions = {}; + Object.keys(events).forEach(evt => { + actions[`run_${evt}`] = function() { + triggeredEvents.push(evt); + }; + }); + + let typeAttr = type ? `type="${type}" ` : ''; + let actionAttrs = Object.keys(events) + .map(evt => `@${events[evt]}={{action 'run_${evt}'}}`) + .join(' '); + let template = ``; + + this.render(template, { actions }); + + Object.keys(events).forEach(evt => this.triggerEvent(evt, null, 'input:first-of-type')); + let normallyTriggeredEvents = [].concat(triggeredEvents); + triggeredEvents.length = 0; + + this.assert.ok( + normallyTriggeredEvents.length > 10, + 'sanity check that most events are triggered' + ); + + normallyTriggeredEvents.forEach(evt => this.triggerEvent(evt, null, 'input:last-of-type')); + + this.assert.deepEqual(triggeredEvents, normallyTriggeredEvents, 'called for all events'); + } } moduleFor( @@ -645,6 +710,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 +961,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..57c9ce1b5c6 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,71 @@ 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', + 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 TestComponent = Component.extend({ tagName: 'input' }); + this.registerComponent('test-component', { ComponentClass: TestComponent }); + + let triggeredEvents = []; + let actions = {}; + Object.keys(events).forEach(evt => { + actions[`run_${evt}`] = function() { + triggeredEvents.push(evt); + }; + }); + + let typeAttr = type ? `type="${type}" ` : ''; + let actionAttrs = Object.keys(events) + .map(evt => `${events[evt]}=(action 'run_${evt}')`) + .join(' '); + let template = `{{test-component ${typeAttr}${actionAttrs}}}{{input ${typeAttr}${actionAttrs}}}`; + + this.render(template, { actions }); + + Object.keys(events).forEach(evt => this.triggerEvent(evt, null, 'input:first-of-type')); + let normallyTriggeredEvents = [].concat(triggeredEvents); + triggeredEvents.length = 0; + + this.assert.ok( + normallyTriggeredEvents.length > 10, + 'sanity check that most events are triggered' + ); + + normallyTriggeredEvents.forEach(evt => this.triggerEvent(evt, null, 'input:last-of-type')); + + this.assert.deepEqual(triggeredEvents, normallyTriggeredEvents, 'called for all events'); + } } moduleFor( @@ -552,6 +617,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 +800,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'); + } } );