Skip to content

Angular Integration: Complete Example

An Angular component and service for the Wedding Band Builder.

Back to Script Tag guide

Service

typescript
// wedding-band.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

const SCRIPT_URL = 'https://releases.ijewel3d.com/libs/mini-viewer/latest/bundle.iife.js';

@Injectable({ providedIn: 'root' })
export class WeddingBandService {
  private scriptLoaded = false;

  api$ = new BehaviorSubject<any>(null);
  price$ = new BehaviorSubject<any>(null);
  ready$ = new BehaviorSubject<boolean>(false);

  loadScript(): Promise<void> {
    if (this.scriptLoaded) return Promise.resolve();
    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = SCRIPT_URL;
      script.onload = () => { this.scriptLoaded = true; resolve(); };
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  initViewer(container: HTMLElement, options: {
    basePath?: string;
    manifestUrl?: string;
    showUI?: boolean;
    theme?: string | object;
  } = {}) {
    const {
      basePath = 'https://your-cdn.com/wbb-assets/',
      manifestUrl = 'wedding-band-project.json',
      showUI = true,
      theme = null,
    } = options;

    const handleReady = (e: Event) => {
      const viewer = (e as CustomEvent).detail.viewer;
      const api = viewer.getPluginByType('WeddingBandBuilder')?.controller;
      if (!api) return;

      if (theme) api.setTheme(theme);

      api.events.on('price:updated', (data: any) => {
        if (data.pricing) this.price$.next(data.pricing);
      });

      this.api$.next(api);
      this.ready$.next(true);

      window.removeEventListener('ijewel-viewer-ready', handleReady);
    };

    window.addEventListener('ijewel-viewer-ready', handleReady);

    new (window as any).ijewelViewer.Viewer(container, {
      name: 'Wedding Band Builder',
      version: 'v5',
      basePath,
      plugins: {
        WeddingBandBuilder: { manifestUrl, showUI },
      },
    }, {
      showCard: false,
      showSwitchNode: false,
      showUiButtons: showUI,
      showConfigurator: false,
      showZoomButtons: showUI,
      enableZoom: true,
    });
  }
}

Component

typescript
// wedding-band-builder.component.ts
import { Component, ElementRef, Input, Output, EventEmitter, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { WeddingBandService } from './wedding-band.service';
import { Subscription } from 'rxjs';
import { filter, take } from 'rxjs/operators';

@Component({
  selector: 'app-wedding-band-builder',
  template: `
    <div class="wbb-wrapper">
      <div #viewer class="wbb-viewer"></div>
    </div>
  `,
  styles: [`
    .wbb-wrapper { width: 100%; height: 100%; }
    .wbb-viewer { width: 100%; height: 100%; }
  `],
})
export class WeddingBandBuilderComponent implements AfterViewInit, OnDestroy {
  @ViewChild('viewer') viewerEl!: ElementRef<HTMLElement>;

  @Input() basePath = 'https://your-cdn.com/wbb-assets/';
  @Input() showUI = true;
  @Input() theme: string | object | null = null;

  @Output() wbbReady = new EventEmitter<any>();
  @Output() priceChange = new EventEmitter<any>();

  private subs: Subscription[] = [];

  constructor(private wbb: WeddingBandService) {}

  async ngAfterViewInit() {
    await this.wbb.loadScript();

    this.wbb.initViewer(this.viewerEl.nativeElement, {
      basePath: this.basePath,
      showUI: this.showUI,
      theme: this.theme,
    });

    this.subs.push(
      this.wbb.ready$.pipe(filter(Boolean), take(1)).subscribe(() => {
        this.wbbReady.emit(this.wbb.api$.value);
      }),
      this.wbb.price$.pipe(filter(Boolean)).subscribe((pricing) => {
        this.priceChange.emit(pricing);
      }),
    );
  }

  ngOnDestroy() {
    this.subs.forEach(s => s.unsubscribe());
  }
}

Usage

typescript
// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div style="width: 100vw; height: 80vh;">
      <app-wedding-band-builder
        [showUI]="true"
        theme="luxury-gold"
        (wbbReady)="onReady($event)"
        (priceChange)="onPrice($event)"
      />
    </div>

    <div style="padding: 24px; font-size: 24px; font-weight: 600;">
      {{ price ? '$' + price.toFixed(2) : 'Loading...' }}
    </div>
  `,
})
export class AppComponent {
  price: number | null = null;
  api: any = null;

  onReady(api: any) {
    this.api = api;
    console.log('Profiles:', api.getAvailableProfiles());
  }

  onPrice(pricing: any) {
    this.price = pricing.totalUsd;
  }
}

Module Declaration

typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { WeddingBandBuilderComponent } from './wedding-band-builder.component';

@NgModule({
  declarations: [AppComponent, WeddingBandBuilderComponent],
  imports: [BrowserModule],
  bootstrap: [AppComponent],
})
export class AppModule {}