Skip to content

Commit 9985d59

Browse files
committed
Add jQuery-based event dispatcher as part of RFC386
1 parent eb4f5e1 commit 9985d59

File tree

4 files changed

+294
-6
lines changed

4 files changed

+294
-6
lines changed

Diff for: addon/index.js

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { getOwner } from '@ember/application';
2+
import { assign } from '@ember/polyfills';
3+
import { assert } from '@ember/debug';
4+
import { get, set } from '@ember/object';
5+
import Ember from 'ember';
6+
import jQuery from 'jquery';
7+
8+
const ActionManager = Ember.__loader.require('@ember/-internals/views/lib/system/action_manager').default;
9+
10+
const ROOT_ELEMENT_CLASS = 'ember-application';
11+
const ROOT_ELEMENT_SELECTOR = `.${ROOT_ELEMENT_CLASS}`;
12+
13+
export default Ember.EventDispatcher.extend({
14+
15+
rootElement: 'body',
16+
17+
init() {
18+
this._super();
19+
20+
assert(
21+
'EventDispatcher should never be instantiated in fastboot mode. Please report this as an Ember bug.',
22+
(() => {
23+
let owner = getOwner(this);
24+
let environment = owner.lookup('-environment:main');
25+
26+
return environment.isInteractive;
27+
})()
28+
);
29+
30+
this._eventHandlers = Object.create(null);
31+
},
32+
33+
setup(addedEvents, _rootElement) {
34+
let events = (this._finalEvents = assign({}, get(this, 'events'), addedEvents));
35+
36+
if (_rootElement !== undefined && _rootElement !== null) {
37+
set(this, 'rootElement', _rootElement);
38+
}
39+
40+
let rootElementSelector = get(this, 'rootElement');
41+
let rootElement = jQuery(rootElementSelector);
42+
assert(
43+
`You cannot use the same root element (${rootElement.selector ||
44+
rootElement[0].tagName}) multiple times in an Ember.Application`,
45+
!rootElement.is(ROOT_ELEMENT_SELECTOR)
46+
);
47+
assert(
48+
'You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application',
49+
!rootElement.closest(ROOT_ELEMENT_SELECTOR).length
50+
);
51+
assert(
52+
'You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application',
53+
!rootElement.find(ROOT_ELEMENT_SELECTOR).length
54+
);
55+
56+
rootElement.addClass(ROOT_ELEMENT_CLASS);
57+
58+
if (!rootElement.is(ROOT_ELEMENT_SELECTOR)) {
59+
throw new TypeError(
60+
`Unable to add '${ROOT_ELEMENT_CLASS}' class to root element (${rootElement.selector ||
61+
rootElement[0]
62+
.tagName}). Make sure you set rootElement to the body or an element in the body.`
63+
);
64+
}
65+
66+
let viewRegistry = this._getViewRegistry();
67+
68+
for (let event in events) {
69+
if (events.hasOwnProperty(event)) {
70+
this.setupHandler(rootElement, event, events[event], viewRegistry);
71+
}
72+
}
73+
},
74+
75+
setupHandler(rootElement, event, eventName, viewRegistry) {
76+
if (eventName === null) {
77+
return;
78+
}
79+
80+
rootElement.on(`${event}.ember`, '.ember-view', function(evt) {
81+
let view = viewRegistry[this.id];
82+
let result = true;
83+
84+
if (view) {
85+
result = view.handleEvent(eventName, evt);
86+
}
87+
88+
return result;
89+
});
90+
91+
rootElement.on(`${event}.ember`, '[data-ember-action]', evt => {
92+
let attributes = evt.currentTarget.attributes;
93+
let handledActions = [];
94+
95+
for (let i = 0; i < attributes.length; i++) {
96+
let attr = attributes.item(i);
97+
let attrName = attr.name;
98+
99+
if (attrName.lastIndexOf('data-ember-action-', 0) !== -1) {
100+
let action = ActionManager.registeredActions[attr.value];
101+
102+
// We have to check for action here since in some cases, jQuery will trigger
103+
// an event on `removeChild` (i.e. focusout) after we've already torn down the
104+
// action handlers for the view.
105+
if (action && action.eventName === eventName && handledActions.indexOf(action) === -1) {
106+
action.handler(evt);
107+
// Action handlers can mutate state which in turn creates new attributes on the element.
108+
// This effect could cause the `data-ember-action` attribute to shift down and be invoked twice.
109+
// To avoid this, we keep track of which actions have been handled.
110+
handledActions.push(action);
111+
}
112+
}
113+
}
114+
});
115+
},
116+
117+
destroy() {
118+
let rootElementSelector = get(this, 'rootElement');
119+
let rootElement;
120+
if (rootElementSelector.nodeType) {
121+
rootElement = rootElementSelector;
122+
} else {
123+
rootElement = document.querySelector(rootElementSelector);
124+
}
125+
126+
if (!rootElement) {
127+
return;
128+
}
129+
130+
jQuery(rootElementSelector).off('.ember', '**');
131+
132+
rootElement.classList.remove(ROOT_ELEMENT_CLASS);
133+
134+
return this._super(...arguments);
135+
}
136+
});

Diff for: app/event_dispatcher.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '@ember/jquery';

Diff for: index.js

+24-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
'use strict';
22

33
const EMBER_VERSION_WITH_JQUERY_DEPRECATION = '3.9.0-alpha.1';
4+
const EMBER_VERSION_WITHOUT_JQUERY_SUPPORT = '4.0.0-alpha.1';
45

56
module.exports = {
67
name: require('./package').name,
7-
included() {
8-
this._super.included.apply(this, arguments);
8+
9+
init() {
10+
this._super.init.apply(this, arguments);
911

1012
const VersionChecker = require('ember-cli-version-checker');
1113

14+
let checker = new VersionChecker(this);
15+
this._ember = checker.forEmber();
16+
},
17+
18+
included() {
19+
this._super.included.apply(this, arguments);
20+
1221
let app = this._findHost();
1322
let optionalFeatures = app.project.findAddonByName("@ember/optional-features");
1423

@@ -18,10 +27,7 @@ module.exports = {
1827

1928
app.import('vendor/shims/jquery.js');
2029

21-
let checker = new VersionChecker(this);
22-
let ember = checker.forEmber();
23-
24-
if (ember.gte(EMBER_VERSION_WITH_JQUERY_DEPRECATION)) {
30+
if (this._ember.gte(EMBER_VERSION_WITH_JQUERY_DEPRECATION)) {
2531
app.import('vendor/jquery/component.dollar.js');
2632
}
2733

@@ -30,6 +36,18 @@ module.exports = {
3036
}
3137
},
3238

39+
treeForAddon() {
40+
if (this._ember.gte(EMBER_VERSION_WITHOUT_JQUERY_SUPPORT)) {
41+
return this._super.treeForAddon.apply(this, arguments);
42+
}
43+
},
44+
45+
treeForApp() {
46+
if (this._ember.gte(EMBER_VERSION_WITHOUT_JQUERY_SUPPORT)) {
47+
return this._super.treeForApp.apply(this, arguments);
48+
}
49+
},
50+
3351
treeForVendor: function(tree) {
3452
const BroccoliMergeTrees = require('broccoli-merge-trees');
3553
const Funnel = require('broccoli-funnel');
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import QUnit, { module, test } from 'qunit';
2+
import { setupRenderingTest } from 'ember-qunit';
3+
import { render, click, focus, blur } from '@ember/test-helpers';
4+
import hbs from 'htmlbars-inline-precompile';
5+
import Component from '@ember/component';
6+
import jQuery from 'jquery';
7+
8+
function assertJqEvent(event) {
9+
let assert = QUnit.assert;
10+
assert.ok(event, 'event was fired!');
11+
assert.ok(event instanceof jQuery.Event, 'event is a jQuery event');
12+
assert.ok(event.originalEvent, 'event has originalEvent');
13+
}
14+
15+
module('Integration | EventDispatcher', function(hooks) {
16+
setupRenderingTest(hooks);
17+
18+
test('a component can handle the click event', async function(assert) {
19+
assert.expect(3);
20+
21+
this.owner.register('component:handles-click', Component.extend({
22+
click(e) {
23+
assertJqEvent(e);
24+
}
25+
}));
26+
this.owner.register('template:components/handles-click', hbs`<button>Click me</button>`);
27+
28+
await render(hbs`{{handles-click id='clickey'}}`);
29+
await click('#clickey');
30+
});
31+
32+
test('actions are properly looked up when clicked directly', async function(assert) {
33+
assert.expect(1);
34+
35+
this.owner.register('component:handles-click', Component.extend({
36+
actions: {
37+
handleClick() {
38+
assert.ok(true, 'click was fired!');
39+
}
40+
}
41+
}));
42+
this.owner.register('template:components/handles-click', hbs`<button {{action 'handleClick'}}>Click me</button>`);
43+
44+
await render(hbs`{{handles-click id='clickey'}}`);
45+
await click('button');
46+
});
47+
48+
test('actions are properly looked up when clicking nested contents', async function(assert) {
49+
assert.expect(1);
50+
51+
this.owner.register('component:handles-click', Component.extend({
52+
actions: {
53+
handleClick() {
54+
assert.ok(true, 'click was fired!');
55+
}
56+
}
57+
}));
58+
this.owner.register('template:components/handles-click', hbs`<div {{action 'handleClick'}}><button>Click me</button></div>`);
59+
60+
await render(hbs`{{handles-click id='clickey'}}`);
61+
await click('button');
62+
});
63+
64+
test('unhandled events do not trigger an error', async function(assert) {
65+
assert.expect(0);
66+
67+
await render(hbs`<button>Click Me!</button>`);
68+
await click('button');
69+
});
70+
71+
test('events bubble up', async function(assert) {
72+
assert.expect(3);
73+
74+
this.owner.register('component:handles-focusout', Component.extend({
75+
focusOut(e) {
76+
assertJqEvent(e);
77+
}
78+
}));
79+
this.owner.register('component:input-element', Component.extend({
80+
tagName: 'input',
81+
82+
focusOut() {
83+
}
84+
}));
85+
86+
await render(hbs`{{#handles-focusout}}{{input-element}}{{/handles-focusout}}`);
87+
await focus('input');
88+
await blur('input');
89+
});
90+
91+
test('events are not stopped by default', async function(assert) {
92+
assert.expect(4);
93+
94+
this.set('submit', (e) => {
95+
e.preventDefault();
96+
assert.ok('submit was fired!');
97+
});
98+
99+
this.owner.register('component:submit-button', Component.extend({
100+
tagName: 'button',
101+
attributeBindings: ['type'],
102+
type: 'submit',
103+
click(e) {
104+
assertJqEvent(e);
105+
}
106+
}));
107+
108+
await render(hbs`<form onsubmit={{action submit}}>{{submit-button}}</form>`);
109+
await click('button');
110+
});
111+
112+
test('events are stopped when returning false from view handler', async function(assert) {
113+
assert.expect(3);
114+
115+
this.set('submit', (e) => {
116+
e.preventDefault();
117+
assert.notOk(true, 'submit should not be fired!');
118+
});
119+
120+
this.owner.register('component:submit-button', Component.extend({
121+
tagName: 'button',
122+
attributeBindings: ['type'],
123+
type: 'submit',
124+
click(e) {
125+
assertJqEvent(e);
126+
return false;
127+
}
128+
}));
129+
130+
await render(hbs`<form onsubmit={{action submit}}>{{submit-button}}</form>`);
131+
await click('button');
132+
});
133+
});

0 commit comments

Comments
 (0)