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

maybe, maybe not better on syntax - this RFC accidentally found some decision trees around when to use {{on}} vs on* #1007

Closed
wants to merge 3 commits into from

Conversation

NullVoxPopuli
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli commented Feb 16, 2024

Propose better on syntax

Rendered

Alternative to RFC#997

Summary

This pull request is proposing a new RFC.

To succeed, it will need to pass into the Exploring Stage), followed by the Accepted Stage.

A Proposed or Exploring RFC may also move to the Closed Stage if it is withdrawn by the author or if it is rejected by the Ember team. This requires an "FCP to Close" period.

An FCP is required before merging this PR to advance to Accepted.

Upon merging this PR, automation will open a draft PR for this RFC to move to the Ready for Released Stage.

Exploring Stage Description

This stage is entered when the Ember team believes the concept described in the RFC should be pursued, but the RFC may still need some more work, discussion, answers to open questions, and/or a champion before it can move to the next stage.

An RFC is moved into Exploring with consensus of the relevant teams. The relevant team expects to spend time helping to refine the proposal. The RFC remains a PR and will have an Exploring label applied.

An Exploring RFC that is successfully completed can move to Accepted with an FCP is required as in the existing process. It may also be moved to Closed with an FCP.

Accepted Stage Description

To move into the "accepted stage" the RFC must have complete prose and have successfully passed through an "FCP to Accept" period in which the community has weighed in and consensus has been achieved on the direction. The relevant teams believe that the proposal is well-specified and ready for implementation. The RFC has a champion within one of the relevant teams.

If there are unanswered questions, we have outlined them and expect that they will be answered before Ready for Release.

When the RFC is accepted, the PR will be merged, and automation will open a new PR to move the RFC to the Ready for Release stage. That PR should be used to track implementation progress and gain consensus to move to the next stage.

Checklist to move to Exploring

  • The team believes the concepts described in the RFC should be pursued.
  • The label S-Proposed is removed from the PR and the label S-Exploring is added.
  • The Ember team is willing to work on the proposal to get it to Accepted

Checklist to move to Accepted

  • This PR has had the Final Comment Period label has been added to start the FCP
  • The RFC is announced in #news-and-announcements in the Ember Discord.
  • The RFC has complete prose, is well-specified and ready for implementation.
    • All sections of the RFC are filled out.
    • Any unanswered questions are outlined and expected to be answered before Ready for Release.
    • "How we teach this?" is sufficiently filled out.
  • The RFC has a champion within one of the relevant teams.
  • The RFC has consensus after the FCP period.

@github-actions github-actions bot added the S-Proposed In the Proposed Stage label Feb 16, 2024
@SergeAstapov
Copy link
Contributor

what do we actually get with the new syntax?
reading through, I'm missing why we can't just keep on built-in and what limitations it has.
what other possibilities new syntax would introduce? will all modifiers as well as custom use new syntax?

@NullVoxPopuli
Copy link
Contributor Author

as well as custom use new syntax?

Naw, just on

what other possibilities new syntax would introduce?

Idk, the main thing is event modifiers

what do we actually get with the new syntax?

Shorter event binding, event modifiers, having builtin event binding look different from userspace modifiers, potentially some perf for cleanup, as usage is more constrained, so we don't need to worry about teardown

@Techn1x
Copy link

Techn1x commented Feb 17, 2024

Feels like a lot of work throughout the ecosystem for little gain. Whole new syntax, would need guides, linting updates..

I like that on being presented as just a modifier makes it clear to devs that it's nothing really special. It's just another modifier, like any we may write for other purposes

@jelhan
Copy link
Contributor

jelhan commented Feb 17, 2024

I see it as a benefit of Ember that even basic functionality like {{on}} modifier can be rebuild using available primitives. That makes it much easier to reason about them. I don't get the benefit of introducing another mental complex for them.

Keeping it aligned with the primitives also eases migration story. No one expects {{on}} modifier to go away any time soon. But I feel 10 years ago we would have thought the same about {{action}}.

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Feb 17, 2024

Counterpoint (to myself) svelte is getting rid of this syntax in favor of normal 'onclick' , 'onkeypress', https://svelte-5-preview.vercel.app/docs/event-handlers#why-the-change

And we can already use those, which i forgot about

@a13o
Copy link

a13o commented Feb 17, 2024

It's easy to teach new Ember devs the {{on}} syntax because it's composed of existing Ember and JS concepts.

The proposed syntax will introduce a new Emberism to memorize, and I don't think it adds enough value to justify its expense on the cognitive load of Ember learners.

The native onclick and proposed on:click are pretty close, I worry about this distinction being too subtle.

The existing {{on}} modifier already supports capture, once, and passive via named args. This makes it easy to teach the {{on}} modifier as a thin wrapper for addEventListener. And with that mental model, there's no expectation that event.preventDefault() was called for you. {{action}} did that, and it was an Octane benefit to drop that and have a little less hamster magic to memorize.

<form on:submit|preventDefault={{@onSubmit}}>

The proposed syntactic sugar for adding the event.preventDefault() logic will live in the template rather than the JS, which is something an Ember developer could grow accustomed to looking for, but would be a gotcha to a Javascript developer trying to learn or evaluate Ember. I don't think Ember in general takes a strong enough position on what logic does or doesn't belong in the template, but I think if the position there were firmed up, it would be a violation to allow 1 specific bit of event logic to live in the template.


With all that said, I do share the concern at the root of the proposal. While template imports are fantastic for the node processes living in the build system, they're a pain for humans authoring components. It's hard to remember the 20 different locations that common things need to be imported from.

I consult this reference almost daily because it just doesn't ever seem to click if one thing comes from @ember/helper, or @ember/modifier, or where things like Input and Textarea live.

Many authors also carry ember-truth-helpers, ember-math-helpers, etc into their gjs/gts out of habit from the split js/hbs days. These libs IMO are not worth the weight of their import statement in the Polaris edition.

Taken in sum, the top of a gjs/gts starts to look like the top of a java file; and without a strong intellisense system to hand wave the problem away. I do think this is an adoption hurdle for Polaris, and reducing the cognitive burden would be a boon for Ember. I'd prefer a holistic approach to this problem rather than a hyperfocus on each specific template import statement.

@evoactivity
Copy link

evoactivity commented Feb 17, 2024

I pretty much agree with what everyone has written.

  • New syntax to teach and work to update all docs
  • Familiarity is good actually
  • It's using primitives and helps people understand it's just a modifier
  • I think preventDefault et al should be dealt with in the JS side, not the template
  • Svelte is moving away from similar syntax as you mentioned

I would like to not have to import the built ins, it makes sense for me to import my user land code but I don't see why things like on, fn, array, hash etc should be imported, they should just be part of the language. I'd support an RFC that takes that approach over one that creates a new syntax.

@flashios09
Copy link

Sorry but i don't like the on:event syntax, i prefer the mustache syntax {{on event doSomething}}, the mustache syntax make it easier to see/capture all the dynamic parts/sequences of the template, anything outside of {{}} is just text/html:

just text here
<button {{on 'click' itsClearThatThisIsAFunctionCall}} id="just-html">Do Something</button>
<span class="text">{{ aDynamicProperty }}</span>

So even we just use the native js event syntax onclick={{doSomething}} or we keep the {{on}} modifier !

I would go more for transforming the native js events behind the scenes to the {{on}} modifier(to make it compatible with EMBER FastBoot), IMO that would be the best solution:

Before:
<button {{on 'click' increment}}>Increment</button>
After:
<button onclick={{increment}}>Increment</button>

@jelhan
Copy link
Contributor

jelhan commented Feb 17, 2024

The onclick HTML property has the major trade-off that it can be only set once. Unless we introduce some Ember-specific syntax, there is no straight forward way converting <button {{on "click" foo}} {{on "click" bar}}> to onclick property.

Additionally, I see it as one of the major problems of Glimmer template syntax that it is unclear if it is an HTML attribute property. This currently depends on logic implementing in the Glimmer VM as far as I remember. We should not make that more complex by introducing more properties, which look like attributes but being handled as properties by the Glimmer VM.

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Feb 17, 2024

Thanks for all the feedback, based on all this, I've opened:

@jelhan
Copy link
Contributor

jelhan commented Feb 17, 2024

demo showing onclick works how we want

* demonstrates that `onclick` can be set more than once

Maybe I'm missing something but that demo does not seem to show that you could use onclick twice on the same element. Of course you can use it on multiple elements. Staying with your demo, I was concerned about the case in which you want to increment and bump the increment on a click event on one button. With {{on}} modifier you can do <button {{on "click" this.increment}} {{on "click" this.bumpIncrement}}>. The onclick attribute would require another function, which call both of them.

We could accept that trade-off for sure. Especially if it has a noticeable performance benefit. Likely primarily an issue for upgrade path and less for new code.

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Feb 17, 2024

twice on the same element.

ah, that's not how I interpreted "it can be only set once."
in the demo, changing incrementBy, re-sets the onclick function, thus setting it more than once 😅

n which you want to increment and bump the increment on a click event on one button.

in this demo, that would have indeterminate behavior cross-browser, because the order of these specific event handlers needs to be, 1. increase the incrementBy, then 2., do the other handler.

it should only be recommended to have multiple event handlers listening to the same event if the event handlers are totally independent in behavior and don't intend to cause the other handlers to behave differently (which is what's happening in my demo 😅 )

We could accept that trade-off for sure. Especially if it has a noticeable performance benefit.

yeah, I think what I'm trying to narrow in on, is a sort of decision tree for when to use which thing:

stateDiagram-v2
  state "use onclick" as useOnclick 
  state "use {{on}}" as useOn
  state "need cleanup" as cleanup
  state "need conditional {{on}}" as cond
  state "need multiple {{on}} the same event name" as multiple
  state "handlers are independent" as indMultiple
  state "combine the handlers to make serial" as serial

  [*] --> cleanup
  cleanup --> cond: no
  cleanup --> useOn: yes
  cond --> useOn: yes
  cond --> multiple: no
  multiple --> indMultiple: yes
  multiple --> useOnclick: no
  indMultiple --> serial: no
  indMultiple --> useOn: yes
  serial --> useOnclick

  
Loading

@NullVoxPopuli NullVoxPopuli changed the title Better on syntax maybe, maybe not better on syntax - this RFC accidentally found some decision trees around when to use {{on}} vs on* Feb 17, 2024
@lifeart
Copy link

lifeart commented Feb 17, 2024

@NullVoxPopuli seems it may be compiler feature - if on is used without extra args and no on modifiers with same event name and no ...attributes compiler could apply prop assign opcode instead of on

@NullVoxPopuli
Copy link
Contributor Author

@lifeart yeah, I like that idea -- we could improve the performance of everyone's apps with a minor update with no change to anyone's code.

and compiler changes like that don't need an RFC, because the wireformat is totally private to ember users

@lifeart
Copy link

lifeart commented Feb 17, 2024

@NullVoxPopuli seems main performance benefit gained not from attribute usage, it depends on do we have destructor or not. And generally on modifier may not have destructors (if properly used), but to get it we need to bypass modifier manager, and this is seems doable because on modifier is an internal implementation.

@kategengler
Copy link
Member

I agree completely with @evoactivity' s comment here, and want to add that microsyntaxes are hard to remember.

@SolPier
Copy link

SolPier commented Feb 19, 2024

Are there other use cases where this syntax could be used ?
So it wouldn't be for one feature only.

@jelhan
Copy link
Contributor

jelhan commented Feb 19, 2024

@NullVoxPopuli seems main performance benefit gained not from attribute usage, it depends on do we have destructor or not.

Do I understand correctly that performance benefit is gained from omitting removeEventListener? That's indeed not needed if destroying the element immediately afterwards. But that's likely not only the case for {{on}} modifier. Also other modifiers may only need to run destructor if modifier is removed but element is not destroyed.

generally on modifier may not have destructors (if properly used)

I don't think that's true. Destructor is critical if the modifier is applied conditionally. Even though that's not that often needed, it's a proper usage.

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Feb 19, 2024

Are there other use cases where this syntax could be used ?

not really, it was only meant for on

. But that's likely not only the case for {{on}} modifier. Also other modifiers may only need to run destructor if modifier is removed but element is not destroyed.

yes, see the decision tree here: #1007 (comment)

I don't think that's true. Destructor is critical if the modifier is applied conditionally.
Even though that's not that often needed, it's a proper usage.

static case is more common than conditional case. I personally cannot be ok with "proper" meaning 'less performant' 🙃 (hence decision tree! 🥳)

@jelhan
Copy link
Contributor

jelhan commented Feb 20, 2024

. But that's likely not only the case for {{on}} modifier. Also other modifiers may only need to run destructor if modifier is removed but element is not destroyed.

yes, see the decision tree here: #1007 (comment)

That decision tree is only working for {{on}} modifier. As there are the on* properties which serves as an alternative. That might be a mitigation if the issue but not a solution.

It sounds as if modifiers need the capability to optionally skip destructor if element is destroyed in the same render cycle. One of the many performance related features still missing for modifiers...

@NullVoxPopuli
Copy link
Contributor Author

That might be a mitigation if the issue but not a solution.

What gap do you see as remaining?

It sounds as if modifiers need the capability to optionally skip destructor if element is destroyed in the same render cycle.

If an element is destroyed, we don't need to worry about cleanup (with on), because an element would be garbage collected, as would any data / event listeners / etc attached to that element (provided no other handles on that data exist in userland).

(for on, specifically) The case where we worry about cleanup is in dynamic modifiers. For all other modifiers, such as using the resize observer service, cleanup is very important)

One of the many performance related features still missing for modifiers...

yeah having more features in modifiers is nice - but is unrelated to how we use modifiers (in any syntax!).

The feature I think we can implement today without any RFC or worry about public API is @lifeart's suggestion, but it is specifically about optimizing DOM cleanup (needed when navigating away from a page).

The feature you want, (and I see you linked from the style-modifier repo), is a pre-paint timing, and that would be amazing, but requires manager changes.

@jelhan
Copy link
Contributor

jelhan commented Feb 20, 2024

The feature you want, (and I see you linked from the style-modifier repo), is a pre-paint timing, and that would be amazing, but requires manager changes.

style modifier is likely affected in the same way as on modifier. It needs to run cleanup only if conditionally applied. Without me noticing it seems that style modifier taken a different decision on the trade-off: it does not support conditional usage and skips cleanup. I'm afraid that supporting conditional usage of style modifier would have negative performance impact on majority of use cases per your performance investigation for on modifier.

@NullVoxPopuli
Copy link
Contributor Author

'm afraid that supporting conditional usage of style modifier would have negative performance impact

Why not have folks do:

<div style="css here {{if cond 'other CSS'}}">

</div>

or

<div class="foo {{if cond 'bar'}}">

</div>
<style>
  .foo {
  
    &.bar {
    
    }
  }
</style>

via https://github.com/cardstack/glimmer-scoped-css
?

@runspired
Copy link
Contributor

imo: the compiler opt isn't worth it for the additional complication and confusion it can lead to around event ordering and cleanup. Events added by a modifier/modifier syntax should all agree on what event delegation timing they have.

@johanrd
Copy link

johanrd commented Feb 21, 2024

Counterpoint (to myself) svelte is getting rid of this syntax in favor of normal 'onclick' , 'onkeypress', https://svelte-5-preview.vercel.app/docs/event-handlers#why-the-change

And we can already use those, which i forgot about

Very intriguing. – Why teach to use a modifier when there is support by HTML natively? As we write in the very beginning of our guides:

Core concept: "Templates are HTML"
At its core, Ember's UIs are HTML driven

onclick seems to me to work well in practice. Yet, currently, Glint complains: Only primitive values are assignable as HTML attributes:

function hello() { 
  alert('hello') 
}

<template>
  <button onclick={{hello}}>Hello</button>
</template>

// Only primitive values (see `AttrValue` in `@glint/template`) are assignable as HTML attributes. 
// If you want to set an event listener, consider using the `{{on}}` modifier instead.
// Type 'void' is not assignable to type 'AttrValue'.glint(2322)

@jelhan
Copy link
Contributor

jelhan commented Feb 21, 2024

onclick seems to me to work well in practice. Yet, currently, Glint complains: Only primitive values are assignable as HTML attributes

I think this is correct. Because we teach that <button onclick={{hello}}> sets the onclick HTML attribute. For historical reasons it is setting the onclick property of the element in this case. That's why it's working. To be clear, it's only working because it is not following HTML syntax in this case.

@NullVoxPopuli
Copy link
Contributor Author

Glint complains: Only primitive values are assignable as HTML attributes

I think this is an oversight, tbh

@NullVoxPopuli
Copy link
Contributor Author

so after talking with a few people it seems the use case for onclick is kind of very specific, so maybe it shouldn't be a default at all and just "a tool you know about for that one specific situation"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-Proposed In the Proposed Stage
Projects
None yet
Development

Successfully merging this pull request may close these issues.