Introduction
A while ago I came across the following tweet
After reading it, I had a few flashbacks to discussions in the community about routable components taking the place of controllers. That transition never happened, and controllers are still around.
Given controllers are long-lived entities in the framework, they are a source of a lot of bugs while writing ember apps. The classic bug is to forget to reset certain state in a controller, so when the user re visits the route, the state of the controller is not automatically reset.
After reading this response:
I decided to take a stab at migrating a controller on a real app and write about the process, so I could actually see how it would look in real life and also share knowledge with the community.
Real world example
At Mimiquate, we have developed an open source app called Timo, that aims to find acceptable time slots to have meetings for remote teams with team members all round the world. If you are interested, you can check the article I wrote on it's release. I decided to migrate Timo's largest controller, and write about the process while doing it.
Here is a link to the commit where the change is implemented if you want to go straight to the point.
Here are a few details to go over, the template of the route is a lot simpler now, which was expected.
All of its content was moved to the new top-level component.
Regarding the component file, most of the changes are straight forward: basically stop relying on the model property, and used the passed arguments instead. I also had to make sure I imported the store and router services, given those are not automatically available within components. This resulted in a few small changes, for example, updating transitions to other routes.
Small hiccup
I would have thought this was the end of it, but it wasn't. When doing this migration you would assume the state of the component would always be refreshed, given that we we have switched from a controller to a component, but that wasn't the case.
On Timo, most of the action happens on the team route, and the user usually moves from one team to the other. This means the component is not destroyed, and only its arguments are updated. As a result, the state of the component never resets when navigating from team to team.
With controllers, the solution would be simple, we would just reset the state of the controller on the resetController
hook of our route and that would be it.
If we were using Ember Classic components, we could use the didUpdateAttrs
hook to reset these variables, since the parameters only update when the route is refreshed, but we don't have that possibility in Octane.
Enter ember-modifiers
The simplest solution I could find to this problem was to use ember-modifiers.
ember-modifier / ember-modifier
A library for writing Ember modifiers
ember-modifier
This addon provides an API for authoring element modifiers in Ember. It mirrors Ember's helper API, with variations for writing both simple function-based modifiers and more complicated class-based modifiers.
NOTE: this is the README for the v4 release. For the v3 README, see here.
Compatibility
- Ember.js v3.24 or above
- Embroider or ember-auto-import v2.0.0 or above (this is v2 addon)
TypeScript
This project follows the current draft of the Semantic Versioning for TypeScript Types proposal.
- Currently supported TypeScript versions: v5.0 - v5.5
- Compiler support policy:…
The addon comes with helpers we are familiar with, in this case, I used the did-update
helper, as shown below.
I added a container div with the did-update
modifier, which is called every time the current team is updated. When that happens, the resetCurrentTime
function is executed, and the state is reset.
<div {{did-update this.resetCurrentTime this @team.id this}}>
Conclusion
Migrating to top-level components is not hard at all, but there are a few issues that can be found after testing your app in different circumstances.
There are times when we will have to deal with Controllers like it or not. One of these situations, is when dealing with query params. Right now, the only way of dealing with them is via queryParams
interface on controllers, which has its fair share of quirkiness.
Regarding issues related to glimmer components, most solutions can be found here.
As always, Ember's documentation goes above and beyond.
In summary, not using controllers is definitely possible, but it’s not a solution that the framework fully supports, and that always has a cost. Although some controllers can be fully moved to components, others can’t, which means there is no uniform way of transforming all the code base this way without compromises.
Special thanks to
For helping me discuss some of the issues I faced while implementing these changes.
Top comments (3)
I think did-update could go way if using the localCopy decorator from tracked+toolbox? Maybe?
Allows setting, will reset itself when source data changes
github.com/pzuraq/tracked-toolbox#...
You could also use ember-render-helpers to avoid the unnecessary
<div>
and put them right atop the template file to see the component lifecycle.Thank you, I had to go through a similar process recently in order to update an app to use Embroider build pipeline.