I try to get used to octane and so far I’m quite impressed. But I’m facing a problem where I even don’t know if I’m searching in the right direction - so maybe the topic is misleading.
I have a property I need to get from store depending on two arguments. If the property isn’t found I need to create a new one.
Up to now I have this code:
import Component from '@glimmer/component';
export default class MyClass extends Component {
@inject store;
@tracked property;
async getOrCreateProperty() {
let p = null;
try {
p = await this.store.findRecord('property-model, ...);
} catch {
p = this.store.createRecord('property-model', {
argument1: this.args.argument1,
argument2: this.args.argument2
});
}
this.property = p;
return p;
}
constructor(owner, args) {
super(owner, args);
this.getOrCreatePropertyUser();
}
}
This works so far. But what should I do, if the two arguments change? With the classic components I could just implement didUpdateAttrs. But combining async and get doesn’t work and returning a promise in a getter doesn’t work either.
There are two basic approaches I would take here. The first is the simpler; the second the one I actually recommend.
You can use ember-render-modifiers to get the same basic hooks as you had in classic components via modifiers:
{{! the component definition }}
<div
{{did-insert this.getOrCreateProperty}}
{{did-update this.getOrCreateProperty}}
>
{{! other stuff }}
</div>
You can restructure your data flow a bit. Usually when I see a situation like the one you’ve described, I try to think through how this could be restructured in terms of data that gets passed into the component, rather than the component having both argument-driven state and internal state driven by services (including the store). One option for now might be to extract this conditional logic into a class-based helper which you call at the invocation site:
Then the helper would be async. The key to making that work well would be having the component be able to deal with the async argument. I tend to want to go one step further and separate that concern as well, using a data-loading helper which is responsible for rendering out the state of a given Promise. If you did that, you might have something like this:
{{#let (load
(loadOrCreate
type=this.targetProp
id=this.targetId
props=(hash argument1=this.argument1 argument2=this.argument2)
)
) as |result|}}
{{#if result.isLoaded}}
<MyClass @foo={{result.value}} />
{{else if result.isLoading}}
Loading...
{{else if result.isError}}
Whoops! Something went wrong!
{{/if}}
{{/let}}
While that’s a bit more involved at first blush than just doing effectful operations via the old didReceiveAttrs hook, it has a couple nice benefits:
Every piece of it is totally reusable. The {{load}} helper (which I can publish a version of to a GitHub Gist if you like!) can be used for anyPromise-based load operation. The {{loadOrCreate}} helper likewise can work on anything like this.
The component doesn’t have to know anything about the store or even about asynchrony. It just gets to go back to being a function of its arguments. Or, you could also make it take the result as its argument and handle the asynchrony, but either way you have a nice bit of control as a result, and it gets way easier to test.
Your first approach works and looks simple, but I’m also not sure if I like it. The way I would use these modifiers reminds me of an observer, except now there is some logic is in the template - so it’s even worse. But hey it just works
I’m not sure I understand your second approach. The sole purpose of this component is to provide the CRUD operations for this model. So there is also a save and a delete method and some stuff for error handling. Would you put these methods in the helper class too? If you have a finished version of your load helper, it would be really nice if you could publish it somewhere.