Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modifiers #353

Open
wants to merge 12 commits into
base: master
from

Conversation

@chadhietala
Copy link
Member

chadhietala commented Aug 16, 2018

Rendered

@chadhietala chadhietala force-pushed the new-modifiers branch from 3b52b8e to fa68f50 Aug 16, 2018

locks and others added some commits Aug 17, 2018

@rtablada

This comment has been minimized.

Copy link

rtablada commented Aug 17, 2018

@chadhietala what does this mean for overloaded helpers vs modifiers?

Like action is both?

Is that something we want to allow as public API for other users?

How do we keep that compatible with current helpers and AST transforms like https://github.com/mmun/ember-component-attributes?

@chadhietala

This comment has been minimized.

Copy link
Member Author

chadhietala commented Aug 17, 2018

@rtablada {{action}} is implemented twice in Ember, both as a modifier and helper. You would be able to do that same thing in your app. The position here is what is important, a MustacheStatement in element position is only ever going to lookup a modifier.

@mike-north

This comment has been minimized.

Copy link
Contributor

mike-north commented Aug 20, 2018

Do you have any concerns around making it easy to static-analyze potential misuse of helpers and modifiers? As it stands, we'd have to know more about the implementation of these two things (my-helper, my-modifier) to detect problems at build-time.

<button onClick={{my-modifier "does not emit a value"}} >
<button {{my-helper "emit a value into nothingness"}}>
This hook has the following timing semantics:

**Always**
- called **after** any children modifiers `didInsertElement` hook are called

This comment has been minimized.

@xg-wang

xg-wang Aug 21, 2018

Contributor

'any' => 'all'?

This hook has the following timing semantics:

**Always**
- called **after** any children modifier's `willDestroyElement` hook is called

This comment has been minimized.

@xg-wang

xg-wang Aug 21, 2018

Contributor

'any' => 'all'? or before any children. willDestroyElement sounds more like a hook called before the recursive destruction.

@chadhietala

This comment has been minimized.

Copy link
Member Author

chadhietala commented Aug 21, 2018

@mike-north this is going to result in a compilation error. In the case of the modifier you would get an error like

No modifier named “my-helper” could be found

In the case of the modifier being used outside of element space you will get a helper not found error.

Effectively the location narrows the type.

@chadhietala chadhietala referenced this pull request Aug 23, 2018

Closed

Implement Modifiers #16906


**May or May Not**
- be called in the same tick as DOM insertion
- have the the parent's children fully initialized in DOM

This comment has been minimized.

@sandstrom

sandstrom Aug 23, 2018

Overall very positive to this RFC!

Minor thing: I guess "the parent's children" is the same as "the elements siblings"? If that's the case, would the term sibling be better?

This comment has been minimized.

@mike-north

mike-north Aug 23, 2018

Contributor

I'd also like clarification. If children is sibling elements, can't those belong to sibling components?

MU paths:
src/ui/components/flummux/element-modifier.js
src/ui/routes/posts/-components/whipperwill/element-modifier.js

This comment has been minimized.

@dfreeman

dfreeman Aug 23, 2018

It seems like there's a discrepancy between the type name of element-modifier and the base package + class names of @ember/modifier + Modifier. Is there a reason to have the leading element for one but not the other?

Relatedly, how does a dashed type name play with Rule 3 of the MU resolution rules? What would the identifier for a named element-modifier export be?

@luxferresum

This comment has been minimized.

Copy link

luxferresum commented Aug 24, 2018

I'm not sure what to think about that we increate the API surface for the application developer to make the API for the addon developer simpler.

Not many people will ever write a component manager, but a lot of developers will use modifiers. And I think the story about {{action}} shows that its quite confusing to have an helper and a modifier with the same name. Maybe we could have a different syntax for this?

Oh, and can I pass a modifier to a glimmer component and later use it with ...attributes? :trollface:

@gossi

This comment has been minimized.

Copy link

gossi commented Aug 25, 2018

Some notes that I quickly come up with:

  1. Modifiers !== Components. period. So don't put them into src/ui/components but e.g. src/ui/modifiers. Don't mix. It just happens I have to work on components that were helpers (and placed there), it is messy. Lesson learned, please don't repeat.

  2. As the Component is basically the umbrella to encapsulate the logic around a component (or to bridge it to a 3rd party library), this is now spread across various modifiers?

  3. Will these modifiers be connected to the Component (if there is one) or will they work in isolation? If so, the concept of a component managing its template/DOM is kinda eliminated? Or that's how I feel about it at the moment.

  4. With helper, modifier and Component it increases complexity. There should be a clear path forward. E.g. <div {{my-whatever}} ...> has to be clear where to find it in the code and what it is.

Although I see the benefits of modifiers (e.g. attaching listeners to elements wo/ js code). At the same time I see this critical as well as we will now declaratively set our listeners which is done programmatically in vanilla JS. With native classes Ember is catching up with recent javascript developments modifiers it looks like there is a chance we are splitting apart again? Components mentally always connect to Web Components (and shadow DOM in some way). Ember should more align with these concepts in order to onboard new ember devs.

@chadhietala

This comment has been minimized.

Copy link
Member Author

chadhietala commented Aug 25, 2018

@luxferresum as mentioned before there are 2 implementations of {{action}} today that has the interfaces aligned, however they behave different. The {{action}} helper returns a bound method that is either assigned directly to an element or is closed over and passed. The {{action}} modifier gains access to the underlying element and registers data- attributes that are used by the event dispatcher.

To draw corollary, JS decorators have different implementations based on where the decorator is used. For instance a class decorator cannot be used as a accessor decorator unless the implementation allows for both.

@gossi some answers to your questions

  1. Module Unification has already specified that "components" is a generic term that encompasses components, specialized components, helpers, and modifiers.

  2. As mentioned in the RFC Glimmer components are effectively "fragments" and thus there is not a single this.element that represents the component, so we need a way to give raw assess to the underlying DOM. I wrote another RFC about providing components raw access to the underlying DOM and there were many issues with it.

  3. Does not foreclose on the idea see the alternatives section in this RFC.

  4. Position narrows the type.

@luxferresum

This comment has been minimized.

Copy link

luxferresum commented Aug 25, 2018

I do understand how {{action}} works. I wanted to point out that currently {{action}} is quite hard to teach and there are many blog posts about how it works. So I hope we will eventually get rid of classic actions and don't reintroduce anything like this again with modifiers.

@luxferresum luxferresum referenced this pull request Aug 25, 2018

Open

rendering events #365


Glimmer components have `outerHTML` semantics, meaning what you see in the template is what you get in the DOM, there is no `tagName` that wraps the template. While this drastically simplifies the API for creating new components it makes reliable access to the a component's DOM structure very difficult to do. As [pointed out](https://github.com/emberjs/rfcs/pull/351#issuecomment-412123046) in the [Bounds RFC](https://github.com/emberjs/rfcs/pull/351) the stability of the nodes in the `Bounds` object creates way too many footguns. So a formalized way for accessing DOM in the Glimmer component world is still needed.

Element modifiers allow for stable access of the DOM node they are installed on. This allows for programatic assess to DOM in Glimmer templates and also offers a more targeted construct for cases where classic components were being used. 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.

This comment has been minimized.

@pablobm

pablobm Aug 25, 2018

s/programatic assess/programmatic access/?

@samselikoff

This comment has been minimized.

Copy link

samselikoff commented Aug 28, 2018

This is an awesome RFC, exactly the kind of low-level primitive Ember needs to foster more experimentation. I think the power of abstractions that can be made with these sorts of things are under appreciated.

<button {{on 'click' (action 'save')}}>Save</button> might be a more enticing example (on vs. add-event-listener).

I think these modifiers could be used for so many useful things – animation for one. They are more like a horizontal composition/mixin pattern than a hierarchical/tree-like composition pattern (and many problem areas lend themselves to horizontal rather than vertical methods of reuse). Cannot wait to get my hands on these!

@sandstrom

This comment has been minimized.

Copy link

sandstrom commented Aug 28, 2018

@chadhietala Great examples! 🏅 We've built something similar to the performance marking example, but with components. Using modifiers would be much cleaner!

And although I liked the preceding RFC, this one is easier to understand and more powerful/generic. 👍

@rwjblue
Copy link
Member

rwjblue left a comment

Awesome, I am excited to see this move forward again! Thanks for picking it up @chadhietala!

General questions I have after reviewing:

  • This RFC doesn't specify how modifiers interact with angle bracket invocation. I think it should be specifically called out (either that we support them on angle invocations and what that means or that we don't).
  • Based on the naming, I had assumed that didUpdate would be called both when incoming arguments have changed and when the underlying element was updated (e.g. an attribute value was updated/changed/etc). Did you consider that case and decide that we shouldn't invoke didUpdate?
  • How could a modifier add attributes (e.g. classes, style, selected, etc) before inserted into the DOM? It seems like the earliest hook we have is didInsertElement, but the guarantees there are that it has already been inserted. This would result in FOUC in some cases (imagine a <div {{style this.styles}}></div> modifier) and broken behaviors in others (where specific attributes must be present when inserted into the DOM for them to function properly). Do we need a willInsertElement (or possibly even just pass the element in to the constructor)?

## Motivation

Classic component instances have a `this.element` property which provides you a single DOM node as defined by `tagName`. The children of this node will be the DOM representation of what you wrote in your template. Templates are typically referred to having `innerHTML` semantics in classic components since there is a single wrapping element that is the parent of the template. These semantics allow for components to encapsulate some 3rd party JavaScript library or do some fine grain DOM manipulation.

This comment has been minimized.

@rwjblue

rwjblue Aug 28, 2018

Member

maybe s/classic components/Ember.Component/ ?


Classic component instances have a `this.element` property which provides you a single DOM node as defined by `tagName`. The children of this node will be the DOM representation of what you wrote in your template. Templates are typically referred to having `innerHTML` semantics in classic components since there is a single wrapping element that is the parent of the template. These semantics allow for components to encapsulate some 3rd party JavaScript library or do some fine grain DOM manipulation.

Glimmer components have `outerHTML` semantics, meaning what you see in the template is what you get in the DOM, there is no `tagName` that wraps the template. While this drastically simplifies the API for creating new components it makes reliable access to the a component's DOM structure very difficult to do. As [pointed out](https://github.com/emberjs/rfcs/pull/351#issuecomment-412123046) in the [Bounds RFC](https://github.com/emberjs/rfcs/pull/351) the stability of the nodes in the `Bounds` object creates way too many footguns. So a formalized way for accessing DOM in the Glimmer component world is still needed.

This comment has been minimized.

@rwjblue

rwjblue Aug 28, 2018

Member

s/Glimmer components/custom components/ (we don't have "glimmer components" yet 😛, but "custom components" are included in 3.4.0).

<b {{crum bing='whoop'}} zip="bango">Hm...</b>
```

Element modifiers may be invoked with params or hash arguments.

This comment has been minimized.

@rwjblue

rwjblue Aug 28, 2018

Member

you mean they can accept positional and/or named arguments, right? As written this infers only positional or named are allowed at once (and that you can't use both).

src/ui/routes/posts/-components/whipperwill/modifier.js
```

In Module Unification, modifiers live within the generalized collection type "components" [as specified](https://github.com/dgeb/rfcs/blob/module-unification/text/0000-module-unification.md#components). Modifiers, like component and helpers, are eligible for local lookup. For example:

This comment has been minimized.

@rwjblue

rwjblue Aug 28, 2018

Member

this link should probably not reference dgeb's fork (I think the same link works on emberjs org too?)...


#### `willDestroyElement` semantics

`willDestroyElement` is called during the destruction of a template. It receives no arguments.

This comment has been minimized.

@rwjblue

rwjblue Aug 28, 2018

Member

The wording here seems odd. It doesn't seem that "destruction of a template" is actually the limit of willDestroyElement. For example:

{{#if someCondition}}
  <div {{flummux foo bar}}></div>
{{/if}}

When someCondition change from true to false, I would expect flummux's willDestroyElement to be called but this is unrelated to "destruction of the template".

This hook has the following timing semantics:

**Always**
- called **after** all children modifier's `willDestroyElement` hook is called

This comment was marked as resolved.

@rwjblue

rwjblue Aug 28, 2018

Member

Can this be fleshed out a bit more? Specifically how does this relate to components that are children to the modifiers element.

I assume that this really means "after all children modifiers and components have had willDestroyComponent called?

This comment was marked as resolved.

@chadhietala

chadhietala Aug 29, 2018

Author Member

Added more detail


## Alternatives

The alternative to this is to create a "ref"-like API that is available in [other client side frameworks](https://reactjs.org/docs/refs-and-the-dom.html). This may look like the following:

This comment was marked as resolved.

@rwjblue

rwjblue Aug 28, 2018

Member

I think that modifiers (as proposed here) allows a "ref-like api" to be implemented. Slightly modified example:

<section>
  <h1 {{ref this "heading"}}>Hello!</h1>
  <p>How are you?</p>
</section>

Could easily be implemented as a modifier:

export default class RefModifier extends Modifier {
  didInsertElement([component, name]) {
    component[name](this.element);
  }

  willDestroyElement([component, name]) {
    component[name](null);
  }
}

In other words, I don't think "refs" is an alternative to modifiers but instead is a valid use case of them...

whoops should have read further

@gossi

This comment has been minimized.

Copy link

gossi commented Aug 29, 2018

@chadhietala I did some rereading and get a better understanding. Some of this is now more clear to me.

  1. For how I understand this:
  • modifiers: will be placed inside an element tag, e.g. <div {{on 'click' (action 'huibuuh')}}>
  • helpers: will be placed outside of an element tag, e.g. {{log 'blah'}}

If that's correct, please add it somewhere at the top to give an immediate better understanding.

  1. Can you please provide two more examples, that show the interaction of modifiers with a controller/component. And a second one (more in direction towards MU) how a component will work with local modifiers and how the file layout will look like (more to give us an impression what we will be able to do with Ember Octane 🎉).

  2. Although the MU RFC mentions to put everything component related into src/ui/components I still think, this is a bad idea. It's good to group them under src/ui but should be src/ui/helpers, src/ui/modifiers, ... maybe there will be a future RFC. Just want to rise awareness for now.

Questions

  1. With handlebars you rarely interact with the DOM directly (unless applying third party libraries). Will modifiers be the (only?) location where you should do this?

  2. That would be a follow up on @rwjblue of passing the element in the constructor to apply and manage attributes. I do like this idea. Though contrary to this, this will be an unknown set of attributes managed by a modifier that are non visible in the template (hidden UI). E.g. just use attributes as we would of today: <input disabled={{this.disabled}}>. The question to this is: Is there a use-case where a modifier MUST be used instead of just going the regular way we already have.

@rwjblue
Copy link
Member

rwjblue left a comment

The changes look good, and I think all of the concerns I mentioned in my prior review have been addressed.

I'm 👍...


#### Invocation Without Parameters

- *If* clusure component, then unroll arguments and invoke

This comment has been minimized.

@rwjblue

rwjblue Aug 29, 2018

Member

clusure -> closure

* `didInsertElement`
* `didUpdate`
* `willDestroyElement`

It is important to note that in server-side rendering environments none of the lifecycle events are called.

#### `willInsertElement` semantics
`willInsertElement` is called when the element has been constructed, but has yet to be inserted into the DOM. Any attributes that where defined within the template will be present. This hook is specifically to be used for programmatically attatching additional attributes or properties that must be there before the element is in-DOM. Some examples of this are `style`, `selected`, `value`, etc. This hook is called once.

This comment has been minimized.

@rwjblue

rwjblue Aug 29, 2018

Member

Might need a newline above this to have the header show up properly...

This comment has been minimized.

@rwjblue

rwjblue Aug 29, 2018

Member

"attributes that where defined" -> "attributes that were defined"

@rwjblue
Copy link
Member

rwjblue left a comment

One thing I think needs to be defined in each of the hooks is the ordering when multiple modifiers exist on a single element.

E.g.

<div {{thing-a arg1}} {{thing-b arg1}}></div>

I assume that it is effectively left to right, but would like that as part of the timing guarantees.

@tomdale

This comment has been minimized.

Copy link
Member

tomdale commented Aug 31, 2018

We discussed this RFC at the core team meeting today and there were a few concerns raised that we don't yet have consensus on.

First, there were some concerns about willInsertElement and whether it was the right solution to the use cases @rwjblue raised given trouble we've had in the past about timing guarantees and subtle bugs caused by DOM ordering quirks. @wycats can expand on some of the details here.

Second, we discussed whether this API puts the cart before the horse in terms of implementing the high-level feature before the low-level capability. Specifically, implementing the element modifier equivalent of the Component Manager API would allow us to experiment with specific implementation and API details before committing to a high-level API as laid out in this RFC.

There were some additional concerns around what arguments are passed to hooks and how, but that discussion was tabled until we get a resolution on whether we should merge a Modifier Manager RFC first, which would make the concern moot for the time being.

Lastly, one item that we didn't have time to discuss but has been on my mind: I don't love the name "element modifier" from a teachability perspective. I feel like it emphasizes the details of how it's implemented vs. the value they provide to the programming model.

For example, if I write an addon that tracks whenever a user clicks on an element and annotates it with a custom tag, like this:

<div {{track-click 'User Profile'}}></div>

I don't think it's obvious or important that it's "modifying" the element. To me, it looks like a helper that can be used with an element, rather than producing a value. Maybe "element helper" is a better term?

@sandstrom

This comment has been minimized.

Copy link

sandstrom commented Sep 3, 2018

I think the current name works. But if you'd like to change, how about element enhancements or element augmentations (or simply enhancements or augmentations)?

I think 'element helper' is easily confused with regular helpers.

@topaxi

This comment has been minimized.

Copy link

topaxi commented Sep 3, 2018

Angular calls these things directives, I'm not sure if they directly map to the same thing, but from a learning perspective, I think this might help at least developers with AngularJS/Angular knowledge.

Show resolved Hide resolved text/0000-modifiers.md Outdated
@ryanto

This comment has been minimized.

Copy link

ryanto commented Sep 14, 2018

I'm excited for this one... it's going to unlock some cool ideas.

Would it be possible to partially apply element modifiers like we do with the component helper? I'm imagining contextual element modifiers here...

{{yield (hash
  my-component=(component "my-component" x=x)
  my-element-modifier=(element-modifier "my-element-modifier" x=x)}}

Maybe useful for modifiers that could be considered related, like a drag and drop zone?

{{#drag-and-drop-modifiers as |drag|}}
  <div {{drag.draggable}}></div>
  <div {{drag.droppable}}></div>
{{/drag-and-drop-modifiers}}

@ryanto ryanto referenced this pull request Sep 14, 2018

Merged

Modifier managers #373

@averydev

This comment has been minimized.

Copy link

averydev commented Sep 14, 2018

I don't think it's obvious or important that it's "modifying" the element. To me, it looks like a helper that can be used with an element, rather than producing a value. Maybe "element helper" is a better term?

Agreed! Also, when you're just talking about DOM, "Element" is clear and specific, but when I first read this I wasn't clear that Element was referring to an HTML-Element. That got me thinking perhaps Node rather than Element?

"Node-Helper"
"Node-Enhancer" per @sandstrom's but more closely parallels "Helper" semantic
"Node-Modifier"

@knownasilya

This comment has been minimized.

Copy link
Contributor

knownasilya commented Sep 14, 2018

Node makes you think node.js, so might not be a good candidate either.

@chadhietala

This comment has been minimized.

Copy link
Member Author

chadhietala commented Oct 22, 2018

#373 Was merged and I intend to create an addon that exposes this API so people can try it out.

@gossi

This comment has been minimized.

Copy link

gossi commented Oct 22, 2018

sparkles-modifiers 🎇 ❇️ 🌟 ? 😂

@buschtoens

This comment has been minimized.

Copy link

buschtoens commented Jan 29, 2019

Motivated by #415 (comment) (@ember/render-modifiers) I've published ember-on-modifier, which implements the exemplary {{on}} modifier.

@luxferresum

This comment has been minimized.

Copy link

luxferresum commented Feb 14, 2019

@buschtoens why would I do {{on "click" this.foo}} instead of onclick={{this.foo}}? Also whats is there a difference between <div {{on "click" this.foo}}> and <div {{action this.foo}}>?


And how can willInsertElement implemented while modifier managers when modifier managers dont have an even for this moment? Dont we need this first?

@knownasilya

This comment has been minimized.

Copy link
Contributor

knownasilya commented Feb 14, 2019

@luxferresum element modifier managers RFC was merged and the feature implemented here: emberjs/ember.js#17143

Regarding <div {{on "click" this.foo}}> vs <div {{action this.foo}}>, the first option is nicer because it's clear that it's for click and much easier to understand and read. Also action is overloaded.

Regarding {{on "click" this.foo}} instead of onclick={{this.foo}}, the first option can handle events in a more performant way in the future, plus you can pass in options to the event listener.

@luxferresum

This comment has been minimized.

Copy link

luxferresum commented Feb 14, 2019

@knownasilya But installModifier, the first event that receives the element, will always be "called after DOM insertion".

@knownasilya

This comment has been minimized.

Copy link
Contributor

knownasilya commented Feb 14, 2019

@luxferresum maybe by caching the value and element?

Ah, I don't think willInsert will be a thing, because you can handle that in the constructor (init for classic components) which fires before didInsert. The work around is to fire did-insert on a parent element I think, due to DOM traversal.

At least for now. It looks like https://github.com/emberjs/ember-render-modifiers is the way forward.

@mixonic mixonic referenced this pull request Mar 2, 2019

Closed

Element Modifiers #112

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.