DEV Community

Cover image for Migrating Controllers to top-level Components in Ember
Juan Manuel Azambuja
Juan Manuel Azambuja

Posted on

Migrating Controllers to top-level Components in Ember

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.

GitHub logo 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 simple functional modifiers and for writing more complicated class modifiers.

This addon is the next iteration of both ember-class-based-modifier and ember-functional-modifiers. Some breaking changes to the APIs have been made For a list of differences, see the API differences section.

Huge thanks to @sukima and @spencer516 for their contributions! This project is based on their work, and wouldn't have been possible without them.

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}}>
Enter fullscreen mode Exit fullscreen mode

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

nullvoxpopuli image

For helping me discuss some of the issues I faced while implementing these changes.

Top comments (3)

Collapse
 
nullvoxpopuli profile image
NullVoxPopuli

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#...

Collapse
 
gossi profile image
Thomas Gossmann

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.

Collapse
 
maxymczech profile image
Maksym Shcherban

Thank you, I had to go through a similar process recently in order to update an app to use Embroider build pipeline.