Skip to content

Conversation

NullVoxPopuli
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli commented Aug 14, 2025

Propose Template Only Class Components

Rendered

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 Aug 14, 2025
@evoactivity
Copy link

Is there a good reason to introduce a new concept instead of detecting if a standard component has no methods/properties except for the template and doing the optimisation to turn it into this?

var Demo = template(`{{@value}}`, { scope: () => ({}) });

@jrjohnson
Copy link

I was thinking the same thing as @evoactivity. Would be cool if we eliminated the template only paradigm entirely, everything is class based, then the class is optimized away when not needed. This is clearly way more complicated on the build, but I've heard from more than one person that the ember/no-empty-glimmer-component-classes rule is annoying as you don't really know when you get started with a component if you're going to need the backing class or not and there is a temptation to disable that rule.

This rule is currently the only thing preventing me from doing this accidentally, and I'm glad for the rule, but it does point to a place where the mental model for components conflicts with the realities of build and performance. This RFC seems to point the same way.

@NullVoxPopuli
Copy link
Contributor Author

I mean, I think that's a great idea, and it means we only need to teach about a "for fun" detail that folks normally don't have to worry about.

I like that better than what's written here and will update the RFC text to reflect

@NullVoxPopuli
Copy link
Contributor Author

Actually, I don't think that's possible without a breaking change.

Consider:

class A extends Component {
  <template>
     <B @state={{this}} />
  </template>
}

class B extends Component {
  get router() {
    return getOwner(this.args.state).lookup('...');
  }
  <template>
    {{this.router.currentURL}}
  </template>
}

Now... should someone do this? absolutely not. lol
But folks could be doing it today, for less silly reasons than the above example, and converting class A to a template-only component would break their apps.

So naturally, we could say something more specific about the logic that governs when class A would be converted to a template only component -- by saying that this cannot be referenced in the template as well.

This requires that we fully parse the template contents during compilation -- which we currently don't do. The conversions to low-level template() format for A would look like this:

class A {
  static {
    template(`<B @state={{this}} />`, { 
      component: this,
      scope: () => ({ B }) 
    });
  }
}

as proposed, we would ignore the component: this line, that's fine, but normally we leave the first arg passed to template() a string (for libraries).

So, this means only applications would get this optimization, as only in applications (where we would parse and compile the template anyway), could we determine if a this Path exists, and then decide to replace the whole class with a var A = template(....)

Is this an ok trade-off?

@evoactivity
Copy link

evoactivity commented Aug 14, 2025

Would be cool if we eliminated the template only paradigm entirely

@jrjohnson I don't think we need to elimate the paradigm entirely since this is still useful

const rowComp = <template><div>I'm row {{@data}}</div></template>; // <-- template only

class ThingWithRows extends Component {
  rows = [...Array(20).keys()].map( i => i+1); ;

  <template>
    {{#each rows as |row|}}
      <rowComp @data={{row}} />
    {{/each}}
  </template>
}

This requires that we fully parse the template contents during compilation -- which we currently don't do.

@NullVoxPopuli Could we just look in the string for any occurrence of this inside of {{ }} using regex to avoid parsing it fully? If not the trade off sounds reasonable.

@jrjohnson
Copy link

jrjohnson commented Aug 14, 2025

I'm ok with this tradeoff, mainly because I think if you're authoring an add-on or some other way to include components into the application it's reasonable to assume you are willing to learn more stuff. But this difference between authoring an app and an add-on can get really difficult quickly. So it's probably not a great path to take unless we can lint the difference and encourage the behavior needed.

I'm also wondering if we need to look all the way into the <template>. I'm not sure, but would it be possible to determine if a component class only included the template itself and nothing else? Would this make it possible to eliminate the difference and the tradeoff? edit: ah I see, you're passing this elsewhere. Got it.

We certainly don't need to eliminate the const style, but I'd say that the template only const declaration is instead an additional authoring method for that special case and completely available if needed in other places. This is the inverse of the way Octane had us thinking (start with his), but I like the concept for teaching. You always start with a class and then you can add const template stuff as well.

@NullVoxPopuli
Copy link
Contributor Author

Could we just look in the string for any occurrence of this inside of {{ }} using regex to avoid parsing it fully?

I thought about that as well, but regex is hard, and it's why the v3 version of ember-template-imports was so buggy.

Situations that'd be hard to deal with with regex:

  • {{
     this
    
    
    }}
  • {{concat "}}" this }}
  • @arg={{"this"}}

@evoactivity
Copy link

@NullVoxPopuli this monstrosity seems to work in many cases but I take your point

{{(?=(?:[^"'{}]|"[^"]*"|'[^']*')*?\bthis\b(?:[^"'{}]|"[^"]*"|'[^']*')*?\}\})(?:[^"'{}]|"[^"]*"|'[^']*')*?\bthis\b(?:[^"'{}]|"[^"]*"|'[^']*')*?}}

https://regex101.com/r/hD8UqN/3

@NullVoxPopuli
Copy link
Contributor Author

Broke it for you :p https://regex101.com/r/aw8K9W/1

@evoactivity
Copy link

I fixed those cases but it's even worse now. Not exactly maintainable. I will code golf this no more 😄
https://regex101.com/r/aw8K9W/2

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Aug 14, 2025

wrote up a new one: #1134

@NullVoxPopuli
Copy link
Contributor Author

potential alternative:

// current: no generics!
export const Demo = <template>...</template> 

// what Glint would want
// but doing this would require a component manager that hijacks all functions in component space
// (or we compile it away and forbid anything other than the <template></template>)
export function Demo<T>() {
  return template(``)
}

// but our runtime is the equiv of
export const Demo = template('...');

// author-time???
export const Demo = <template type:{
  <T extends Record<string, unknown>, K extends keyof T>() => {
    Element: HTMLTextAreaElement;
    Args: {
      foo: T;
      bar: K;
    },
    Blocks: {
      default: []
    }
  }
}>...</template>

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.

3 participants