Let me start with the easiest part first:
I mean, how do I write code that works, on Ember 3.12 , where I’m doing some of the migration and where I need to use this.set
This you can solve pretty easily by updating every use of this.set
to use the standalone function version of set
:
import Component from '@ember/component';
+ import { set } from '@ember/object';
export default Component.extend({
doSomething() {
- this.set('key', 'value')
+ set(this, 'key', 'value');
}
});
Once you’ve made that transformation (and the same for get
), then you don’t need to worry about whether the object in question is descended from EmberObject
or not. I don’t know if there’s a codemod out there that can do this, but even if not you can actually do it safely with a relatively simple regex find and replace.
Second, as regards the specific kind of refactoring you’re doing: this is going to be a bit of work, there’s no way around it. It’d be nice if we could just code-mod this kind of thing, but unfortunately, mixins are so dynamic that every use case would look different.
I’d recommend starting by identifying the intent of the shared code and breaking it apart as much as possible. For example: repeating the classNameBindings
is worth just copying into each usage—not least because when you migrate to Glimmer components, you won’t be using classNameBindings
anyway, but will be doing all of that in the template! The same goes for attributeBindings
.
Anything that isn’t stateful, you should just pull out into standalone functions. And a lot of times things that are stateful can be turned into stateless functions. For example, if you are wrapping around a this.store.query(...)
because you want to have the shared configuration for it, you can pull the this.store.query(...)
invocation back out to the components’ actions, but use a function to build the configuration for it. That standalone function becomes very easy to test: no need for the store, or mocking API endpoints, or anything, just “Did I get out the configuration I should have given the arguments I passed in?” Then the components can just do something like:
import Component from '@ember/component';
+ import { inject as service } from '@ember/service';
+ import { buildConfig } from 'app/lib/my-config-builder';
- export default Component.extend(TheMixin, {
+ export default Component.extend({
+ store: service(),
+
actions: {
doSomething() {
- this.mixinStoreMethod();
+ this.store.query(buildConfig(this.someValue));
}
},
});
(If the action itself is shared you’d also need to add it to the components, but you get the idea.)
In other cases, you may also be able to begin by using a delegate and passing this
into its methods, and then iterating from there. The best way to shift to using a delegate, though, would be to pass an object derived from this
instead. That way you can clearly and explicitly define what’s actually required by the delegate, rather than just coupling yourself to the innards of this
permanently. That in turn gives you more tools for refactoring, because you can clearly see what you might want to whittle down or break into separate pieces later.
One last note: it helps me to remember when tackling things like this that I also don’t have to accomplish the whole refactor at once—and in fact probably shouldn’t! Instead, isolate one piece at a time, identify the right solution for that, test that one small change well, and ship it. Then repeat! Eventually you’re done, but each step was small, self-contained, and manageable.