-
-
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
An alternative to Controllers #499
An alternative to Controllers #499
Conversation
In current queryParams implementation qp-values binded to model, it's possible to have different states for one page for different models, how we can handle it using new approach? |
the gist is that we'll probably rely on community solutions for caching of query params between routes, maybe like https://github.com/NullVoxPopuli/ember-query-params-service/blob/master/tests/acceptance/navigation-test.ts#L56 |
@NullVoxPopuli my comment about this behavour https://ember-twiddle.com/b9e78f44f0a63a1a5b0f75ca28b9ebef - try type some search and change models. |
|
This is on a good track but needs more detail. Can you expand upon the lookup priority rules? For example, I think if That would be safe as long as we gave you an upgrade step that generated explicit template files for any of your classic routes that were previously relying on I also think we end up in an inconsistent state if we don't also allow [Edited: I had a ton of typos in my path examples that made it hard to follow before.] |
The title doesn't really match the RFC @NullVoxPopuli maybe |
@jrjohnson That's fair, removing controllers would require a deprecation RFC, which this is not |
Should the RFC recommend alternative approaches to storing long lived state? I imagine the rouleable components will not be singleton's - correct? |
@ef4 I've added some details around look priority :)
I've changed my mind. changing the error and loading stuff keeps cohesion :) (but I'm in favor of @jasonmit correct. any component used for routing should be considered ephemeral. Services should be used for long-term state. :) |
FWIW, I feel like the move away from controllers can be aligned with router refactors. In that, once folks move to (potentially) new route semantics, controllers will no longer be used/loaded. Instead, some form of a component will become the route template, such as a template-only component or maybe the route defines which component to render, etc. More thoughts coming as soon as I get to my ember2019 post. |
bingo |
I imagined the store(s) would be backed by a service but will there be a recommended approach regarding strategy or common API that can be shared? Or is this an opportunity for Ember, as a the framework, to step out of the way and lean more on community solutions? |
service injections cover this :) import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
export default class MyComponent extends Component {
@service store;
@service toast;
@task(function*() {
const record = this.store.createRecord('post', { ...whateverAttributes });
try {
yield record.save();
toast.success('yay!');
} catch (e) {
toast.error(e);
}
}) create;
} <button {{on 'click' (fn (perform this.create))}}>
Create a Post
</button> |
I'm aware of the service inject API but feel it doesn't quite layer over what we have now. Currently there's a 1:1 mapping of route backed state vs what sounds like a recommendation for arbitrary service(s) used to hang state from. Uniformity across codebases breaks down here and may be worth documenting recommended paths in the migration docs. Perhaps something for the Ember learning team. |
hmm, maybe I misunderstand? can you provide some concrete examples of how you use controllers today, that components wouldn't be able to handle?
can you expand on this? |
I understand that it's more a concern that it's a move from a highly conventional approach (FooRoute => FooController), to a more ad-hoc approach (FooRoute => ???Service/s). Can someone explain why routes themselves can't be singletons and store their own long-lived state? Like a service that just automagically has hooks invoked on route changes? |
Any state that isn't queryparam state. One example could be an email client that keeps draft state for emails in memory over the lifetime of the app instance. Today, you might throw it in a controller or a service. If you're in a large codebase then there may be many instances like this and you'd end up with really small, oddly shaped, services when migrating an app over to a controller-less world. So the question I've been failing to pose (sorry, limited by typing on my phone) is are we recommending moving everyone to services for this state or is there a common API that can be shared for mapping route backed state without one off services where a dedicated services may be a bit too heavy. |
I'm OK with explicit import of components and I agree with that general direction, but I do want to point out that it doesn't give us any static analyzability that we don't already have, so long as people are sticking to the conventional names for everything (which they'll now be forced to do since Embroider can already statically determine which Route, Controller, and template files go with each route. Having a convention means it's not ambiguous. So design explicit imports if that's the developer experience we want. But don't design them that way assuming it's needed for static analysis, because it's not. |
@ef4 I should clarify that my own emphasis on static analyzability is not for the build pipeline, where I recognize that, as you note, "convention means it's not ambiguous", but for all the other tooling. There, the explicitness of imports is a huge win, as it ends up being a thing we don't have to build ourselves. The fewer things I have to teach a language server to understand/etc. and the more I can just rely on tools' existing support, the better. |
@NullVoxPopuli, the final unanswered question is answered unambiguously in the guides:
|
I love the direction this is going! I do like a lot the ability to import a component to the route to render. It does address some of my concerns from my #EmberJS2019 blog post. For the loading and error states, I believe it would be useful to pass an argument to these components to clarify what context they have been rendered. For example, if I would like to render the same component for all the three states, it would be difficult to know if it is loading or not. If your model returns a promise, you would need to get a helper for seeing if it is pending or not. Maybe Ember could pass a flag to these components, something equivalent to this: |
Even better would be to pass a tagged value, which is nicer for JS users, and much nicer from the perspective of TS users. Given these backing definitions (in TS, but they could equally just be "what we do" as long as it's well-defined): enum State {
Loading = "loading",
Resolved = "resolved",
Error = "error"
}
type Model<T, E> =
| { state: State.Loading }
| { state: State.Resolved, data: T }
| { state: State.Error, error: E }; Assuming that whole item is passed as the model to the component, equivalent to this: <SomeComponent @model={{model}} /> From the JS perspective, you'd be able to check it like this: import Component from '@glimmer/component';
class SomeComponent extends Component {
constructor(owner, args) {
super(owner, args);
switch (args.model.state) {
case 'resolved':
// normal handling...
break;
case 'loading':
// loading handling...
break;
case 'error':
// error handling...
break;
default:
// can't actually get here
break;
}
}
} If we designed it to "spread" the arguments instead, as if this were the invocation: <SomeComponent @state={{model.state}} @data={{model.data}} /> Then on the JS side, it'd be the same, except with one fewer layers of access: just The upshot for TS is that you could write something like this (assuming the original invocation style): import Component from '@glimmer/component';
class SomeComponent extends Component<Model<String, Error>> {
constructor(owner, args) {
super(owner, args);
switch (args.model.state) {
case State.Resolved:
// We can't see the `error` value here; it's a type error to access it.
transformData(args.model.data);
break;
case State.Loading:
// We can't see the `error` or `data` values here; it's a type error to
// access either.
break;
case State.Error:
// We can't see the `data` value here; it's a type error to access it.
transformErrorForDisplay(args.model.error);
break;
default:
// special helper that throws a runtime error, but more importantly
// *type-checks for exhaustiveness*.
assertNever(args.model.state);
break;
}
}
} You can do similar with the booleans, but in general this pattern is much more robust and reliable in my experience—even when you're just using JS, because you can never accidentally end up with |
I just added some examples and incorporated @chriskrycho's idea with the route state type args and such :) <3 |
Love where this is at! When new routes are generated, I imagine we want to use this format. It would probably make sense to generate a component with the same name. Does that make sense? If so, should that be included under How We Teach This? When a route is generated, what should be the default value for the loading and error components? Should an empty component for Loading and Error be included in new apps and the default values set to those? |
Also, does this really depend on #496 and #454? It seems like this is complementary, not dependent. For that matter, the changes this RFC proposes have routes render components rather than controller-backed templates. I don’t think it should really depend on #380, as those concerns aren’t dependent. With this and #380 implemented, we no longer need controllers, but they can be worked on in parallel, not depending on each other. |
Are we missing anything here around the Maybe then in a 4.0 release they switch behavior to use components in their "implicit" form? Or maybe they will need to be explicit? I think these concerns are for a future RFC, but I just want to make sure we aren't missing anything for this RFC. |
That means for the route
200% agree. I used to create a wrapper component to avoid using controllers and routers -when possible- for the application's business logic. I can't forward to using this RFC proposal 👯♀️ As @michaelrkn mentioned, the #380 dependency of the RFC may not be required for implementing it. However, is the proposal here really relevant without #380 ? Query parameters are the cornerstone of controllers 😅 |
I would suggest explicitly, in line with how much of Ember is moving now.
I think they are independent. You could have #380 and still have a route render a template, no component. And you could have this RFC, and have a route render a component, no query params. The two together cover all use cases, but neither blocks the other. |
Fix typo Loading to ErrorComponent
They are technically independent, but as independent RFCs, they don't tell the story I want to tell. :-\ |
I had another thought today, although I don't know if it's any good. One possible disadvantage to the approach we have here and in #510 is that there's still some hidden/implicit behavior. Here in #499, we have some static properties that define what loading, error, and render components to use, and we just have to know the framework calls them in order from somewhere. In #510, we have an object that's returned from a hook that we have to know the framework will pass into the component as arguments. What if we took a much more explicit approach, like: import hash from 'rsvp';
import ProfileComponent from './components/Profile';
import LoadingComponent from './components/Loading';
import ErrorComponent from './components/Error';
export default class ProfileRoute extends Route {
@service store;
onEnter({ profile_id }) {
renderComponent(LoadingComponent);
hash({
profile: this.store.findRecord('profile', profile_id),
comments: this.store.query('comments', { profile_id })
}).then((data) => {
renderComponent(ProfileComponent, data);
}).catch((error) => {
renderComponent(ErrorComponent, error);
});
}
} Then it would be very obvious what gets called, at what point, and what data is getting passed in. On the other hand, maybe this is overly verbose, and maybe it gives too much freedom for people to stray away from the happy path. Just a thought! |
Does Route need hooks? If so, it would be great to explicitly implement them: import { OnRender, OnLoading, OnError } from '@ember/routing/route';
export default class ProfileRoute extends Route implements OnRender, OnLoading, OnError {} But this is currently impossible because Ember does not rely on TS, right? It looks like Angular, though. @michaelrkn What about getting, also, a function instead of a static field? In the case, we want to handle states differently: import FooComponent, { LoadingComponent, ErrorComponent } from './components/Foo';
import Route from '@ember/routing/route';
export default class extends Route {
@service router;
@service remoteLogger;
static render = FooComponent;
static loading = LoadingComponent;
error(error) {
if(error instanceof ErrorAPI) {
this.remoteLogger.log(error, new Date().getTime());
}
return ErrorComponent; // how to pass `error` to that component?
}
} |
I Updated the description with the status of this RFC -- which links to something @pzuraq spit balled in Discord ;) |
Closing in favor of: Thanks for moving the ball with |
rendered
depends on:
RFC 496: Handlebars Strict ModeRFC 380: Add queryParams to the router serviceand RFC 715: Arbitrary Query Params
Update (2020-02-08):
RFC 570: URL Manager replaced Add queryParams to the router service #380 (and is required, because without controllers, we need a way to handle query params)Update (2020-03-03):