Third Party Library Integration

Learn how to integrate third-party JavaScript libraries with Aurelia 2 using proper lifecycle management, DOM interaction patterns, and ref usage.

Integrating third-party JavaScript libraries with Aurelia 2 requires understanding Aurelia's component lifecycle, DOM interaction patterns, and proper cleanup strategies. This guide covers best practices for seamless integration.

Understanding Aurelia's Lifecycle

Aurelia provides several lifecycle hooks that are crucial for third-party library integration:

export class MyComponent {
  // 1. Constructor - DI injection, basic setup
  constructor() {}
  
  // 2. created() - Component fully constructed, children resolved
  public created(): void {}
  
  // 3. binding() - Bindable properties assigned, before view binding
  public binding(): void {}
  
  // 4. bound() - View bindings established, refs available
  public bound(): void {}
  
  // 5. attached() - Component attached to DOM, ideal for 3rd party libs
  public attached(): void {}
  
  // 6. detaching() - Before DOM removal, cleanup time
  public detaching(): void {}
  
  // 7. unbinding() - Before view unbinding
  public unbinding(): void {}
}

DOM Interaction Patterns

Using Template References

Template references (ref) provide direct access to DOM elements:

import { customElement } from 'aurelia';

@customElement({
  name: 'chart-component',
  template: `
    <template>
      <div ref="chartContainer" class="chart-container"></div>
    </template>
  `
})
export class ChartComponent {
  private chartContainer!: HTMLDivElement;
  private chartInstance: any;

  public attached(): void {
    // chartContainer is now available and attached to DOM
    this.initializeChart();
  }

  private initializeChart(): void {
    // Third-party library initialization
    this.chartInstance = new SomeChartLibrary(this.chartContainer, {
      // configuration options
    });
  }

  public detaching(): void {
    // Always cleanup
    if (this.chartInstance) {
      this.chartInstance.destroy();
      this.chartInstance = null;
    }
  }
}

Working with Multiple Refs

@customElement({
  name: 'complex-widget',
  template: `
    <template>
      <div ref="headerElement" class="widget-header"></div>
      <div ref="contentElement" class="widget-content"></div>
      <div ref="footerElement" class="widget-footer"></div>
    </template>
  `
})
export class ComplexWidget {
  private headerElement!: HTMLDivElement;
  private contentElement!: HTMLDivElement;
  private footerElement!: HTMLDivElement;
  private widgetInstance: any;

  public attached(): void {
    // All refs are guaranteed to be available
    this.widgetInstance = new ComplexLibrary({
      header: this.headerElement,
      content: this.contentElement,
      footer: this.footerElement,
      onHeaderClick: this.handleHeaderClick.bind(this),
      onContentChange: this.handleContentChange.bind(this)
    });
  }

  private handleHeaderClick(): void {
    console.log('Header clicked');
  }

  private handleContentChange(data: any): void {
    console.log('Content changed:', data);
  }

  public detaching(): void {
    this.widgetInstance?.destroy();
  }
}

Common Integration Patterns

1. Chart Libraries (D3.js, Chart.js, etc.)

import { customElement, bindable } from 'aurelia';
import * as d3 from 'd3';

export interface ChartData {
  label: string;
  value: number;
}

@customElement({
  name: 'd3-bar-chart',
  template: `
    <template>
      <svg ref="svgElement" class="d3-chart"></svg>
    </template>
  `
})
export class D3BarChart {
  @bindable public data: ChartData[] = [];
  @bindable public width: number = 400;
  @bindable public height: number = 300;

  private svgElement!: SVGElement;
  private svg: d3.Selection<SVGElement, unknown, null, undefined>;

  public attached(): void {
    this.svg = d3.select(this.svgElement);
    this.renderChart();
  }

  public propertyChanged(name: string): void {
    if (['data', 'width', 'height'].includes(name) && this.svg) {
      this.renderChart();
    }
  }

  private renderChart(): void {
    // Clear previous chart
    this.svg.selectAll('*').remove();

    if (!this.data || this.data.length === 0) return;

    // Set dimensions
    this.svg
      .attr('width', this.width)
      .attr('height', this.height);

    // Create scales
    const xScale = d3.scaleBand()
      .domain(this.data.map(d => d.label))
      .range([0, this.width])
      .padding(0.1);

    const yScale = d3.scaleLinear()
      .domain([0, d3.max(this.data, d => d.value) || 0])
      .range([this.height, 0]);

    // Draw bars
    this.svg.selectAll('.bar')
      .data(this.data)
      .enter()
      .append('rect')
      .attr('class', 'bar')
      .attr('x', d => xScale(d.label) || 0)
      .attr('y', d => yScale(d.value))
      .attr('width', xScale.bandwidth())
      .attr('height', d => this.height - yScale(d.value))
      .attr('fill', 'steelblue');
  }
}

2. Date Pickers and Input Widgets

import { customElement, bindable } from 'aurelia';
import flatpickr from 'flatpickr';
import type { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance';

@customElement({
  name: 'date-picker',
  template: `
    <template>
      <input 
        ref="inputElement" 
        type="text" 
        class="date-input"
        placeholder="Select date..." />
    </template>
  `
})
export class DatePicker {
  @bindable public value: Date | null = null;
  @bindable public format: string = 'Y-m-d';
  @bindable public minDate?: Date;
  @bindable public maxDate?: Date;
  @bindable public onChange?: (date: Date | null) => void;

  private inputElement!: HTMLInputElement;
  private flatpickrInstance: FlatpickrInstance | null = null;

  public attached(): void {
    this.flatpickrInstance = flatpickr(this.inputElement, {
      dateFormat: this.format,
      minDate: this.minDate,
      maxDate: this.maxDate,
      defaultDate: this.value,
      onChange: (selectedDates) => {
        const newValue = selectedDates.length > 0 ? selectedDates[0] : null;
        this.value = newValue;
        this.onChange?.(newValue);
      }
    });
  }

  public propertyChanged(name: string): void {
    if (!this.flatpickrInstance) return;

    switch (name) {
      case 'value':
        this.flatpickrInstance.setDate(this.value || '');
        break;
      case 'minDate':
        this.flatpickrInstance.set('minDate', this.minDate);
        break;
      case 'maxDate':
        this.flatpickrInstance.set('maxDate', this.maxDate);
        break;
    }
  }

  public detaching(): void {
    if (this.flatpickrInstance) {
      this.flatpickrInstance.destroy();
      this.flatpickrInstance = null;
    }
  }
}

3. Rich Text Editors

import { customElement, bindable } from 'aurelia';
import Quill from 'quill';

@customElement({
  name: 'rich-text-editor',
  template: `
    <template>
      <div ref="editorContainer" class="editor-container"></div>
    </template>
  `
})
export class RichTextEditor {
  @bindable public content: string = '';
  @bindable public placeholder: string = 'Start typing...';
  @bindable public readOnly: boolean = false;
  @bindable public onContentChange?: (content: string) => void;

  private editorContainer!: HTMLDivElement;
  private quillInstance: Quill | null = null;
  private isUpdatingFromProperty = false;

  public attached(): void {
    this.quillInstance = new Quill(this.editorContainer, {
      theme: 'snow',
      placeholder: this.placeholder,
      readOnly: this.readOnly,
      modules: {
        toolbar: [
          ['bold', 'italic', 'underline'],
          ['link', 'blockquote', 'code-block'],
          [{ list: 'ordered' }, { list: 'bullet' }],
          ['clean']
        ]
      }
    });

    // Set initial content
    if (this.content) {
      this.quillInstance.clipboard.dangerouslyPasteHTML(this.content);
    }

    // Listen for content changes
    this.quillInstance.on('text-change', () => {
      if (!this.isUpdatingFromProperty) {
        const html = this.quillInstance!.root.innerHTML;
        this.content = html;
        this.onContentChange?.(html);
      }
    });
  }

  public propertyChanged(name: string): void {
    if (!this.quillInstance) return;

    switch (name) {
      case 'content':
        if (!this.isUpdatingFromProperty) {
          this.isUpdatingFromProperty = true;
          this.quillInstance.clipboard.dangerouslyPasteHTML(this.content);
          this.isUpdatingFromProperty = false;
        }
        break;
      case 'readOnly':
        this.quillInstance.enable(!this.readOnly);
        break;
    }
  }

  public detaching(): void {
    if (this.quillInstance) {
      this.quillInstance.off('text-change');
      // Quill doesn't have a destroy method, but we can clean up
      this.editorContainer.innerHTML = '';
      this.quillInstance = null;
    }
  }
}

4. Modal and Overlay Libraries

import { customElement, bindable } from 'aurelia';
import { Modal } from 'bootstrap';

@customElement({
  name: 'bootstrap-modal',
  template: `
    <template>
      <div 
        ref="modalElement" 
        class="modal fade" 
        tabindex="-1"
        data-bs-backdrop.bind="backdrop"
        data-bs-keyboard.bind="keyboard">
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header" if.bind="title">
              <h5 class="modal-title">\${title}</h5>
              <button 
                type="button" 
                class="btn-close" 
                data-bs-dismiss="modal"
                aria-label="Close">
              </button>
            </div>
            <div class="modal-body">
              <slot></slot>
            </div>
            <div class="modal-footer" if.bind="showFooter">
              <button 
                type="button" 
                class="btn btn-secondary" 
                data-bs-dismiss="modal">
                Close
              </button>
            </div>
          </div>
        </div>
      </div>
    </template>
  `
})
export class BootstrapModal {
  @bindable public isOpen: boolean = false;
  @bindable public title?: string;
  @bindable public backdrop: boolean | 'static' = true;
  @bindable public keyboard: boolean = true;
  @bindable public showFooter: boolean = true;
  @bindable public onShow?: () => void;
  @bindable public onHide?: () => void;

  private modalElement!: HTMLDivElement;
  private modalInstance: Modal | null = null;

  public attached(): void {
    this.modalInstance = new Modal(this.modalElement, {
      backdrop: this.backdrop,
      keyboard: this.keyboard
    });

    // Listen to Bootstrap modal events
    this.modalElement.addEventListener('shown.bs.modal', () => {
      this.isOpen = true;
      this.onShow?.();
    });

    this.modalElement.addEventListener('hidden.bs.modal', () => {
      this.isOpen = false;
      this.onHide?.();
    });

    // Show modal if initially open
    if (this.isOpen) {
      this.modalInstance.show();
    }
  }

  public propertyChanged(name: string): void {
    if (name === 'isOpen' && this.modalInstance) {
      if (this.isOpen) {
        this.modalInstance.show();
      } else {
        this.modalInstance.hide();
      }
    }
  }

  public detaching(): void {
    if (this.modalInstance) {
      this.modalInstance.dispose();
      this.modalInstance = null;
    }
  }
}

Advanced Integration Techniques

Custom Attributes for Third-Party Libraries

import { customAttribute } from 'aurelia';

@customAttribute('tooltip')
export class TooltipAttribute {
  private element: Element;
  private tooltipInstance: any;

  constructor(element: Element) {
    this.element = element;
  }

  public attached(): void {
    // Initialize tooltip library
    this.tooltipInstance = new Tooltip(this.element, {
      title: this.value,
      placement: 'top',
      trigger: 'hover'
    });
  }

  public valueChanged(newValue: string): void {
    if (this.tooltipInstance) {
      this.tooltipInstance.setContent(newValue);
    }
  }

  public detaching(): void {
    if (this.tooltipInstance) {
      this.tooltipInstance.destroy();
      this.tooltipInstance = null;
    }
  }
}

Usage:

<button tooltip="Click me for action">Action Button</button>

Handling Async Library Loading

@customElement({
  name: 'lazy-map',
  template: `
    <template>
      <div if.bind="loading" class="loading">Loading map...</div>
      <div if.bind="error" class="error">Failed to load map: \${error}</div>
      <div if.bind="!loading && !error" ref="mapContainer" class="map-container"></div>
    </template>
  `
})
export class LazyMap {
  @bindable public apiKey: string = '';
  @bindable public center: { lat: number; lng: number } = { lat: 0, lng: 0 };

  private mapContainer!: HTMLDivElement;
  private mapInstance: any;
  private loading = true;
  private error: string | null = null;

  public async attached(): Promise<void> {
    try {
      // Dynamically load Google Maps
      await this.loadGoogleMapsAPI();
      this.initializeMap();
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Unknown error';
    } finally {
      this.loading = false;
    }
  }

  private async loadGoogleMapsAPI(): Promise<void> {
    // Check if already loaded
    if (window.google?.maps) {
      return;
    }

    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = `https://maps.googleapis.com/maps/api/js?key=${this.apiKey}&callback=initGoogleMaps`;
      script.async = true;
      script.defer = true;

      (window as any).initGoogleMaps = () => {
        resolve();
      };

      script.onerror = () => {
        reject(new Error('Failed to load Google Maps API'));
      };

      document.head.appendChild(script);
    });
  }

  private initializeMap(): void {
    this.mapInstance = new google.maps.Map(this.mapContainer, {
      center: this.center,
      zoom: 10
    });
  }

  public propertyChanged(name: string): void {
    if (name === 'center' && this.mapInstance) {
      this.mapInstance.setCenter(this.center);
    }
  }

  public detaching(): void {
    // Google Maps doesn't need explicit cleanup
    // But remove global callback if needed
    delete (window as any).initGoogleMaps;
  }
}

Error Handling and Resilience

Library Loading with Fallbacks

@customElement({
  name: 'resilient-chart',
  template: `
    <template>
      <div if.bind="usingFallback" class="fallback-chart">
        <h4>\${data.title}</h4>
        <ul>
          <li repeat.for="item of data.items">
            \${item.label}: \${item.value}
          </li>
        </ul>
      </div>
      <div if.bind="!usingFallback" ref="chartContainer"></div>
    </template>
  `
})
export class ResilientChart {
  @bindable public data: any = {};
  
  private chartContainer!: HTMLDivElement;
  private chartInstance: any;
  private usingFallback = false;

  public async attached(): Promise<void> {
    try {
      // Try to load preferred library
      const ChartLibrary = await import('preferred-chart-library');
      this.chartInstance = new ChartLibrary.Chart(this.chartContainer, {
        data: this.data
      });
    } catch (primaryError) {
      console.warn('Primary chart library failed, trying fallback:', primaryError);
      
      try {
        // Try fallback library
        const FallbackLibrary = await import('fallback-chart-library');
        this.chartInstance = new FallbackLibrary.SimpleChart(this.chartContainer, {
          data: this.data
        });
      } catch (fallbackError) {
        console.error('Both chart libraries failed:', fallbackError);
        // Use HTML fallback
        this.usingFallback = true;
      }
    }
  }

  public detaching(): void {
    if (this.chartInstance && !this.usingFallback) {
      this.chartInstance.destroy?.();
    }
  }
}

Performance Optimization

Intersection Observer for Lazy Loading

@customElement({
  name: 'lazy-third-party',
  template: `
    <template>
      <div class="placeholder" ref="placeholder" if.bind="!loaded">
        <div class="loading-indicator">Component will load when visible...</div>
      </div>
      <div ref="componentContainer" if.bind="loaded"></div>
    </template>
  `
})
export class LazyThirdParty {
  @bindable public threshold: number = 0.1;
  
  private placeholder!: HTMLDivElement;
  private componentContainer!: HTMLDivElement;
  private loaded = false;
  private observer: IntersectionObserver | null = null;
  private thirdPartyInstance: any;

  public attached(): void {
    this.observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          this.loadComponent();
        }
      },
      { threshold: this.threshold }
    );

    this.observer.observe(this.placeholder);
  }

  private async loadComponent(): Promise<void> {
    if (this.loaded) return;

    try {
      this.loaded = true;
      this.observer?.disconnect();

      // Wait for DOM update
      await new Promise(resolve => setTimeout(resolve, 0));

      // Initialize third-party component
      const ThirdPartyLib = await import('heavy-third-party-library');
      this.thirdPartyInstance = new ThirdPartyLib.Component(
        this.componentContainer,
        {
          // configuration
        }
      );
    } catch (error) {
      console.error('Failed to load third-party component:', error);
      this.loaded = false;
    }
  }

  public detaching(): void {
    this.observer?.disconnect();
    this.thirdPartyInstance?.destroy();
  }
}

Best Practices Summary

1. Lifecycle Management

  • Initialize third-party libraries in attached() when DOM is ready

  • Clean up in detaching() to prevent memory leaks

  • Use propertyChanged() for reactive updates

2. DOM Access

  • Always use ref for direct DOM element access

  • Ensure elements are available before library initialization

  • Avoid direct DOM queries when possible

3. Error Handling

  • Wrap library initialization in try-catch blocks

  • Provide fallbacks for critical functionality

  • Log errors for debugging

4. Performance

  • Use Intersection Observer for lazy loading

  • Consider async loading for non-critical libraries

  • Clean up event listeners and observers

5. Memory Management

  • Always call destroy/cleanup methods

  • Remove event listeners

  • Clear references to prevent memory leaks

This comprehensive approach ensures robust third-party library integration while maintaining Aurelia's reactive capabilities and performance characteristics.

Last updated

Was this helpful?