0

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

1

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>
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you. And in which way can I set a default selected button?
Posted an update describing some ways to achieve that

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.