Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
423 changes: 423 additions & 0 deletions ADAPTERS.md

Large diffs are not rendered by default.

43 changes: 38 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,28 @@
"types": "./dist/index.d.ts"
},
"./react": {
"import": "./dist/react.js",
"types": "./dist/react.d.ts"
"import": "./dist/adapters/react.js",
"types": "./dist/adapters/react.d.ts"
},
"./vue": {
"import": "./dist/vue.js",
"types": "./dist/vue.d.ts"
"import": "./dist/adapters/vue.js",
"types": "./dist/adapters/vue.d.ts"
},
"./angular": {
"import": "./dist/adapters/angular.js",
"types": "./dist/adapters/angular.d.ts"
},
"./solid": {
"import": "./dist/adapters/solid.js",
"types": "./dist/adapters/solid.d.ts"
},
"./svelte": {
"import": "./dist/adapters/svelte.js",
"types": "./dist/adapters/svelte.d.ts"
},
"./webcomponent": {
"import": "./dist/adapters/webcomponent.js",
"types": "./dist/adapters/webcomponent.d.ts"
},
"./style.css": "./dist/style.css"
},
Expand Down Expand Up @@ -50,6 +66,11 @@
"sports",
"react",
"vue",
"angular",
"solid",
"solidjs",
"svelte",
"web-components",
"typescript"
],
"author": "Erik Zettersten",
Expand Down Expand Up @@ -83,13 +104,25 @@
"vue": "^3.5.12"
},
"peerDependencies": {
"react": ">=16.8.0",
"@angular/core": ">=18.0.0",
"react": ">=18.0.0",
"solid-js": ">=1.8.0",
"svelte": ">=4.0.0 || >=5.0.0",
"vue": ">=3.0.0"
},
"peerDependenciesMeta": {
"@angular/core": {
"optional": true
},
"react": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
}
Expand Down
228 changes: 228 additions & 0 deletions src/adapters/angular.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import {
Component,
Input,
Output,
EventEmitter,
OnInit,
OnDestroy,
OnChanges,
SimpleChanges,
ElementRef,
ViewChild,
ChangeDetectionStrategy,
inject,
effect,

Check failure on line 14 in src/adapters/angular.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'effect' is defined but never used
signal,
} from '@angular/core';
import { Gracket } from '../core/Gracket';
import type { GracketOptions, TournamentData } from '../types';

/**
* Angular 18+ Component wrapper for Gracket
* Uses modern Angular features: signals, inject, standalone components
*/
@Component({
selector: 'gracket',
standalone: true,
template: `
@if (error()) {
<div class="gracket-error" [style]="{ color: 'red', padding: '1rem' }">
Error initializing Gracket: {{ error()?.message }}
</div>
} @else {
<div #container [class]="className" [ngStyle]="style"></div>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GracketComponent implements OnInit, OnDestroy, OnChanges {
@ViewChild('container', { static: false }) containerRef?: ElementRef<HTMLDivElement>;

@Input({ required: true }) data!: TournamentData;
@Input() options?: Omit<GracketOptions, 'src'>;
@Input() className?: string;
@Input() style?: Record<string, string>;

@Output() init = new EventEmitter<Gracket>();
@Output() errorOccurred = new EventEmitter<Error>();
@Output() dataUpdated = new EventEmitter<TournamentData>();

// Using signals for reactive state - Angular 18+ best practice
protected error = signal<Error | null>(null);
private gracketInstance = signal<Gracket | null>(null);
private elementRef = inject(ElementRef);

ngOnInit(): void {
// Initialize will happen after view init when container is available
}

ngAfterViewInit(): void {
this.initializeGracket();
}

ngOnChanges(changes: SimpleChanges): void {
if (changes['data'] && !changes['data'].firstChange) {
this.updateData();
}

if (changes['options'] && !changes['options'].firstChange) {
this.reinitializeGracket();
}
}

ngOnDestroy(): void {
this.destroy();
}

private initializeGracket(): void {
const container = this.containerRef?.nativeElement;
if (!container || !this.data?.length) return;

try {
const instance = new Gracket(container, {
...this.options,
src: this.data,
});

this.gracketInstance.set(instance);
this.error.set(null);
this.init.emit(instance);
} catch (err) {
const error = err as Error;
this.error.set(error);
this.errorOccurred.emit(error);
console.error('Gracket initialization error:', err);
}
}

private reinitializeGracket(): void {
this.destroy();
this.initializeGracket();
}

private updateData(): void {
const instance = this.gracketInstance();
if (instance && this.data?.length) {
try {
instance.update(this.data);
this.error.set(null);
this.dataUpdated.emit(this.data);
} catch (err) {
const error = err as Error;
this.error.set(error);
this.errorOccurred.emit(error);
}
}
}

/** Update a team's score */
public updateScore(
roundIndex: number,
gameIndex: number,
teamIndex: number,
score: number
): void {
try {
this.gracketInstance()?.updateScore(roundIndex, gameIndex, teamIndex, score);
this.error.set(null);
} catch (err) {
this.error.set(err as Error);
}
}

/** Advance to next round */
public advanceRound(fromRound?: number): TournamentData | undefined {
try {
const result = this.gracketInstance()?.advanceRound(fromRound);
this.error.set(null);
return result;
} catch (err) {
this.error.set(err as Error);
return undefined;
}
}

/** Get the Gracket instance */
public getInstance(): Gracket | null {
return this.gracketInstance();
}

/** Destroy the instance */
public destroy(): void {
const instance = this.gracketInstance();
if (instance) {
instance.destroy();
this.gracketInstance.set(null);
}
}
}

/**
* Angular Service for programmatic Gracket control
* Can be injected into components
*/
export class GracketService {
private instances = new Map<string, Gracket>();

/** Create a new Gracket instance */
create(
id: string,
container: HTMLElement,
data: TournamentData,
options?: GracketOptions
): Gracket {
// Destroy existing instance if any
this.destroy(id);

const instance = new Gracket(container, {
...options,
src: data,
});

this.instances.set(id, instance);
return instance;
}

/** Get an existing instance */
get(id: string): Gracket | undefined {
return this.instances.get(id);
}

/** Update data for an instance */
update(id: string, data: TournamentData): void {
this.instances.get(id)?.update(data);
}

/** Update score */
updateScore(
id: string,
roundIndex: number,
gameIndex: number,
teamIndex: number,
score: number
): void {
this.instances.get(id)?.updateScore(roundIndex, gameIndex, teamIndex, score);
}

/** Advance round */
advanceRound(id: string, fromRound?: number): TournamentData | undefined {
return this.instances.get(id)?.advanceRound(fromRound);
}

/** Destroy an instance */
destroy(id: string): void {
const instance = this.instances.get(id);
if (instance) {
instance.destroy();
this.instances.delete(id);
}
}

/** Destroy all instances */
destroyAll(): void {
this.instances.forEach((instance) => instance.destroy());
this.instances.clear();
}
}

export default GracketComponent;
20 changes: 20 additions & 0 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Framework adapters for Gracket
*
* Modern, framework-specific wrappers for the Gracket tournament bracket library.
* Each adapter follows the latest best practices for its respective framework.
*
* @packageDocumentation
*/

// Re-export core library
export { Gracket } from '../core/Gracket';
export * from '../types';

// Note: Framework adapters should be imported directly from their submodules:
// - import { GracketReact } from 'gracket/react'
// - import { GracketVue } from 'gracket/vue'
// - import { GracketComponent } from 'gracket/angular'
// - import { GracketSolid } from 'gracket/solid'
// - import { gracket, createGracket } from 'gracket/svelte'
// - import { GracketElement } from 'gracket/webcomponent'
Loading
Loading