Hi,
I build projects on Aurelia for more than couple of years now, but I feel I am still missing some recommended design patterns fundamentals, having troubles with some approaches I regularly use. Here is one of the examples - I would be very grateful if someone can point me in the right direction to resolve the issue:
Let’s say I have a Toolbar control, build as a custom element with some bindable properties, one of which is specs, expecting toolbar items definition. In my screen VM, I can have the following:
buttons = [
{
type: 'button',
name: 'Do something',
visible: true,
enabled: false,
clicked: () => this.doSomething()
},
{
type: 'dropdown',
name: 'Simple Menu',
items: [
{
type: 'button',
name: 'Do something else',
visible: true,
enabled: true,
clicked: () => this.doSomethingElse()
},
{
type: 'button',
name: 'Do nothing',
visible: true,
enabled: true,
clicked: null
}
]
}
]
And in my View, I have:
<toolbar specs.bind="buttons"></toolbar>
In the control itself, I use repeat.for directly on specs property to render buttons and menus. Everything works nicely, but obviously I as the application lives, I need to change the toolbar content, disable or hide some buttons or change menu items based on data from backend.
If I would just change the values inside the buttons array, everything would work perfectly:
onDataChange() {
this.buttons[0].enabled = true;
this.buttons[1].items[0].visible = false;
}
But I don’t like to code that way, I like to use computed properties to drive the screen behaviour. So, instead of having buttons as a class variable, I define it as a computed property:
@computedFrom('btnVisible', 'btnEnabled', 'menuItems')
get buttons() {
return [
{
type: 'button',
name: 'Do something',
visible: this.btnVisible,
enabled: () => this.btnEnabled,
clicked: () => this.doSomething()
},
{
type: 'dropdown',
name: 'Simple Menu',
items: () => this.menuItems.map(i => ({
type: 'button',
name: i.name,
visible: true,
enabled: i.enabled,
clicked: () => this.doSomethingElse(i)
}))
}
];
}
Although it works nicely in most situations, the problem is that it causes the whole toolbar to be re-rendered if any of the referenced properties change. Although it is hardly noticeable by an eye, it can cause troubles - menus can disappear if they rebind when they are open, for example.
What I would like is to somehow use the @computedFrom inside the toolbar definition. To specify easily that “Do something” enabled is computed from one property, and “Simple Menu” items are generated from another property. That way, only the parts of the toolbar would be re-rendered as needed.
Every possible solution I can think of seems too complicated or just wrong to me:
-
Instead of binding directly to “spec” property in the toolbar control, bind to its deep copy, and merge the updated “spec” to the copied version, without updating unchanged properties. That would work, but then the control would not react to any change inside the “spec”, so I would be forced to always update the “spec” as a whole.
-
Pass the VM context to the toolbar, and have properties like enabledComputedFrom: string[], visibleComputedFrom: string[] etc as part of the definition. Observe changes on those passed expressions and invoke callbacks to update the values. That would be really ugly :-))
-
Have the “buttons” as a variable in the VM, listen to dependencies using observers or other means, and update the specs insides as necessary, on every screen the toolbar is used. The code would grow very quickly such way. I don’t want to do that.
What I hope for is that as you read this, you just think “what the hell is that guy doing, he really didn’t get it yet”, and point me to the right magic direction. Can you, pretty please?
Thanks a lot!