-
-
Notifications
You must be signed in to change notification settings - Fork 408
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
Add setRouteComponent API #731
Conversation
For the time being, this API will not be the recommended way of writing Ember | ||
apps. As such, it will not be included in the guides. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for calling this out
No changes will be made to the route or controller lifecycle, hooks, or | ||
behaviors in general, since this is not related to the goal of unlocking | ||
experimentation with strict mode and route templates, or the goal of | ||
experimenting with controller-less applications. Changes in these areas will be | ||
done in future RFCs instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One unanswered question I thought of here: how does this interact with the existing abilities to muck with specific rendering patterns on routes, render
and renderTemplate
? Is using those with this a hard error, a deprecation, a warning, etc.? And what's the actual behavior—is renderTemplate
ignored if using setRouteComponent
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, we should address that. I believe these APIs would be ignored, since we are fully ignoring the route's template in general. Since these APIs are deprecated, I think this also won't introduce any inconsistencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had one question, but my overall sentiment is: ❗
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
text/0731-set-route-component.md
Outdated
`@ember/routing/route`. | ||
|
||
```js | ||
declare function setRouteComponent(component: Component, route: Route): void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some patterns I have seen is
articles
list
detail
and articles.hbs
is just an {{outlet}}
.
How strict is this API to only include a component
? Could handlebars expressions be allowed as the first argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How strict is this API to only include a component? Could handlebars expressions be allowed as the first argument?
Whether or not this fits in to the current proposal, I think this is a really cool idea! I've found it useful in the past to be able to have all a route's assets (templates and controllers) come from one JS file.
This is something I've achieved before by overriding the resolver so that it would look for a template
named export from a route.
Being able to use setRouteComponent
to do something similar would be pretty awesome, even if it required a separate createComponent
function to maintain the semantics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the idea with ember-template-imports
is that an hbs
expression is a template-only component, and using them together you could definitely do this in practice:
import { hbs } from 'ember-template-imports';
import Route, { setRouteComponent } from '@ember/routing/route';
export default class MyRoute extends Route {}
setRouteComponent(hbs`Hello, world!`, MyRoute);
If we find that it still makes sense to associate a template that is not a component with a route in the future, then we can add that functionality either via a separate API, or via this API. My hope is that we can start to do the opposite though, lowering the distinction between templates and components even more in the future. I would like, for instance, for hbs
used in tests to actually be inline-template-only-components, rather than just templates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Ravenstine note that this is a low-level API on which exactly those kinds of things would be built. In the same way that setComponentTemplate
allows us to add ember-template-imports syntax on top of a component definition (rather than using setComponentTemplate
directly, this API is the necessary building block to allow you to do something similar in the future.
Worth note, though, that it wouldn't be exactly like that, because the backing class for a component is not a Route
. You'd always want to be attaching a component definition, whether that's a template-only component or a class-backed component. With template imports and this API, though, you can easily imagine being able to do something like this, in a hypothetical future where we did add the decorator approach you suggested in the main thread:
import Route, { withComponent } from '@ember/routing/route';
import Component from '@glimmer/component';
const MyRouteView =
<template>
<div>{{@model.someProp}}</div>
</template>
@withComponent(MyRouteView)
export default class MyRoute extends Route {
model(params, transition) {
// ...
}
}
(Note that I'm not suggesting I want that specifically; just noting that the point is having the appropriate set of primitives that lest us build and experiment with a variety of things in this space.)
As a closely related point, @snewcomer we wouldn't want this to accept strings there, but with strict mode we can straightforwardly use any tool which produces a strict-mode component, so you can imagine just passing <template>{{outlet}}</template>
there (or the equivalent with hbs
) and it would Just Work™.
Edit: to clarify re: "strings", basically what @pzuraq said. We were apparently typing simultaneously. 😂
How do we feel about supporting decorator syntax? |
The decorators syntax is still not stable, and function withComponent(SomeComponent) {
return (Route) => {
setComponentTemplate(SomeComponent, Route);
return Route;
}
} import Route from '@ember/routing/route';
import { withComponent } from 'somewhere';
import CoolComponent from '../components/cool-component';
@withComponent(CoolComponent)
export default class MyRoute extends Route {
// ...
} (I'm not at all suggesting this is an API we should adopt, just showing that it's trivial with this primitive in place. The point of this RFC is to add the primitive so we can build something nice on top of it after experimenting in the community!) |
Echoing @chriskrycho's concerns, the point of the For instance, In the same way, we really don't want to focus on adding high level API features here. If the function happened to work as a decorator without any additional modifications, then I would be ok with saying that's supported. However, it would require us to actually support two different calling conventions, which is extra complexity, with the only benefit being that it's slightly nicer to use directly, which is a non-goal, so I don't think we should support it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few concerns on first review:
- Why would we only pass
@controller
if you use this API? It seems reasonable to always pass@controller
. For example, it's a really convenient transition path for existing route templates to move allthis.
references to@controller
and then move the template to be a template only component and 💥 you are "in the new world". - The RFC should describe the semantics of lifecycle of the component. Saying that it is the same as invoking a component from the route template is fine, but then we should 1) say that explicitly and 2) provide some "non-normative" summary of the semantics.
- Implementation wise, I'd prefer to make the
@controller
a lazily reified value. That gives us a possible path away from eagerly constructing controllers for routes that do not use query params (eventually).
- Explicitly define component lifecycle - Define laziness of `@controller` arg - Add `{{@controller}}` to existing route templates - Define behavior of routing substates - Return route class from `setRouteComponent`
Updated to address your concerns @rwjblue, along with a few other changes! |
Beautiful! 🎉 |
Will there be an issue with the |
Routes currently can defer to the `loading` and `error` substates based on the | ||
current state of the `model()` hook. These substates are _themselves_ | ||
implemented as routes - you can define a `routes/loading.js` and | ||
`controller/loading.js` to go along with the `app/templates/routes/loading.js` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
app/templates/loading.hbs
instead ?
For historical reasons I understand why // strawman example
import Route, { setRouteComponent, setComponentController } from '@ember/routing/route';
import Controller from '@ember/controller';
class MyRoute extends Route {}
class Ember2009 extends Controller {}
const RouteComponent = hbs`Hello, world!`;
setRouteComponent(RouteComponent, MyRoute);
setComponentController(Ember2009, RouteComponent); // Validates its actually associated with a Route This decouples the associated route even further away from controller references. If controllers ever go away the migration path is:
Finding all |
I really like @chadhietala proposal. The implicit passing of |
We discussed this a bit on the core team call on last week, and I believe the consensus so far was to keep
From a pragmatic perspective, keeping |
I don't believe there will, the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exciting stuff!
- Can we add a note on how
setComponentTemplate
would affect route sub and super classes? - Greenfield apps that use this feature exclusively would still incur the penalty of all the other route/template loading features in their vendor bundle. I realize that it's not a goal to do anything about that in this RFC, but would it be possible to get a sense of what this penalty is for such a greenfield app? In other words, if Ember could shake out all code related to loading route templates, how much smaller would the vendor bundle be? I imagine it would be much more exciting if we could connect this to some tangible user-facing benefit, rather than just better DX.
I just wrote essentially a polyfill for this: https://github.com/luxferresum/experimental-set-route-component
|
Anything we want to change in this RFC I'd like to see this move forward 🙃 Excited to have a way to have private layout / route scoped components |
It looks like there's solid interest in this RFC. Let's see if we can get it moving forwards again. |
I could be misunderstanding, but based on feedback I got on Discord, it seems to me that things are going in a different direction and this isn't needed: https://wycats.github.io/polaris-sketchwork/routing.html |
The Framework team will need to get on the same page to make sure, but I think we need this as part of the path to that most likely. In particular, because we do not want to block the adoption of strict mode templates on having a brand new routing design fully designed and then also implemented and then also adopted! |
Whats the status here? |
It seems to me that it's unlikely that this will be the actual primitive we want long term. The existing polyfill allows for experimentation in this area so I think it may make sense to close this. A more ergonomic solution would be to allow for replacing the route template file with a gjs/gts file. |
Yeah, the problem here is that the goal of a low-level primitive like But in this case, I'm not sure we want to commit to supporting this API. It probably defeats route-based code-splitting, because it requires the entire component graph for a route to be loaded before you can assign that component to the route. I agree we need an immediate-term solution for strict-mode adoption in routes. I just don't think this is the API for that. We need either a low-level API that we can support long-term, or a declarative solution where the implementation can be changed and/or codemodded reliably in the future. For example, I think we could probably ship an addon that lets you convert |
Does it? I have an app built using 100% strict mode components (no app/templates directory at all) and use my own implementation of setRouteComponent() and route splitting seems to work. I see different chunks getting loaded depending on which route I hit. import type Controller from '@ember/controller';
import { guidFor } from '@ember/object/internals';
import type Route from '@ember/routing/route';
import type { ComponentLike } from '@glint/template';
//@ts-expect-error No typedefs
import { precompileTemplate } from '@ember/template-compilation';
import E from 'ember';
import type { TemplateFactory } from 'ember-cli-htmlbars';
const Ember = E as typeof E & { TEMPLATES: Record<string, TemplateFactory> };
type RouteComponent<M> =
| ComponentLike<{
Args: { Named: { model: M; controller: Controller } };
Element: unknown;
}>
| ComponentLike<{ Args: unknown; Element: unknown }>;
declare function precompileTemplate(
template: string,
options: { strict?: boolean; scope?: () => Record<string, unknown> },
): TemplateFactory;
export type RouteClass<M = unknown> = abstract new () => Route<M>;
export function setRouteComponent<M>(
route: RouteClass<M>,
component: RouteComponent<M>,
): void {
setRouteTemplate(route, templateForRouteComponent<M>(component));
}
function templateForRouteComponent<M>(routeComponent: RouteComponent<M>): TemplateFactory {
return precompileTemplate('<routeComponent @model={{@model}} @controller={{this}} />', {
strict: true,
scope: () => ({
routeComponent,
}),
});
}
export function setRouteTemplate(route: RouteClass, template: TemplateFactory): void {
const guid = guidFor(route);
const templateName = `route-${guid}`;
route.prototype.templateName = templateName;
Ember.TEMPLATES[templateName] = template;
}
//
// Route Class Decorators
//
export function withComponent<M>(
component: RouteComponent<M>,
): <R extends RouteClass<M>>(route: R) => R {
return route => {
setRouteComponent(route, component);
return route;
};
}
export function withTemplate(
template: TemplateFactory,
): <R extends RouteClass>(route: R) => R {
return route => {
setRouteTemplate(route, template);
return route;
};
} |
I've done some further testing on this and can't find any issues with route-based code splitting when the routes import the components. The split must be happening above the route level, i.e. the route itself isn't loaded until it is navigated to. Thinking back, I should've known this already because I had to implement a workaround in At minimum, it would be great to have a import { setApplication } from '@ember/test-helpers';
import Application from 'client-dashboard/app';
import config from 'client-dashboard/config/environment';
import { start } from 'ember-qunit';
import * as QUnit from 'qunit';
import { setup } from 'qunit-dom';
setup(QUnit.assert);
setApplication(Application.create(config.APP));
declare const window: Window & {
_embroiderRouteBundles_?: { loaded: boolean; load(): Promise<void> }[];
};
if (window._embroiderRouteBundles_) {
const bundles = window._embroiderRouteBundles_;
Promise.all(bundles.filter(bundle => !bundle.loaded).map(bundle => bundle.load())).then(() =>
start(),
);
} else {
start();
} |
yes, exactly. That’s the behavior I don’t think we want to keep forever. It means you wait two round trips to the server, in series: one to load the Route and then another for the Route to load all the data. It’s probably better to load the data and components in parallel. |
Could it be an option to pass a function that resolves to a promise to
|
Some questions I don't know the answer to, but could probably be tested easily:
If Embroider does do the nice thing here, then the only other thing that I think would be needed is to stall the completion of the afterModel hook until the import() promise has resolved, so that the component is available when the route's template gets rendered. |
Yes, that is one possibility.
The short answer is, yes, in general Embroider lazily loads |
People who have been wanting this feature may want to check out the ember-route-template addon, which while not a formalized part of Ember's public API is built on stable public APIs and addresses the pain point that this low-level RFC was attempting to address. I think this RFC was primarily intended to close that gap, and (intentionally) doesn't try to be a new low-level primitive for a reformed router, which is where we expect our efforts to do, so I'm suggesting we close this one. |
Rendered