I am struggling to figure out how to implement data down, actions up in a glimmer component hierarchy (using Ember Octane, v3.15).
I have a parent component with a list of items. When the user clicks on a button within the Parent component, I want to populate an Editor component with the data from the relevant item; when the user clicks "Save" within the Editor component, populate the changes back to the parent. Here's what happens instead:

How can I make the text box be populated with "Hello", and have changes persisted back to the list above when I click "Save"?
{{!-- app/components/parent.hbs --}}
<ul>
{{#each this.models as |model|}}
<li>{{model.text}} <button {{on 'click' (fn this.edit model)}}>Edit</button></li>
{{/each}}
</ul>
<Editor @currentModel={{this.currentModel}} @save={{this.save}} />
// app/components/parent.js
import Component from '@glimmer/component';
export default class ParentComponent extends Component {
@tracked models = [
{ id: 1, text: 'Hello'},
{ id: 2, text: 'World'}
]
@tracked currentModel = null;
@action
edit(model) {
this.currentModel = model;
}
@action
save(model) {
// persist data
this.models = models.map( (m) => m.id == model.id ? model : m )
}
}
{{!-- app/components/editor.hbs --}}
{{#if @currentModel}}
<small>Editing ID: {{this.id}}</small>
{{/if}}
<Input @value={{this.text}} />
<button {{on 'click' this.save}}>Save</button>
// app/components/editor.hbs
import Component from '@glimmer/component';
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
export default class EditorComponent extends Component {
@tracked text;
@tracked id;
constructor() {
super(...arguments)
if (this.args.currentModel) {
this.text = this.args.currentModel.text;
this.id = this.args.currentModel.id;
}
}
@action
save() {
// persist the updated model back to the parent
this.args.save({ id: this.id, text: this.text })
}
}
I decided to implement Editor as a stateful component, because that seemed like the most idiomatic way to get form data out of the <Input /> component. I set the initial state using args. Since this.currentModel is @tracked in ParentComponent and I would expect re-assignment of that property to update the @currentModel argument passed to Editor.
Indeed that seems to be the case, since clicking "Edit" next to one of the items in ParentComponent makes <small>Editing ID: {{this.id}}</small> appear. However, neither the value of the <Input /> element nor the id are populated.
I understand that this.text and this.id are not being updated because the constructor of EditorComponent is not being re-run when currentModel changes in the parent... but I'm stuck on what to do instead.
As I was trying to figure this out, I came across this example (code), which has pretty much the same interaction between BlogAuthorComponent (hbs) and BlogAuthorEditComponent (hbs, js). Their solution, as applied to my problem, would be to write EditorComponent like this:
{{!-- app/components/editor.hbs --}}
{{#if this.isEditing}}
<small>Editing ID: {{@currentModel.id}}</small>
<Input @value={{@currentModel.text}} />
<button {{on 'click' this.save}}>Save</button>
{{/if}}
// app/components/editor.hbs
import Component from '@glimmer/component';
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
export default class EditorComponent extends Component {
get isEditing() {
return !!this.args.currentModel
}
@action
save() {
// persist the updated model back to the parent
this.args.save({ id: this.id, text: this.text })
}
}

It works! But I don't like this solution, for a few reasons:
arg seems... spooky... I'm honestly not sure why it works at all (since while ParentComponent#models is @tracked, I wouldn't expect properties of POJOs within that array to be followed...)ParentComponent as you type which, while neat, isn't what I want---I want the changes to be persisted only when the user clicks "Save" (which in this case does nothing)<Input @value...I also came across this question, but it seems to refer to an old version of glimmer.
Thank you for reading this far---I would appreciate any advice!
To track changes to currentModel in your editor component and set a default value, use the get accessor:
get model() {
return this.args.currentModel || { text: '', id: null };
}
And in your template do:
{{#if this.model.id}}
<small>
Editing ID:
{{this.model.id}}
</small>
{{/if}}
<Input @value={{this.model.text}} />
<button type="button" {{on "click" this.save}}>
Save
</button>
Be aware though that this will mutate currentModel in your parent component, which I guess is not what you want. To circumvent this, create a new object from the properties of the model you're editing.
Solution:
// editor/component.js
export default class EditorComponent extends Component {
get model() {
return this.args.currentModel;
}
@action
save() {
this.args.save(this.model);
}
}
In your parent component, create a new object from the passed model. Also, remember to reset currentModel in the save action. Now you can just check whether id is null or not in your parent component's save action, and if it is, just implement your save logic:
// parent/component.js
@tracked currentModel = {};
@action
edit(model) {
// create a new object
this.currentModel = { ...model };
}
@action
save(model) {
if (model.id) {
this.models = this.models.map((m) => (m.id == model.id ? model : m));
} else {
// save logic
}
this.currentModel = {};
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With