emberjs / rfcs

RFCs for changes to Ember
https://rfcs.emberjs.com/
790 stars 408 forks source link

Feature Request: consistent API between classic and Glimmer components #563

Closed boris-petrov closed 2 years ago

boris-petrov commented 4 years ago

... for creation and rendering of such templates.

This is provoked from this Discord discussion.

The main point is as follows. I have the following code currently (Ember 3.15):

import Component from '@ember/component';
import { createTemplateFactory } from '@ember/template-factory';

...

const component = Component.extend({ tagName: '', layout: createTemplateFactory(precompiledTemplate) });
getOwner(this).lookup('application:main').register(componentName, component);

...

const component = getOwner(this).factoryFor(componentName).create();
component.on('didRender', function() {
  const html = this.element.innerHTML;
  ...
});
component.appendTo('...');

It dynamically creates a classic component, registers it, instantiates it and the instance is rendered on the page to extract some info from it.

In order to achieve the same result with a Glimmer component, the first part of the code could be translated like so:

import { setComponentTemplate } from '@ember/component';
import { createTemplateFactory } from '@ember/template-factory';
import GlimmerComponent from '@glimmer/component';

const CustomComponent = class extends GlimmerComponent {};
setComponentTemplate(createTemplateFactory(precompiledTemplate), CustomComponent);
getOwner(this).lookup('application:main').register(componentName, CustomComponent);

However the other parts have no Glimmer counterparts. I would have expected at least getOwner(this).factoryFor(componentName).create() to work fine but it blows up with Failed to create an instance of 'component:some-component'. Most likely an improperly defined class or an invalid module export..

Having a unified JavaScript API for component creation and rendering for any kind of component (classic, Glimmer, template-only Glimmer) would be nice. Not sure if it makes sense or is something Ember wants to invest in, but I just wanted to put this here so a discussion can be started.

CC @Gaurav0

NullVoxPopuli commented 4 years ago

personally I don't think glimmer componets should allow this.

This could be the sort of thing LifeCyclecomponent solves

Glimmer is intended to be the lightweight, covers the 95% of use cases

I wouldn't want added callbacks and LifeCycle hooks forced on everyone

https://github.com/NullVoxPopuli/ember-lifecycle-component

Gaurav0 commented 4 years ago

Here's some context:

We allow users to write their own HBS templates which are used in different places. One such place is the browser's title for example. There, obviously, only text is accepted. So I need a mechanism to get from an HBS template to a string of text (of course by passing some context data to the HBS template). Right now I do that by creating a classic component, instantiating it, rendering it and getting the text (all in JavaScript). I want to do the same with a Glimmer component for performance reasons. Not possible right now I think.

Gaurav0 commented 4 years ago

@NullVoxPopuli He doesn't need callbacks or lifecycle hooks.

Gaurav0 commented 4 years ago

dev-rfc discussion

pzuraq commented 4 years ago

I'd like to dig into the use cases here so we can get more details, but it sounded like most of the use cases could be solved if {{#in-element}} worked on elements that were not attached to the DOM. I'm not sure if it does, but I think it would, and this would allow the user to render whatever they wanted into a fragment without ever actually attaching it to the document, and then do what they wanted with the output.

rwjblue commented 4 years ago

I agree with @pzuraq, in-element is the right fit here. Also, just to clarify, in-element does work properly even when the target element is not in the DOM.

ivp-dev commented 3 years ago

If talks about use cases, for example, I have wrote an addon with itemscontrol (like in wpf), where component (item container) should be created in code, in response to itemcollection changing. Now I have to create model of component and render it in template. I would like to create and initialize component in code and manage component lifecycle by myself.

pzuraq commented 3 years ago

I think after learning more about the Glimmer rendering cycle while implementing strict mode, I'm more confident that this is something we do not generally want to support or allow to be a common pattern. There is a lot of overhead that comes with multiple rendering roots, both actual overhead of booting up another VM and running it, and conceptual overhead of managing a render root manually and not using templates.

So if we do want to add this capability, we need to not only show that it could be useful to some use cases, but that it would be essential to those use cases. That is, there would be no other reasonable way to accomplish these use cases that to have the ability to render multiple roots in a single application, manually.

@ivp-dev I understand how your example works, but can you provide more of explanation about why you architected it the way you did? Why, for instance, could you not have used contextual components and the {{component}} keyword instead? Why is programmatic creation essential here?

ivp-dev commented 3 years ago

Thank you for the answer. ItemsControl the base class for all components which should take care about multiple child elements. We can pass items to the component as a collection and render it internally in template

<TabControl @itemsSource="{{array}}"/>

or declare and pass it as a content and render with yield.

<TabControl as |Tabs|>
 <Tabs.Tab @header="Head">Some content</Tabs.Tab>
</TabControl >

I want to support both of these cases. In the first case I can wrap item in Model class and add some functionality (for example extend it with some properties, such as isSelected property or events) and render it with {{component}}. In the second case the component itself is the data passed as a child and I need to add it to the internal collection to support some features, for example to support selection or filtering. I solved it through the render-modifier. When the child component rendered I pass class instance of the child component to the internal collection of the itemscontrol and now I have an access to properties and can support selection or filtering. The problem that I would solve is that I need to support two different classes with the same features. Model class for itemsSource and Component class. Component could be used as data item. Model class will no need if I could create component manually.

wagenet commented 2 years ago

I'm closing this due to inactivity. This doesn't mean that the idea presented here is invalid, but that, unfortunately, nobody has taken the effort to spearhead it and bring it to completion. Please feel free to advocate for it if you believe that this is still worth pursuing. Thanks!

NullVoxPopuli commented 2 years ago

I think this RFC solves the needs here? https://github.com/emberjs/rfcs/pull/813 ?