0
votes

I'm in trouble to create a component like mat-button-toggle-group of material

I create a simple container with an ng-content that wraps buttons and inside it some customized buttons. THe number of components-button can change...

<container-buttons-wrapper>
  <component-button>Test 1</component-button>
  <component-button>Test 2</component-button>
  <component-button>Test 3</component-button>
</container-buttons-wrapper>

component-button has inside only a button tag

<button (click)="setActive()" [ngClass]="active? 'active-class' : 'no-active-class'"><ng-content></button>

I defined a function setActive() that toggle active value

 setActive() {
   this.active = !this.active
 }

But I can't find a solution to control the other buttons into container. I want to reproduce exactly what mat-button-toggle-group. Is possible to define an eventEmitter inside the template html?

1

1 Answers

1
votes

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>