Better approach to build a toolbar control

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:

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

  2. 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 :-))

  3. 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!

3 Likes

Rather than use a list of plain objects, you could use a list of classes which can have getters with @computedFrom.

class MyButton {
  constructor(private service: DoSomethingService) {  }

  public type: string = 'button';
  public name: string = 'Do something else';

  @computedFrom('service.isEnabled')
  public get enabled(): string {
    return this.service.isEnabled;
  }

  public click() {
    this.service.doSomething();
  }
}

4 Likes

I sort of agree with @timfish’s suggestion. It seems that the items in the toolbar is pretty static, except sometimes, you need to hide or disable individual items. From that perspective, there is no need to bind a new array of items to the toolbar. A simple way might be something like this:

this.button = {
  type: 'button',
  name: 'Do something',
  visible: true,
  enabled: true,
  clicked: () => this.doSomething()
};
// do the same for dropdown
this.toolbarItems = [this.button,  this.dropdown];

You can then bind the toolbarItems with the toolbar custom element. If you flip the this.button.enabled = false;, Aurelia should do the rest for you.

3 Likes

Don’t know if it helps in your case, but I tend to avoid having a component that “do everything”. Like the toolbar.

I recognize when I get there when I need to pass lots of options to one component. I’ve been there for exactly the same thing with top bars, menu, toolbar, …

Now I tend to split down components and use slots. In this case the toolbar works more as a DOM container with a slot. This component is responsible for the right positioning, effects, transition etc of the toolbar. It can also affect the styling of the buttons and handle some high-level things in the toolbar.

Then I have a toolbar-button custom element that handle the display of the button itself with all the settings enabled, visible etc…

Now in your main template it could look something like

<toolbar>
  <toolbar-button repeat.for="button of buttons" button.bind="button" click.delegate="doSomething(button.id)">${button.name}</toolbar>
</toolbar>

I’ve found that most of the time I’m trying to pass many options to a component but really the one responsible for handling these options and even handling the events (such as click) is the main view.

3 Likes

Hi @timfish,

Thank you for your suggestion. But wouldn’t this just move the question of “how to elegantly set isEnabled on multiple buttons” to “how to elegantly set isEnabled on multiple services”? How would you instantiate the services and pass/bind buttons to the control?

Thank you for your time,
David

1 Like

Hi @Sayan751,

Thank you. This make sense, but then I would still need to create some observation for the VM properties isEnabled and other buttons properties depends on, and switch the toggles in code, wouldn’t I? That would definitely work, but it doesn’t have the elegance of @computedFrom. In the real-world application, I can easily have 4-5 buttons in a toolbar, couple of them dropdowns, and need to set the visibility / enabled flags, tooltips, styles etc. on all of them, depending on the data. Wouldn’t it lead to a huge amount of code just for keeping the toolbar state in sync?

Thanks for your time!

1 Like

Hi @ben-girardet,

Yes. This definitely leads to development of nice extensible framework of controls with nice potential to reuse, and solves the rebinding issue nicely. I really like it and thinking about going that way.

The reason why I tried to opt for all-typescript solution instead is because I have tons of screens around the application having toolbars with buttons and sub-menus, which obviously are driven by data. The number of properties I have on toolbar items is significantly larger than in the example: visibility, name, tooltip, icon and other appearance and behavior specific properties. And I absolutely love the type safety and refactoring features TypeScript offers, and how easy and safe it is to add another item to toolbar or change the behavior of the existing one when needed. No need to open HTML template and add items there, no need to add hefty of properties to VM, I just need to find the single “toolbar” property on the VM and work in there in safe environment checked by the compiler.

What I would love is to be able to keep the majority of code in TypeScript, but find an elegant way how to apply @computedFrom or similar in a more granular manner, to avoid rebinding of the whole thing.

Thank you very much for your input.
Kind regards,
David

1 Like

instantiate via dependency injection and pass/bind using Aurelia bindings?

1 Like

Who would guess so :-). Maybe that’s the piece I still don’t get…

Correct me if I am wrong please - For the @computedFrom to work, the component - Button in your example, needs to be instantiated by the dependency injection as well, correct? So I cannot simply “return [new MyButton(doSomethingService1), new MyButton(doSomethingService2)]”, can I?

Either way, in your proposed solution, do I understand correctly that you would create a ButtonControlService (unversal), and you would instantiate it (using dependency injection) for each button with some parameters? Or did you meant it to have the specific service developed for each button instance?

Thank you very much.

EDIT: Possibly that’s the thing. At the moment, I don’t use dependency injection extensively in my apps - I use it mostly only where Aurelia requires me to do so. I have few shared services developed and injected to my screens, but otherwise most of the screen specific logic is writen directly in my screens VM. It is quite possible that is not the “right way” to write the application, and I should extract things to small services in my own code…

1 Like

If you have ToolbarButton custom element it can act as a service. (edit: what I mean is that it can handle the button specific logic)

Back to your question on the Custom Element approach. You fear that you miss the typescript help, but I don’t think you have to miss it. I also rely much on TS for safe typing. In your case I would export an interface (in the same file of the CE) that describe how the buttons settings must look like.

And from your main view, you can keep the exact same

@computedFrom(...)
public get buttons(): Array<ToolbarButtonInterface> {
   // compute your typing-safe buttons settings
   return ....;
}

What’s cool then is that from the toolbar button CE you can also rely on @computedFrom() if needed on button properties. Or depending on your use can use a propertyObserver.

Also, from what you describe you might also benefit from using aurelia-store and define your buttons in the state. Then you could subscribe to state changes as an efficient observing strategy.

2 Likes

@computedFrom will work regardless of how you instantiate the class.

You could use an array of buttons that you create yourself which is closest to what you already have:

[
  new MyButton(doSomethingService1), 
  new MyButton(doSomethingService2)
]

Or you could load them via DI in a couple of other ways.

It really depends on your use case. A shared service might work, or for complex scenarios you might need a few different services, or you might find the event-aggregator works for you.

We’ve got a similar setup with toolbars but we don’t render them from arrays as we don’t need them to be populated dynamically. We simply treat them as individual components and hide the buttons that aren’t valid in each context:

<zoom-button x-bound.bind="xBounds"></zoom-button>
<undo-button history.bind="history"></undo-button>

This has some big pluses over creating and rendering a list of components via repeat.for:

  • Automatic instantiation and DI for each component
  • Different bindables for each button

I would only ever render components via repeat.for if you really need to create and render components dynamically.

2 Likes

Thats what I do now. But I don’t have the classes implementing ToolbarButtonInterace, I just write the toolbar definition inline as plain object (or array of objects), and let TypeScript check whether I wrote them correctly. As @timfish suggested, I can use classes instead and then I might be able to use @computedFrom on button properties, I just struggle on how to put it all together, trying to define the @computedFrom dependencies in the body of the “get buttons()”.

1 Like

Seems like your biggest challenge is related to observation rather than design pattern ?

I find that @computedFrom is very useful in some scenario but is also quite limited. Sometimes you better have to setup another observation strategy. Are you using aurelia-store in your app ? Did you consider using the state as a way to dynamically store the data related to your interface ?

Thank you, that matches with what @ben-girardet proposed. Also, thank you for the information on @computedFrom working even on classes instantiated directly, not using DI. I wasn’t sure about that.

Obviously the buttons in my app are not truly dynamic - only some of the drop-downs / sub-menus items are, but that’s fine. I suppose what you both suggested just makes sense. I would still love to have more type safety and be able to do something like this:

<toolbar>
  <button params.bind="menuItem1"></button>
  <dropdown params.bind="menuItem2"></button>
</toolbar>

…binding all the button parameters in a single property returning ButtonInterface. Unfortunately, that way I would still need to find a way how to define that each of the button inner properties can be computed from different properties of my VM. I would love to be able to specify that inline…

Thank you

1 Like

Yes I would say so. @computedFrom is super elegant, but I often use property/expressionObservers in my VMs to get around its limitations.

I don’t use Aurelia Store. I’ve studied the documentation available when starting the development of the current project, and decided not to use it. I felt there were not enough examples and I didn’t have the fantasy to imagine how to structure the large application I work on. At the end of my decision making before development, I read somewhere that where the store concept falls short are the applications with (many) forms, which was my case… or at least I though so at the time.

The project is going to its final stage now and I believe the Store would save me a lot problems, but it is also possible it would introduce new problems - one never knows without trying that…

1 Like

Not always easy to fine tune the right usage of every possibilities. After quite a lot of trial and errors I’ve found that the Store is very useful but should not be used to solved every problems.

From what you describe I guess that the Store would be a great fit to solve your toolbar situation. You can dynamically update the State with the buttons you need for each view. It’s typically a “State” issue because your buttons are a reflection of the current state of the application and what could possibly the next steps for the current user.

But when it comes to the core data that your application manages, it probably shouldn’t be all handled by the store. This might be better handled by the view directly or dedicated services.

I like the term “Stage” to help me decided what should be in the store or not. What kind of informations really describe the state of my application at a current time. What kind of things I would like to have directly available if the user refreshes the browser (before to make any API calls). I like to have the UX all available on refresh, even if I need to fetch data again from the API to populate the view.

BTW: I use localStorage to save the state so that I can get it on refresh…

Not sure if this is the best approach TBH. Learning every day new things… Hopefully we get better along the way…

3 Likes

Exactly. I’ve build a couple of large web application on Aurelia before, but they were “typical” web sites pretty simple from screens and data life-cycle point of of view. User went to a list, selected an item, went to an item detail, clicked Edit, changed the form, clicked Save, returned back to a list, went to another list.

This time, we wanted to build a Windows-app like interface, mimicking the UI of previous version of the app, providing super fast UI with easy access to everything. We wanted auto-save on forms, and real-time updating from the back-end. Multiple users can work on a single entity. Additionally to that, it has to be able to work offline without internet connectivity…

All of that presented completely new challenges to me, especially regarding the state of the app, as many of the components can stay on the screen for a long time, and need to update their content both based on the user input/navigation, and back-end data in real time.

This is how one of the screens look like:

As implemented, I communicate the changes between components using Event Agregator. SIgnal-R raises its own events and components determine whether they need to refresh the data or not. Initially, we thought it would be heavy on forms, but at the end it was not the case, so I am quite sure such application would be a perfect adept for Aurelia Store. But I just wasn’t sure how to use it…

Possibly next time :smiley:

2 Likes

Yes perfect example for the store indeed. Had very similar challenges with the IDE I’m building in Electron and that was what drove the creation of the store plugin :slight_smile:

3 Likes

I believe you can use the store in addition to services. You don’t have to buy all-in. Just use the store for a small piece and have a feel. :wink:

2 Likes