import {
Component,
ElementRef,
EventEmitter,
Input,
Output,
ViewChild,
type AfterViewInit,
type OnChanges,
type OnDestroy,
type SimpleChanges,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { Brackets as CoreBrackets } from '../facade/create.facade';
import type {
BracketsApi,
BracketsLabels,
BracketsState,
BracketsTheme,
MatchInput,
RoundsInput,
ShowScores,
} from '../types';
const EMPTY_STATE: BracketsState = {
titles: false,
rounds: [],
thirdPlace: null,
};
function remountKeyFrom(inputs: {
titles?: boolean | string[];
thirdPlace?: boolean | MatchInput;
theme?: BracketsTheme;
radius?: number | string;
matchWidth?: number | string | null;
showScores?: ShowScores;
roundNav?: boolean;
labels?: BracketsLabels | null;
}): string {
return JSON.stringify({
titles: inputs.titles ?? false,
thirdPlace: inputs.thirdPlace ?? false,
theme: inputs.theme ?? 'default',
radius: inputs.radius ?? 8,
matchWidth: inputs.matchWidth ?? null,
showScores: inputs.showScores ?? 'auto',
roundNav: inputs.roundNav ?? false,
labels: inputs.labels ?? null,
});
}
@Component({
selector: 'tb-brackets',
standalone: true,
imports: [CommonModule],
template: `
`,
})
export class BracketsComponent
implements BracketsApi, AfterViewInit, OnChanges, OnDestroy
{
@ViewChild('host', { static: true }) hostRef!: ElementRef;
@Input() rounds: RoundsInput | null | undefined;
@Input() titles?: boolean | string[];
@Input() thirdPlace?: boolean | MatchInput;
@Input() theme?: BracketsTheme;
@Input() radius?: number | string;
@Input() matchWidth?: number | string | null;
@Input() showScores?: ShowScores;
@Input() roundNav?: boolean;
@Input() viewFromRound?: number;
@Input() labels?: BracketsLabels | null;
@Input() class = '';
@Input() style: Record | null = null;
@Output() change = new EventEmitter();
@Output() roundChange = new EventEmitter();
private api: BracketsApi | null = null;
private viewReady = false;
private lastRemountKey = '';
ngAfterViewInit(): void {
this.lastRemountKey = remountKeyFrom(this);
this.mountInstance();
this.viewReady = true;
}
ngOnChanges(changes: SimpleChanges): void {
if (!this.viewReady) return;
const key = remountKeyFrom(this);
if (key !== this.lastRemountKey) {
this.lastRemountKey = key;
this.mountInstance();
return;
}
if (changes['rounds']) {
this.api?.setRounds(this.rounds ?? null);
}
if (changes['viewFromRound'] && this.viewFromRound != null) {
this.api?.setViewFromRound(this.viewFromRound);
}
}
ngOnDestroy(): void {
this.teardown();
}
getState(): BracketsState {
return this.api?.getState() ?? EMPTY_STATE;
}
setRounds(
rawRounds: RoundsInput | null | undefined,
thirdPlaceOverride?: boolean | MatchInput,
): void {
this.api?.setRounds(rawRounds, thirdPlaceOverride);
}
setViewFromRound(index: number): void {
this.api?.setViewFromRound(index);
}
destroy(): void {
this.teardown();
}
private mountInstance(): void {
const el = this.hostRef?.nativeElement;
if (!el) return;
this.teardown();
this.api = CoreBrackets.create(el, {
rounds: this.rounds,
titles: this.titles,
thirdPlace: this.thirdPlace,
theme: this.theme,
radius: this.radius,
matchWidth: this.matchWidth,
showScores: this.showScores,
roundNav: this.roundNav,
viewFromRound: this.viewFromRound,
labels: this.labels ?? undefined,
onChange: (state) => {
queueMicrotask(() => this.change.emit(state));
},
onRoundChange: (index) => {
queueMicrotask(() => this.roundChange.emit(index));
},
});
}
private teardown(): void {
this.api?.destroy();
this.api = null;
}
}