Skip to content

renderComponent() #1099

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

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open

renderComponent() #1099

wants to merge 24 commits into from

Conversation

NullVoxPopuli
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli commented May 1, 2025

Propose renderComponent()

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 May 1, 2025
@NullVoxPopuli NullVoxPopuli changed the title import { renderComponent } from '@ember/renderer'; renderComponent() May 1, 2025

When isInteractive is false, modifiers don't run, when true, modifiers do run.

#### `into`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could add after to allow app rendering after some node, without creating extra element wrapper

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never heard of folks doing this -- is there a compelling reason / use case?

(maybe a bunch of render components in a list?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i kinda want to push back on this, as I don't know if the current render can do this.
It can always be added later if we need it

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NullVoxPopuli, I agree.
Main use-case is render ember app in the middle of smf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smf?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NullVoxPopuli in between nodes, for example, we have:

<html>
  <body>
    <header> ama header </header>
    { HERE_I_WANT_TO_RENDER_WHOLE_APP without creating extra node }
    <footer> ama footer </footer>
  </body>
</html>

@gossi
Copy link

gossi commented May 2, 2025

I'm a big fan of this, as this is the baseline to write a storybook adapter for gts files. But I have some questions around this, as I think the narrative is not clear (yet):

  1. Why is it renderComponent() ... when it renders a template? (which I very much prefer). We can use that to render html that uses a helper or modifier. Vue and svelte have similar concepts for helper and modifier, but their testing lacks support for them, because they constrained themselves to components only (see my Inline Svelte components for tests sveltejs/svelte#14791 (comment), alse see API for vitest-browser-svelte and vitest-browser-vue) and I like us not to run into that constrained. I see it similar to the render() function we use in tests and would even expect the same behavior.

  2. There is args but no backing class? That means a template-only-component? Is that by-design?

  3. I wonder why there is an into parameter and later a parentElement() on the result type? If into becomes the parentElement then please name it like that (or is it the render replacing the into). Is the parentElement() on the result then needed? We can assume that is a given?

  4. There is plenty API that adressess templates, but this is also very much fragmented. Can we all the API that is about templates in @ember/template? In the end it doesn't really matter if these are compilers or renderers or parsers or whatever. In the end everything that you need to operate on templates is coming from one package.

@NullVoxPopuli
Copy link
Contributor Author

Why is it renderComponent() ... when it renders a template?
There is plenty API that adressess templates, but this is also very much fragmented. Can we all the API that is about templates in @ember/template

template() only returns a component 🎉
when not used with a class, it returns a template-only component.

I see it similar to the render() function we use in tests and would even expect the same behavior.

aye! the render() utility from @ember/test-helpers is quite involved, and after this RFC we could change its implementation to use renderComponent to

  • simplify implementation of @ember/test-helpers / ember-qunit
  • encourage exploration of other render-utilities, such as what would be needed to make browser-mode vitest work

There is args but no backing class? That means a template-only-component? Is that by-design?

what do you mean no backing class?

This is covered in RFC#931

import { template } from "@ember/template-compiler";

class Example extends Component {
  static {
    template(
      "Hello {{message}}",
      {
        component: this,
        scope: () => ({ message }),
      },
    );
  }
}

s the parentElement() on the result then needed? We can assume that is a given?

I only included it because the sample implementation from @wycats included it -- I'm more than happy to remove it, as it does feel redundant, since you need a handle to that same element (it is the into) to render anything anyway.

/**
* Renders a component into an element, given a valid component definition.
*/
export function renderComponent(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if it make sense to give function name just "render", because we can't render anything except component (or template-only component) in ember (app has it's own render logic)


#### `into`

The element to render the compnoent into.
Copy link

@lifeart lifeart May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we have body as default target node?

Copy link

@gossi gossi May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rename it to parentElement to make it match what the DOM has already defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we removed parentElement 🙈

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we have body as default target node?

idk, any downsides?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you removed the getter from the result. I see into is ambivalent. It can mean, that this is a parent or replaced with the contents you provide. parentElement is a concept, that is already known.

const render = modifier((element) => {
let result = renderComponent(Demo, {
owner,
env: { document: document, isInteractive: true },
Copy link

@lifeart lifeart May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for non-interactive envs we may have different function like renderSSR, so, wondering if we really need isInteractive flag, any use-cases for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great question -- I'd need to dig in to glimmer-vm to see what all isInteractive is used for.

I don't know how much benefit there is to having renderComponent do multiple things via this flag vs your suggestion of renderSSR (which I like)

* Destroys the render tree and removes all rendered content from the element rendered into.
*/
destroy(): void

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think render result should contain component instance itself, to be able to update it's properties if needed (and to be able to debug it)

@ef4 ef4 added S-Exploring In the Exploring RFC Stage and removed S-Proposed In the Proposed Stage labels May 2, 2025
/**
* Optional owner. Defaults to `{}`, can be any object, but will need to implement the [Owner](https://api.emberjs.com/ember/release/classes/Owner) API for components within this render tree to access services.
*/
owner?: object;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but will need to implement the Owner API

So should this be owner?: Owner; then? And how can it default to {}, which obvisouly does not implement Owner?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the owner doesn't need any specific methods implemented if no dependency injection is used.

in this use case for rendering, the owner is more about wiring up lifetimes, than it is adding resolving mechanisms.


## How we teach this

`renderComponent` is meant as an integration-enabling API and would not be added to the guides, or need any documentation beyond what would live in source on the function itself.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: add some examples

  • REPL
  • astro
  • vitepress
  • etc, etc

/**
* Defaults to globalThis.document.
*/
document?: SimpleDocument | Document;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: investigate what all is used on the document -- can the interface be more minimal?

There is an existing implementation in [PR#20781](https://github.com/emberjs/ember.js/pull/20781/)

Here is where this RFC differs:
- no hasDOM (we always have a DOM, as we have not done any design on [Swappable Renderers](https://github.com/emberjs/ember.js/issues/20648))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: add this back, but default to true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain that removing this involves legacy SSR, and we'd probably want to explore how modern SSR works


The args to pass to the component

### Behaviors
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create a renderer if one doesn't already exist.
otherwise use the host app's existing renderer (and revalidation stuff, etc)

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

Successfully merging this pull request may close these issues.

5 participants