Skip to content

Commit

Permalink
[BUGFIX] Tests and support for native DOM events on Inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
wagenet committed Mar 21, 2019
1 parent a142948 commit f7ac046
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 4 deletions.
4 changes: 3 additions & 1 deletion packages/@ember/-internals/glimmer/lib/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<any | undefined>('event_dispatcher:main');
Expand Down
9 changes: 9 additions & 0 deletions packages/@ember/-internals/glimmer/lib/components/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
21 changes: 20 additions & 1 deletion packages/@ember/-internals/glimmer/lib/components/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<any | undefined>('event_dispatcher:main');
let events: Dict<string> = (eventDispatcher && eventDispatcher._finalEvents) || {};
Object.values(events).forEach(key => {
if (this[key]) {
target[key] = this[key];
}
});
},
});

Input.toString = () => '@ember/component/input';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 6 additions & 0 deletions packages/@ember/-internals/glimmer/lib/templates/input.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
@autofocus={{@autofocus}}
@required={{@required}}
@form={{@form}}

@__sourceInput={{this}}

...attributes
/>
{{~else~}}
Expand Down Expand Up @@ -64,6 +67,9 @@
@type={{@type}}
@value={{@value}}
@width={{@width}}

@__sourceInput={{this}}

...attributes
/>
{{~/if~}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'div' });
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 = `<TestComponent ${actionAttrs} /><Input ${typeAttr}${actionAttrs} />`;

this.render(template, { actions });

Object.keys(events).forEach(evt => this.triggerEvent(evt, null, 'div'));
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));

this.assert.deepEqual(triggeredEvents, normallyTriggeredEvents, 'called for all events');
}
}

moduleFor(
Expand Down Expand Up @@ -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 `<Input EVENT={{action "foo"}} />` for native DOM events']() {
this.assertTriggersNativeDOMEvents();
}
}
);

Expand Down Expand Up @@ -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 `<Input EVENT={{action "foo"}} />` for native DOM events']() {
this.assertTriggersNativeDOMEvents('checkbox');
}
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'div' });
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 ${actionAttrs}}}{{input ${typeAttr}${actionAttrs}}}`;

this.render(template, { actions });

Object.keys(events).forEach(evt => this.triggerEvent(evt, null, 'div'));
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));

this.assert.deepEqual(triggeredEvents, normallyTriggeredEvents, 'called for all events');
}
}

moduleFor(
Expand Down Expand Up @@ -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();
}
}
);

Expand Down Expand Up @@ -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');
}
}
);

Expand Down

0 comments on commit f7ac046

Please sign in to comment.