From 1f7f7348890fd0d14b112a4307c7d46aa2937840 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Sun, 18 Oct 2015 11:57:24 -0400 Subject: [PATCH 1/4] Kebab Actions --- text/0000-kebab-actions.md | 208 +++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 text/0000-kebab-actions.md diff --git a/text/0000-kebab-actions.md b/text/0000-kebab-actions.md new file mode 100644 index 0000000000..9fb7af8916 --- /dev/null +++ b/text/0000-kebab-actions.md @@ -0,0 +1,208 @@ +- Start Date: 2015-10-17 +- RFC PR: (leave this empty) +- Ember Issue: (leave this empty) + +# Summary + +Ember's `action` helper has two inconsistent forms. The goal of this RFC is to +allow the deprecation or removal of the classic action helper by adding a new +DOM event listener API designed for closure actions. + +The RFC suggests three phases of implementation: + +* In Ember 2.x, kebab actions are released as an API +* In Ember 2.x+1, classic actions will be deprecated +* In Ember 3.0, classic actions will be removed + +# Motivation + +Classic actions bubble a string action through handlers, and are used in the +space of an element. For example: ``. + +Closure actions pluck a function from the immediate context of a template, +and return another function closing over that function and arguments. These +are used where subexpressions are valid in Ember, for example +`{{my-button onChange=(action 'save')}}` or +` +``` + +For comparison, to attach the `save` action with classic actions this syntax +would be used instead: + +```hbs + +``` + +Kebabs are more verbose in the above case, but have the +advantage of a clear and extensible syntax. Consider attaching a +`mouseover` event: + +```hbs +
Save
+``` + +vs. + +```hbs +
Save
+``` + +The kebab version is an basic evolution of the `on-click` usage. + +The second advantage of kebab syntax is its parity with Ember component +action passing. For example consider these components: + +```hbs +{{my-button on-click=(action 'save')}}Save{{/my-button}} +Save +``` + +Kebab syntax is much closer to the above than classic action syntax. Of course +a kebab action passed to a component (or angle bracket component) is simply +a function with no special behavior. + +If any non-function is passed to a kebab, such as in +`on-click="alert('blah');"` an assertion should fail. Only functions are +permitted. + +#### Handling a Kebab Action + +Kebab actions will be invoked with the raw DOM event (not the jQuery event) +as an argument. For example: + +```js +// app/components/my-button.js +export Ember.Component.extend({ + actions: { + save(event) { + console.log('saved with click on ', event.target); + this.get('model').save(); + } + } +}); +``` + +```hbs +{{! app/templates/components/my-button.hbs }} + +``` + +Actions curry arguments, thus the event may not always be the first argument. For +example: + +```js +// app/components/my-input.js +export Ember.Component.extend({ + actions: { + log(prefix, event) { + console.log(prefix, Ember.$(event.target).val()); + } + } +}); +``` + +```hbs +{{! app/templates/components/my-input.hbs }} + +``` + +Additionally, the `value` argument to `action` is useful when passing the +event. + + +```js +// app/components/my-input.js +export Ember.Component.extend({ + actions: { + log(value) { + console.log(target); + } + } +}); +``` + +```hbs +{{! app/templates/components/my-input.hbs }} + +``` + +Passing the event is useful for interacting with the originating DOM node, +but additionally important for allowing event propagation to be controlled +(cancelled). + +#### Event Management + +Kebab actions are lazy. If a kebab action of `on-flummux` is used, then Ember +should listen for the event of `flummux` on the root element and dispatch +that action when it fires. + +If an event from the [list of events Ember listens for](http://emberjs.com/api/classes/Ember.View.html#toc_event-names) +is used, then kebabs can use the already existing listeners. + +# Drawbacks + +Obviously, if this is an RFC, there cannot be drawbacks. + +j/k. + +Kebab actions, as described here, use a dasherized multi-word format. For +example `on-mouse-over`. This corresponds to how Ember already camelizes +event listeners (for example `mouseOver` is the method for a component +listener), but does *not* correspond to the native browser APIs which would +imply `on-mouseover`. As what form of camelization, dasherization, or single-word-ness +to use can be extremely confusing to a newcomer, I am inclined to suggest +matching the native browser instead of following Ember's convention of splitting +the words. + +# Alternatives + +One immediate alternative making the rounds today is using event listeners +directly. For example: + +```hbs + +``` + +The above passes a function to the `onclick` property of the ``. + +Closure actions pluck a function from the immediate context of a template, +and return another function closing over that function and arguments. These +are used where subexpressions are valid in Ember, for example +`{{my-button onChange=(action 'save')}}` or +` +``` + +For comparison, to attach the `save` action with classic actions this syntax +is used today: + +```hbs + +``` + +Attribute actions are more verbose in the above case, but +have the advantage of a clear and extensible syntax. Consider attaching a +`mouseover` event: + +```hbs +
Save
+``` + +vs. + +```hbs +
Save
+``` + +The `onmouseover` attribute action is an evolution of the `onclick` usage. + +A second advantage this change is its parity with Ember component +action passing. For example consider these components: + +```hbs +{{my-button save=(action 'save')}}Save{{/my-button}} +Save +``` + +This common usage of action passing is visually very similar to the new +attribute attachment syntax. Or course an `on*` attribute passed to an +`Ember.Component` is simply a function with no special behavior. + +Passing non-actions to `on*` attributes will be discouraged. + +* Literals should be permitted, such as `onclick="window.alert('foo')"`. +* Non-function bindings are not permitted, and will throw from an assertion. + For example `onclick={{someBoundString}}` will cause an assertion to throw. +* Non-action functions such as `onclick={{someBoundFunction}}` should cause + a warning to be logged. This warning will encourage the user to use + `onclick={{action someBoundFunction}}` or use actions as normally + documented. + +#### Handling an Attribute Action + +`on*` attributes will be invoked with the raw DOM event (not the jQuery event) +as an argument. For example: + +```js +// app/components/my-button.js +export Ember.Component.extend({ + actions: { + save(event) { + console.log('saved with click on ', event.target); + this.get('model').save(); + } + } +}); +``` + +```hbs +{{! app/templates/components/my-button.hbs }} + +``` + +Actions create a closure over arguments, thus the event may not always be +the first argument. For example: + +```js +// app/components/my-input.js +export Ember.Component.extend({ + actions: { + log(prefix, event) { + console.log(prefix, Ember.$(event.target).val()); + } + } +}); +``` + +```hbs +{{! app/templates/components/my-input.hbs }} + +``` + +Additionally, the `value` argument to `action` is useful when passing the +event. + + +```js +// app/components/my-input.js +export Ember.Component.extend({ + actions: { + log(value) { + console.log(target); + } + } +}); +``` + +```hbs +{{! app/templates/components/my-input.hbs }} + +``` + +Passing the event is useful for interacting with the originating DOM node, +but additionally important for allowing event propagation to be controlled +(cancelled). + +#### Event Dispatching + +Attribute actions utilize a native API for dispatching that places them on +the bubbling event dispatcher. Browsers supported by Ember 2.x have three +kind of DOM event attachment: + +* `el.addEventListener('click', handlerFn, false)` add a handler to the bubbling + phase. +* `el.addEventListener('click', handlerFn, true)` add a handler to the capture + phase. +* `el.onclick = handlerFn;` add a handler to the bubbling phase. + +Attribute actions will be dispatched on the bubbling phase, and attached via +`addEventListener`. This ensures that if they are multiple handler making +their way onto an element they do not stomp on each other. + +Glimmer components may (and this is specilation more than design) permit +multiple events to be attached via reflection of invocation attrs to the +root element. For example: + +```hbs +{{! app/routes/index/template.hbs }} +{{! Invoke the component 'my-button' }} + +``` + +```hbs +{{! app/components/my-button/template.hbs }} +{{! Create a div for the root element, with a logging action attached }} +
+``` + +The `logClick` handler should be attached prior to the `save` handler being +attached. + +# Drawbacks + +Other events managed by Ember use a bespoke solution which delegates to +handlers from a single listener on the document body. Because this manager +is on the body, an event dispatched by that system will always execute after +all attribute actions. + +[This JSBin](http://emberjs.jsbin.com/refanumatu/1/edit?html,js,output) demonstrates using action and onclick, and you can see that the +order of the wrapper click event and the action/onclick attached function +are not consistent. Events that will still execute via the event manager are: + +* Those on `Ember.Component` components +* Those attached via element space `{{action 'foo' on="click"}}` usage + +This RFC schedules the deprecation the second API, however to ensure the +ordering problems of `Ember.Component` do not continue into glimmer components +it also recommends that `Ember.GlimmerComponent` have no `save() {` event +attachment API. Instead attribute actions on the root element should be +used. + +Additionally, Ember's [current event naming schema](http://emberjs.com/api/classes/Ember.View.html#toc_event-names) +for handlers on components and to the `on=` argument of `{{action}}` does not +stay consistantly lowercase as the native attribute names do. This inconsistancy +is unfortunate. + +# Alternatives + +Considered (and rejected) was the idea of using `on-*` instead of the native +action attribute name. This option would make clear that native semantics do +not apply, and we could perhaps opt-in to some consistency features such as +delegating attribute actions from the event manager. + +# Unresolved questions + +There is some debate over if `Ember.GlimmerComponent` event attachment should +skip the event manager or not. This has unknown implications to the plan here. +If it should use the event manager, then what happens to ordering of event +handler execution? Do we return to all actions being on the event manager? diff --git a/text/0000-kebab-actions.md b/text/0000-kebab-actions.md deleted file mode 100644 index 9fb7af8916..0000000000 --- a/text/0000-kebab-actions.md +++ /dev/null @@ -1,208 +0,0 @@ -- Start Date: 2015-10-17 -- RFC PR: (leave this empty) -- Ember Issue: (leave this empty) - -# Summary - -Ember's `action` helper has two inconsistent forms. The goal of this RFC is to -allow the deprecation or removal of the classic action helper by adding a new -DOM event listener API designed for closure actions. - -The RFC suggests three phases of implementation: - -* In Ember 2.x, kebab actions are released as an API -* In Ember 2.x+1, classic actions will be deprecated -* In Ember 3.0, classic actions will be removed - -# Motivation - -Classic actions bubble a string action through handlers, and are used in the -space of an element. For example: ``. - -Closure actions pluck a function from the immediate context of a template, -and return another function closing over that function and arguments. These -are used where subexpressions are valid in Ember, for example -`{{my-button onChange=(action 'save')}}` or -` -``` - -For comparison, to attach the `save` action with classic actions this syntax -would be used instead: - -```hbs - -``` - -Kebabs are more verbose in the above case, but have the -advantage of a clear and extensible syntax. Consider attaching a -`mouseover` event: - -```hbs -
Save
-``` - -vs. - -```hbs -
Save
-``` - -The kebab version is an basic evolution of the `on-click` usage. - -The second advantage of kebab syntax is its parity with Ember component -action passing. For example consider these components: - -```hbs -{{my-button on-click=(action 'save')}}Save{{/my-button}} -Save -``` - -Kebab syntax is much closer to the above than classic action syntax. Of course -a kebab action passed to a component (or angle bracket component) is simply -a function with no special behavior. - -If any non-function is passed to a kebab, such as in -`on-click="alert('blah');"` an assertion should fail. Only functions are -permitted. - -#### Handling a Kebab Action - -Kebab actions will be invoked with the raw DOM event (not the jQuery event) -as an argument. For example: - -```js -// app/components/my-button.js -export Ember.Component.extend({ - actions: { - save(event) { - console.log('saved with click on ', event.target); - this.get('model').save(); - } - } -}); -``` - -```hbs -{{! app/templates/components/my-button.hbs }} - -``` - -Actions curry arguments, thus the event may not always be the first argument. For -example: - -```js -// app/components/my-input.js -export Ember.Component.extend({ - actions: { - log(prefix, event) { - console.log(prefix, Ember.$(event.target).val()); - } - } -}); -``` - -```hbs -{{! app/templates/components/my-input.hbs }} - -``` - -Additionally, the `value` argument to `action` is useful when passing the -event. - - -```js -// app/components/my-input.js -export Ember.Component.extend({ - actions: { - log(value) { - console.log(target); - } - } -}); -``` - -```hbs -{{! app/templates/components/my-input.hbs }} - -``` - -Passing the event is useful for interacting with the originating DOM node, -but additionally important for allowing event propagation to be controlled -(cancelled). - -#### Event Management - -Kebab actions are lazy. If a kebab action of `on-flummux` is used, then Ember -should listen for the event of `flummux` on the root element and dispatch -that action when it fires. - -If an event from the [list of events Ember listens for](http://emberjs.com/api/classes/Ember.View.html#toc_event-names) -is used, then kebabs can use the already existing listeners. - -# Drawbacks - -Obviously, if this is an RFC, there cannot be drawbacks. - -j/k. - -Kebab actions, as described here, use a dasherized multi-word format. For -example `on-mouse-over`. This corresponds to how Ember already camelizes -event listeners (for example `mouseOver` is the method for a component -listener), but does *not* correspond to the native browser APIs which would -imply `on-mouseover`. As what form of camelization, dasherization, or single-word-ness -to use can be extremely confusing to a newcomer, I am inclined to suggest -matching the native browser instead of following Ember's convention of splitting -the words. - -# Alternatives - -One immediate alternative making the rounds today is using event listeners -directly. For example: - -```hbs - -``` - -The above passes a function to the `onclick` property of the ``. +Classic actions bubble an action name through handlers, and declared in the +"element space" of an element. For example: + +```hbs + +``` Closure actions pluck a function from the immediate context of a template, -and return another function closing over that function and arguments. These -are used where subexpressions are valid in Ember, for example -`{{my-button onChange=(action 'save')}}` or -` Save +{{my-button save=(action 'save')}}Save{{/my-button}} ``` -This common usage of action passing is visually very similar to the new +Action passing to a component is visually similar to the new attribute attachment syntax. Or course an `on*` attribute passed to an `Ember.Component` is simply a function with no special behavior. Passing non-actions to `on*` attributes will be discouraged. -* Literals should be permitted, such as `onclick="window.alert('foo')"`. * Non-function bindings are not permitted, and will throw from an assertion. For example `onclick={{someBoundString}}` will cause an assertion to throw. * Non-action functions such as `onclick={{someBoundFunction}}` should cause @@ -189,8 +207,33 @@ Attribute actions will be dispatched on the bubbling phase, and attached via their way onto an element they do not stomp on each other. Any attribute named `on*` will be attached as such. -For a webcomponent to dispatch an attached attribute action, it should use -`dispatchEvent`. +#### Web Components + +This RFC suggests official support for actions on Web Components be added to +Ember. + +The web component [best practices](http://webcomponents.org/articles/web-components-best-practices/) +doc as well as discussion on +[2ality](http://www.2ality.com/2015/08/web-component-status.html) +agree that data coming out of a web component should use events. + +The syntax for attaching a listener to a web component will be the same as +any event: + +```hbs + +``` + +Merely setting the `oncustomfoo` property on the DOM node is not sufficient +([this jsbin](http://jsbin.com/vedahareda/1/edit?html,js,output) demonstrates +custom events not calling functions on properties). +The implementation of `my-custom` may (and should) dispatch events to send out +data. + +This is partial motivation for using `addEventListener` over setting the +function on a prop. + +#### Glimmer Components Glimmer components may (and this is speculation more than design) permit multiple events to be attached via reflection of invocation attrs to the @@ -209,10 +252,15 @@ root element. For example: ``` The `logClick` handler should be attached prior to the `save` handler being -attached. +attached. Using the rules we have thus far for property reflection and +Glimmer Components, these props would instead smash. The natural merge +semantics of `addEventListener` are a second +motivation for using that API over setting a function on a prop. # Drawbacks +#### Order of execution + Other events managed by Ember use a bespoke solution which delegates to handlers from a single listener on the document body. Because this manager is on the body, an event dispatched by that system will always execute after @@ -227,18 +275,34 @@ are not consistent. Events that will still execute via the event manager are: * Possibly `Ember.GlimmerComponent` components, however these are current still un-designed in this area. -Additionally, Ember's [current event naming schema](http://emberjs.com/api/classes/Ember.View.html#toc_event-names) +#### Inconsistency with legacy event names + +Ember's [current event naming schema](http://emberjs.com/api/classes/Ember.View.html#toc_event-names) for handlers on components and to the `on=` argument of `{{action}}` does not stay consistantly lowercase as the native attribute names do. This inconsistancy -is unfortunate. +is unfortunate, but moving to the native event names is less surprising than +maintaining the proprietary capitalization. # Alternatives +#### Wildcard on\* + Considered (and rejected) was the idea of using `on-*` instead of the native action attribute name. This option would make clear that native semantics do not apply, and we could perhaps opt-in to some consistency features such as delegating attribute actions from the event manager. +By treating all DOM attributes with `on*` as attribute actions, we stomp on +several english language words that would no longer be viable attribute names. +The [list of words is not terrible](http://www.morewords.com/starts-with/on/). + +A whitelist is not appropriate, as web components may emit custom event names +we have not expected. Some other options to consider: + +* Fall back to `on-*` as an official syntax. +* Use a runtime-configurable whitelist of attribute action event names. +* Use a runtime-configurable blacklist of attribute action event names. + # Unresolved questions None.