Skip to content

Commit

Permalink
[FEATURE] Implement on modifier.
Browse files Browse the repository at this point in the history
See https://emberjs.github.io/rfcs/0471-on-modifier.html for details.

Special thanks to [buschtoens](https://github.com/buschtoens)'s work on
[ember-on-modifier](https://github.com/buschtoens/ember-on-modifier)
which heavily inspired this implementation.
  • Loading branch information
rwjblue committed Apr 23, 2019
1 parent ed4fb96 commit 2c6a039
Show file tree
Hide file tree
Showing 4 changed files with 578 additions and 4 deletions.
241 changes: 241 additions & 0 deletions packages/@ember/-internals/glimmer/lib/modifiers/on.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import { Opaque, Simple } from '@glimmer/interfaces';
import { Tag } from '@glimmer/reference';
import { Arguments, CapturedArguments, ModifierManager } from '@glimmer/runtime';
import { Destroyable } from '@glimmer/util';

/*
Internet Explorer 11 does not support `once` and also does not support
passing `eventOptions`. In some situations it then throws a weird script
error, like:
```
Could not complete the operation due to error 80020101
```
This flag determines, whether `{ once: true }` and thus also event options in
general are supported.
*/
const SUPPORTS_EVENT_OPTIONS = (() => {
try {
const div = document.createElement('div');
let counter = 0;
div.addEventListener('click', () => counter++, { once: true });

let event;
if (typeof Event === 'function') {
event = new Event('click');
} else {
event = document.createEvent('Event');
event.initEvent('click', true, true);
}

div.dispatchEvent(event);
div.dispatchEvent(event);

return counter === 1;
} catch (error) {
return false;
}
})();

export class OnModifierState {
public tag: Tag;
public element: Element;
public args: CapturedArguments;
public eventName!: string;
public callback!: EventListener;
private userProvidedCallback!: EventListener;
public once?: boolean;
public passive?: boolean;
public capture?: boolean;
public options?: AddEventListenerOptions;
public shouldUpdate = true;

constructor(element: Element, args: CapturedArguments) {
this.element = element;
this.args = args;
this.tag = args.tag;
}

updateFromArgs() {
let { args } = this;

let { once, passive, capture }: AddEventListenerOptions = args.named.value();
if (once !== this.once) {
this.once = once;
this.shouldUpdate = true;
}

if (passive !== this.passive) {
this.passive = passive;
this.shouldUpdate = true;
}

if (capture !== this.capture) {
this.capture = capture;
this.shouldUpdate = true;
}

let options: AddEventListenerOptions;
if (once || passive || capture) {
options = this.options = { once, passive, capture };
} else {
this.options = undefined;
}

assert(
'You must pass a valid DOM event name as the first argument to the `on` modifier',
args.positional.at(0) !== undefined && typeof args.positional.at(0).value() === 'string'
);
let eventName = args.positional.at(0).value() as string;
if (eventName !== this.eventName) {
this.eventName = eventName;
this.shouldUpdate = true;
}

assert(
'You must pass a function as the second argument to the `on` modifier',
args.positional.at(1) !== undefined && typeof args.positional.at(1).value() === 'function'
);
let userProvidedCallback = args.positional.at(1).value() as EventListener;
if (userProvidedCallback !== this.userProvidedCallback) {
this.userProvidedCallback = userProvidedCallback;
this.shouldUpdate = true;
}

if (this.shouldUpdate) {
let callback = (this.callback = function(this: Element, event) {
if (DEBUG && passive) {
event.preventDefault = () => {
assert(
`You marked this listener as 'passive', meaning that you must not call 'event.preventDefault()': \n\n${userProvidedCallback}`
);
};
}

if (!SUPPORTS_EVENT_OPTIONS && once) {
removeEventListener(this, eventName, callback, options);
}
return userProvidedCallback.call(null, event);
});
}
}

destroy() {
let { element, eventName, callback, options } = this;

removeEventListener(element, eventName, callback, options);
}
}

let adds = 0;
let removes = 0;

function removeEventListener(
element: Element,
eventName: string,
callback: EventListener,
options?: AddEventListenerOptions
): void {
removes++;

if (SUPPORTS_EVENT_OPTIONS) {
// when options are supported, use them across the board
element.removeEventListener(eventName, callback, options);
} else if (options !== undefined && options.capture) {
// used only in the following case:
//
// `{ once: true | false, passive: true | false, capture: true }
//
// `once` is handled via a custom callback that removes after first
// invocation so we only care about capture here as a boolean
element.removeEventListener(eventName, callback, true);
} else {
// used only in the following cases:
//
// * where there is no options
// * `{ once: true | false, passive: true, capture: false }
element.removeEventListener(eventName, callback);
}
}

function addEventListener(
element: Element,
eventName: string,
callback: EventListener,
options?: AddEventListenerOptions
): void {
adds++;

if (SUPPORTS_EVENT_OPTIONS) {
// when options are supported, use them across the board
element.addEventListener(eventName, callback, options);
} else if (options !== undefined && options.capture) {
// used only in the following case:
//
// `{ once: true | false, passive: true | false, capture: true }
//
// `once` is handled via a custom callback that removes after first
// invocation so we only care about capture here as a boolean
element.addEventListener(eventName, callback, true);
} else {
// used only in the following cases:
//
// * where there is no options
// * `{ once: true | false, passive: true, capture: false }
element.addEventListener(eventName, callback);
}
}

export default class OnModifierManager implements ModifierManager<OnModifierState, Opaque> {
public SUPPORTS_EVENT_OPTIONS: boolean = SUPPORTS_EVENT_OPTIONS;

get counters() {
return { adds, removes };
}

create(element: Simple.Element | Element, _state: Opaque, args: Arguments) {
const capturedArgs = args.capture();

return new OnModifierState(<Element>element, capturedArgs);
}

getTag({ tag }: OnModifierState): Tag {
return tag;
}

install(state: OnModifierState) {
state.updateFromArgs();

let { element, eventName, callback, options } = state;

addEventListener(element, eventName, callback, options);

state.shouldUpdate = false;
}

update(state: OnModifierState) {
// stash prior state for el.removeEventListener
let { element, eventName, callback, options } = state;

state.updateFromArgs();

if (!state.shouldUpdate) {
return;
}

// use prior state values for removal
removeEventListener(element, eventName, callback, options);

// read updated values from the state object
addEventListener(state.element, state.eventName, state.callback, state.options);

state.shouldUpdate = false;
}

getDestructor(state: Destroyable) {
return state;
}
}
15 changes: 11 additions & 4 deletions packages/@ember/-internals/glimmer/lib/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { lookupComponent, lookupPartial, OwnedTemplateMeta } from '@ember/-inter
import {
EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS,
EMBER_GLIMMER_FN_HELPER,
EMBER_GLIMMER_ON_MODIFIER,
EMBER_MODULE_UNIFICATION,
} from '@ember/canary-features';
import { assert } from '@ember/debug';
Expand Down Expand Up @@ -44,6 +45,7 @@ import { default as readonly } from './helpers/readonly';
import { default as unbound } from './helpers/unbound';
import ActionModifierManager from './modifiers/action';
import { CustomModifierDefinition, ModifierManagerDelegate } from './modifiers/custom';
import OnModifierManager from './modifiers/on';
import { populateMacros } from './syntax';
import { mountHelper } from './syntax/mount';
import { outletHelper } from './syntax/outlet';
Expand Down Expand Up @@ -95,10 +97,17 @@ if (EMBER_GLIMMER_FN_HELPER) {
BUILTINS_HELPERS.fn = fn;
}

const BUILTIN_MODIFIERS = {
interface IBuiltInModifiers {
[name: string]: ModifierDefinition | undefined;
}
const BUILTIN_MODIFIERS: IBuiltInModifiers = {
action: { manager: new ActionModifierManager(), state: null },
on: undefined,
};

if (EMBER_GLIMMER_ON_MODIFIER) {
BUILTIN_MODIFIERS.on = { manager: new OnModifierManager(), state: null };
}
export default class RuntimeResolver implements IRuntimeResolver<OwnedTemplateMeta> {
public compiler: LazyCompiler<OwnedTemplateMeta>;

Expand All @@ -109,9 +118,7 @@ export default class RuntimeResolver implements IRuntimeResolver<OwnedTemplateMe

private builtInHelpers: IBuiltInHelpers = BUILTINS_HELPERS;

private builtInModifiers: {
[name: string]: ModifierDefinition;
} = BUILTIN_MODIFIERS;
private builtInModifiers: IBuiltInModifiers = BUILTIN_MODIFIERS;

// supports directly imported late bound layouts on component.prototype.layout
private templateCache: Map<Owner, Map<TemplateFactory, OwnedTemplate>> = new Map();
Expand Down
Loading

0 comments on commit 2c6a039

Please sign in to comment.