From dcc2410e75fdc417dd1e1d60d15305a826bfb656 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Sun, 24 Jan 2016 15:30:57 -0500 Subject: [PATCH 1/3] Element Modifier --- text/0111-element-modifier.md | 226 ++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 text/0111-element-modifier.md diff --git a/text/0111-element-modifier.md b/text/0111-element-modifier.md new file mode 100644 index 0000000000..c7d3e57592 --- /dev/null +++ b/text/0111-element-modifier.md @@ -0,0 +1,226 @@ +- Start Date: 2016-01-23 +- RFC PR: https://github.com/emberjs/rfcs/pull/111 +- Ember Issue: + +# Summary + +As an outcome of [RFC 100](https://github.com/emberjs/rfcs/pull/100#issuecomment-172427565) +it was decided that element-space helpers should be added to Ember as a public +API. + +In this example, `add-event-listener` fits the syntax of an element modifier: + +```hbs +Save +``` + +The implementation of `add-event-listener` would have several hooks called during +rendering, similar to the rendering hooks fired on a component. Unlike a +component, there is no template/layout for an element modifier. Unlike a +helper, an element modifier does not return a value. + +# Motivation + +Element modifiers allow for user-space implementation of event listeners, +style, and animation tooling that does not exist in Ember today. It brings +back some functionality removed from the framework in Ember 2.0 (when +various intimate helper APIs were removed). + +The introduction of this API will likely result in the proliferation of +one or several popular addons for managing element event listeners, style +and animation. + +# Detailed design + +### Invocation + +An element modifier is invoked in "element space". This is the space between +`<` and `>` opening an HTML tag. For example: + +```hbs + +Some DOM +Hm... +``` + +Element modifiers may be invoked with params or hash arguments. + +### Definition and lookup + +A basic element modifier is defined with the type of `element-modifier`. For +example these paths would be global element modifiers in an application: + +``` +Classic paths: + + app/element-modifiers/flummux.js + app/element-modifiers/whipperwill.js + +Pods paths: + + app/flummux/element-modifier.js + app/whipperwill/element-modifier.js +``` + +Element modifiers, like component and helpers, are eligible for local lookup. +For example: + +``` +Pods paths: + + app/posts/index/element-modifiers/flummux.js +``` + +The element modifier class is a default export from these files. For example: + +```js +import Ember from 'ember'; + +export default Ember.ElementModifier.extend({}); +``` + +### Hooks + +During rendering and teardown of a target element, any attached element +modifiers will execute a series of hooks. These hooks are: + +* `willInsertElement` (only upon initial render) +* `didUpdateAttrs` (only upon subsequent render) +* `willRender` +* `didRender` +* `didInsertElement` (only upon initial render) +* `willDestroyElement` (only upon teardown) + +These lifecycle hooks are similar to those of the component lifecycle. Similarly +the properties of `this.element` and `this.attrs` may be present during the +execution of a hook. + +* during `willInsertElement`, initial `this.attrs` will be present, but no `this.element` +* during `didUpdateAttrs` both `this.attrs` and `this.element` are present. Additionally + the hook is pass `oldAttrs, newAttrs` as arguments. +* during `willRender` both `this.attrs` and `this.element` are present +* during `didRender` both `this.attrs` and `this.element` are present +* during `didInsertElement` both `this.attrs` and `this.element` are present +* during `willDestroyElement` both `this.attrs` and `this.element` are present + +An example of a simple element modifier definition: + +```hbs + +``` + +```js +// app/element-modifiers/noodle.js +import Ember from 'ember'; + +export default Ember.ElementModifier.extend({ + + init() { + this._super(...arguments); + this._logMouseover = () => console.log('mouseover!'); + }, + + didRender() { + document.addEventListener(this.attrs.mouseover, this._logMouseover); + }, + + didUpdateAttrs(newAttrs, oldAttrs) { + document.removeEventListener(oldAttrs.mouseover, this._logMouseover); + }, + + willDestroyElement() { + document.removeEventListener(this.attrs.mouseover, this._logMouseover); + } + +}); +``` + +### Positional params + +Similar to the [positionalParams](http://emberjs.com/api/classes/Ember.Component.html#property_positionalParams) +API in Ember, positional params can be defined for a modifier. An example: + +```hbs + +``` + +```js +// app/element-modifiers/noodle.js +import Ember from 'ember'; + +const Noodle = Ember.ElementModifier.extend({ + + init() { + this._super(...arguments); + this._logMouseover = () => console.log('mouseover!'); + }, + + didRender() { + document.addEventListener(this.attrs.mouseover, this._logMouseover); + }, + + didUpdateAttrs(newAttrs, oldAttrs) { + document.removeEventListener(oldAttrs.mouseover, this._logMouseover); + }, + + willDestroyElement() { + document.removeEventListener(this.attrs.mouseover, this._logMouseover); + } + +}); + +Noodle.reopenClass({ + positionalParams: ['mouseover'] +}); + +export default Noodle; +``` + +### Rerender + +An element modifier may call `this.rerender()`. This triggers the same +hook execution as would be expected from the change of an attr. + +# Drawbacks + +This is an easy API to abuse, and is coupled more closely to the DOM that +other helpers like components or helpers in that it deals with the setup/teardown +lifecycle of a rendered DOM node. + +Element modifiers are not required to have a `-` or any distinguishing +character, thus they may conflict the variable names. For example given the +following example: + +```hbs + +``` + +It must be assumed that `nudge` is an element modifier. Ember will be constrained +in that it may not add later support for `nudge` being a variable (with the +value of an HTML element-space string for example) without namespace conflicts. + +# Alternatives + +[RFC 100](https://github.com/emberjs/rfcs/pull/100#issuecomment-172427565) attempted +to scenario-solve event listeners across native elements, Ember component root elements, and +web components. It failed to reach a unified design that could serve all three +options. Since there does not appear to be an ideal unified solution, kicking +the challenge to use-space for a few cycles seems ideal. + +Additionally, there are other uses for element modifiers beyond event listener +attachment. These may also have specific, narrow fixes. + +The hooks suggested for element modifier life-cycles do not include +`didInitAttrs`, `didReceiveAttrs`, or `didUpdate`. This +may be an oversight. + +Finally, instead of using component-ish hooks, we could introduce new and +bespoke hooks with whatever level of resolution we want. For example, just +a simple setup and teardown on each rerender. + +We could omit the inclusion of `rerender`. + +# Unresolved questions + +Include attrs hooks mentioned in Alternatives? + From d4b3b87fcf9c8493c9b785c18e7605eb4356b5d3 Mon Sep 17 00:00:00 2001 From: Ilya Radchenko Date: Thu, 8 Sep 2016 11:49:55 -0400 Subject: [PATCH 2/3] Fix noodle example --- text/0111-element-modifier.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0111-element-modifier.md b/text/0111-element-modifier.md index c7d3e57592..02fa83af30 100644 --- a/text/0111-element-modifier.md +++ b/text/0111-element-modifier.md @@ -121,15 +121,15 @@ export default Ember.ElementModifier.extend({ }, didRender() { - document.addEventListener(this.attrs.mouseover, this._logMouseover); + document.addEventListener(this.attrs.logEvent, this._logMouseover); }, didUpdateAttrs(newAttrs, oldAttrs) { - document.removeEventListener(oldAttrs.mouseover, this._logMouseover); + document.removeEventListener(oldAttrs.logEvent, this._logMouseover); }, willDestroyElement() { - document.removeEventListener(this.attrs.mouseover, this._logMouseover); + document.removeEventListener(this.attrs.logEvent, this._logMouseover); } }); From 4de94e0929981c6dd3b78002d5410208278ee1db Mon Sep 17 00:00:00 2001 From: Ilya Radchenko Date: Thu, 8 Sep 2016 11:58:58 -0400 Subject: [PATCH 3/3] Clarity and typo --- text/0111-element-modifier.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/text/0111-element-modifier.md b/text/0111-element-modifier.md index 02fa83af30..5d59405806 100644 --- a/text/0111-element-modifier.md +++ b/text/0111-element-modifier.md @@ -156,21 +156,21 @@ const Noodle = Ember.ElementModifier.extend({ }, didRender() { - document.addEventListener(this.attrs.mouseover, this._logMouseover); + document.addEventListener(this.attrs.logEvent, this._logMouseover); }, didUpdateAttrs(newAttrs, oldAttrs) { - document.removeEventListener(oldAttrs.mouseover, this._logMouseover); + document.removeEventListener(oldAttrs.logEvent, this._logMouseover); }, willDestroyElement() { - document.removeEventListener(this.attrs.mouseover, this._logMouseover); + document.removeEventListener(this.attrs.logEvent, this._logMouseover); } }); Noodle.reopenClass({ - positionalParams: ['mouseover'] + positionalParams: ['logEvent'] }); export default Noodle; @@ -205,7 +205,7 @@ value of an HTML element-space string for example) without namespace conflicts. to scenario-solve event listeners across native elements, Ember component root elements, and web components. It failed to reach a unified design that could serve all three options. Since there does not appear to be an ideal unified solution, kicking -the challenge to use-space for a few cycles seems ideal. +the challenge to user-space for a few cycles seems ideal. Additionally, there are other uses for element modifiers beyond event listener attachment. These may also have specific, narrow fixes.