-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add jQuery-based event dispatcher as part of RFC386
- Loading branch information
1 parent
1daeb95
commit 295c3cc
Showing
5 changed files
with
294 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { getOwner } from '@ember/application'; | ||
import { assign } from '@ember/polyfills'; | ||
import { assert } from '@ember/debug'; | ||
import { get, set } from '@ember/object'; | ||
import Ember from 'ember'; | ||
import jQuery from 'jquery'; | ||
|
||
const ActionManager = Ember.__loader.require('@ember/-internals/views/lib/system/action_manager').default; | ||
|
||
const ROOT_ELEMENT_CLASS = 'ember-application'; | ||
const ROOT_ELEMENT_SELECTOR = `.${ROOT_ELEMENT_CLASS}`; | ||
|
||
export default Ember.EventDispatcher.extend({ | ||
|
||
rootElement: 'body', | ||
|
||
init() { | ||
this._super(); | ||
|
||
assert( | ||
'EventDispatcher should never be instantiated in fastboot mode. Please report this as an Ember bug.', | ||
(() => { | ||
let owner = getOwner(this); | ||
let environment = owner.lookup('-environment:main'); | ||
|
||
return environment.isInteractive; | ||
})() | ||
); | ||
|
||
this._eventHandlers = Object.create(null); | ||
}, | ||
|
||
setup(addedEvents, _rootElement) { | ||
let events = (this._finalEvents = assign({}, get(this, 'events'), addedEvents)); | ||
|
||
if (_rootElement !== undefined && _rootElement !== null) { | ||
set(this, 'rootElement', _rootElement); | ||
} | ||
|
||
let rootElementSelector = get(this, 'rootElement'); | ||
let rootElement = jQuery(rootElementSelector); | ||
assert( | ||
`You cannot use the same root element (${rootElement.selector || | ||
rootElement[0].tagName}) multiple times in an Ember.Application`, | ||
!rootElement.is(ROOT_ELEMENT_SELECTOR) | ||
); | ||
assert( | ||
'You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', | ||
!rootElement.closest(ROOT_ELEMENT_SELECTOR).length | ||
); | ||
assert( | ||
'You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', | ||
!rootElement.find(ROOT_ELEMENT_SELECTOR).length | ||
); | ||
|
||
rootElement.addClass(ROOT_ELEMENT_CLASS); | ||
|
||
if (!rootElement.is(ROOT_ELEMENT_SELECTOR)) { | ||
throw new TypeError( | ||
`Unable to add '${ROOT_ELEMENT_CLASS}' class to root element (${rootElement.selector || | ||
rootElement[0] | ||
.tagName}). Make sure you set rootElement to the body or an element in the body.` | ||
); | ||
} | ||
|
||
let viewRegistry = this._getViewRegistry(); | ||
|
||
for (let event in events) { | ||
if (events.hasOwnProperty(event)) { | ||
this.setupHandler(rootElement, event, events[event], viewRegistry); | ||
} | ||
} | ||
}, | ||
|
||
setupHandler(rootElement, event, eventName, viewRegistry) { | ||
if (eventName === null) { | ||
return; | ||
} | ||
|
||
rootElement.on(`${event}.ember`, '.ember-view', function(evt) { | ||
let view = viewRegistry[this.id]; | ||
let result = true; | ||
|
||
if (view) { | ||
result = view.handleEvent(eventName, evt); | ||
} | ||
|
||
return result; | ||
}); | ||
|
||
rootElement.on(`${event}.ember`, '[data-ember-action]', evt => { | ||
let attributes = evt.currentTarget.attributes; | ||
let handledActions = []; | ||
|
||
for (let i = 0; i < attributes.length; i++) { | ||
let attr = attributes.item(i); | ||
let attrName = attr.name; | ||
|
||
if (attrName.lastIndexOf('data-ember-action-', 0) !== -1) { | ||
let action = ActionManager.registeredActions[attr.value]; | ||
|
||
// We have to check for action here since in some cases, jQuery will trigger | ||
// an event on `removeChild` (i.e. focusout) after we've already torn down the | ||
// action handlers for the view. | ||
if (action && action.eventName === eventName && handledActions.indexOf(action) === -1) { | ||
action.handler(evt); | ||
// Action handlers can mutate state which in turn creates new attributes on the element. | ||
// This effect could cause the `data-ember-action` attribute to shift down and be invoked twice. | ||
// To avoid this, we keep track of which actions have been handled. | ||
handledActions.push(action); | ||
} | ||
} | ||
} | ||
}); | ||
}, | ||
|
||
destroy() { | ||
let rootElementSelector = get(this, 'rootElement'); | ||
let rootElement; | ||
if (rootElementSelector.nodeType) { | ||
rootElement = rootElementSelector; | ||
} else { | ||
rootElement = document.querySelector(rootElementSelector); | ||
} | ||
|
||
if (!rootElement) { | ||
return; | ||
} | ||
|
||
jQuery(rootElementSelector).off('.ember', '**'); | ||
|
||
rootElement.classList.remove(ROOT_ELEMENT_CLASS); | ||
|
||
return this._super(...arguments); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from '@ember/jquery'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import QUnit, { module, test } from 'qunit'; | ||
import { setupRenderingTest } from 'ember-qunit'; | ||
import { render, click, focus, blur } from '@ember/test-helpers'; | ||
import hbs from 'htmlbars-inline-precompile'; | ||
import Component from '@ember/component'; | ||
import jQuery from 'jquery'; | ||
|
||
function assertJqEvent(event) { | ||
let assert = QUnit.assert; | ||
assert.ok(event, 'event was fired!'); | ||
assert.ok(event instanceof jQuery.Event, 'event is a jQuery event'); | ||
assert.ok(event.originalEvent, 'event has originalEvent'); | ||
} | ||
|
||
module('Integration | EventDispatcher', function(hooks) { | ||
setupRenderingTest(hooks); | ||
|
||
test('a component can handle the click event', async function(assert) { | ||
assert.expect(3); | ||
|
||
this.owner.register('component:handles-click', Component.extend({ | ||
click(e) { | ||
assertJqEvent(e); | ||
} | ||
})); | ||
this.owner.register('template:components/handles-click', hbs`<button>Click me</button>`); | ||
|
||
await render(hbs`{{handles-click id='clickey'}}`); | ||
await click('#clickey'); | ||
}); | ||
|
||
test('actions are properly looked up when clicked directly', async function(assert) { | ||
assert.expect(1); | ||
|
||
this.owner.register('component:handles-click', Component.extend({ | ||
actions: { | ||
handleClick() { | ||
assert.ok(true, 'click was fired!'); | ||
} | ||
} | ||
})); | ||
this.owner.register('template:components/handles-click', hbs`<button {{action 'handleClick'}}>Click me</button>`); | ||
|
||
await render(hbs`{{handles-click id='clickey'}}`); | ||
await click('button'); | ||
}); | ||
|
||
test('actions are properly looked up when clicking nested contents', async function(assert) { | ||
assert.expect(1); | ||
|
||
this.owner.register('component:handles-click', Component.extend({ | ||
actions: { | ||
handleClick() { | ||
assert.ok(true, 'click was fired!'); | ||
} | ||
} | ||
})); | ||
this.owner.register('template:components/handles-click', hbs`<div {{action 'handleClick'}}><button>Click me</button></div>`); | ||
|
||
await render(hbs`{{handles-click id='clickey'}}`); | ||
await click('button'); | ||
}); | ||
|
||
test('unhandled events do not trigger an error', async function(assert) { | ||
assert.expect(0); | ||
|
||
await render(hbs`<button>Click Me!</button>`); | ||
await click('button'); | ||
}); | ||
|
||
test('events bubble up', async function(assert) { | ||
assert.expect(3); | ||
|
||
this.owner.register('component:handles-focusout', Component.extend({ | ||
focusOut(e) { | ||
assertJqEvent(e); | ||
} | ||
})); | ||
this.owner.register('component:input-element', Component.extend({ | ||
tagName: 'input', | ||
|
||
focusOut() { | ||
} | ||
})); | ||
|
||
await render(hbs`{{#handles-focusout}}{{input-element}}{{/handles-focusout}}`); | ||
await focus('input'); | ||
await blur('input'); | ||
}); | ||
|
||
test('events are not stopped by default', async function(assert) { | ||
assert.expect(4); | ||
|
||
this.set('submit', (e) => { | ||
e.preventDefault(); | ||
assert.ok('submit was fired!'); | ||
}); | ||
|
||
this.owner.register('component:submit-button', Component.extend({ | ||
tagName: 'button', | ||
attributeBindings: ['type'], | ||
type: 'submit', | ||
click(e) { | ||
assertJqEvent(e); | ||
} | ||
})); | ||
|
||
await render(hbs`<form onsubmit={{action submit}}>{{submit-button}}</form>`); | ||
await click('button'); | ||
}); | ||
|
||
test('events are stopped when returning false from view handler', async function(assert) { | ||
assert.expect(3); | ||
|
||
this.set('submit', (e) => { | ||
e.preventDefault(); | ||
assert.notOk(true, 'submit should not be fired!'); | ||
}); | ||
|
||
this.owner.register('component:submit-button', Component.extend({ | ||
tagName: 'button', | ||
attributeBindings: ['type'], | ||
type: 'submit', | ||
click(e) { | ||
assertJqEvent(e); | ||
return false; | ||
} | ||
})); | ||
|
||
await render(hbs`<form onsubmit={{action submit}}>{{submit-button}}</form>`); | ||
await click('button'); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.