Problem: when a button changes its status to "active", container should change the state of the remaining buttons to "inactive".
Solution: Implement two-way communication between buttons and the container:
- a button should be able to notify the container that it became active
- a container should be able to set the remaining buttons to inactive state (or, a button should be able to know whether it is active or not from the container)
Basically, the container becomes a holder of the shared state for itself and all nested buttons. This state is made available to nested buttons via DI. State can be handled by a separate service, or it can be a part of the container component itself for simplicity (the latter approach is implemented in Material):
const CONTAINER = new InjectionToken<ContainerComponent>();
@Directive({
providers: [{provide: CONTAINER, useExisting: forwardRef(() => ContainerComponent)}]
})
class ContainerComponent {
private selectedButton: ButtonComponent | null = null
toggleButton(button: ButtonComponent) {
if (this.selectedButton = button) {
this.selectedButton = null
} else {
this.selectedButton = button;
}
}
isSelected(button: ButtonComponent): boolean {
return this.selectedButton = button
}
}
@Component({template: `
<button [class.selected]="isSelected()" (click)="onClick()">
<ng-content></ng-content>
</button>
`})
class ButtonComponent {
constructor(@Inject(CONTAINER) private container: ContainerComponent) {}
isSelected() {
return this.container.isSelected(this)
}
onClick() {
this.container.toggleButton(this)
}
}
Update: Pre-selecting a button
How do we set some button as "selected" initially?
Approach 1
One way is doing smth similar to what Material does.
- Add "selected" input to a button componentn
- In the Container we would read all buttons via ContentChildren
- Whenever the input changes, button should update the state in container.
A challenge is that now we have two sources of truth for "selected" flag in a button (button's input and the state coming from the container) - so we need to reconcile them, and the overall code becomes more involved.
Approach 2
Alternatively, let's assume your toggle component has some sort of "value" property. Kind of like html <select> element - each option has value property, and the selected property of the <select> is derived based on that.
In this case, we would have an input in ContainerComponent that allows to set initial value:
class ContainerComponent {
@Input() selected: any
toggleButton(value: any) {
if (this.selected !== value) {
this.selected = value
} else {
this.selected = null
}
}
}
class ButtonComponent {
@Input() value: any
onClick() {
this.container.toggleButton(this.value)
}
}
// usage
<container selected="option-1">
<my-button value="option-1"><my-button>
<my-button value="option-2"><my-button>
</container>