Component Library Development

Practical guide for creating reusable Aurelia 2 component libraries, covering architecture patterns, distribution strategies, and best practices without strong opinions.

Creating reusable component libraries enables code sharing across projects and contributes to the Aurelia ecosystem. This guide covers practical approaches to building, packaging, and distributing Aurelia 2 component libraries.

Library Architecture Options

Monolithic Library

Single package containing all components:

my-ui-library/
├── src/
│   ├── button/
│   ├── input/
│   ├── modal/
│   └── index.ts
└── package.json

Benefits: Simple to manage, single install Trade-offs: Larger bundle if using only some components

Modular Library

Separate packages for each component or component group:

@my-ui/
├── button/
├── input/
├── form-controls/
└── overlays/

Benefits: Granular imports, smaller bundles Trade-offs: More complex management, multiple packages

Hybrid Approach

Main package with optional modular packages:

my-ui-library/          # Main package with all components
├── packages/
│   ├── button/         # Optional standalone packages
│   ├── input/
│   └── modal/

Project Setup

Basic Structure

my-aurelia-library/
├── src/
│   ├── components/
│   │   ├── button/
│   │   │   ├── button.html
│   │   │   ├── button.ts
│   │   │   └── button.css
│   │   └── index.ts
│   ├── index.ts              # Main export
│   └── configuration.ts      # Registration helper
├── dist/                     # Build output
├── examples/                 # Usage examples
├── package.json
├── tsconfig.json
└── build.config.js           # Build configuration

Package.json Configuration

{
  "name": "@yourorg/aurelia-ui-library",
  "version": "1.0.0",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "peerDependencies": {
    "@aurelia/kernel": "^2.0.0",
    "@aurelia/runtime": "^2.0.0",
    "@aurelia/runtime-html": "^2.0.0"
  },
  "devDependencies": {
    "aurelia": "^2.0.0",
    "typescript": "^4.9.0"
  }
}

Component Development Patterns

Base Component Structure

// src/components/button/button.ts
import { bindable, customElement } from 'aurelia';

@customElement({
  name: 'ui-button',
  template: `
    <button 
      class="\${classes}" 
      disabled.bind="disabled"
      click.trigger="handleClick($event)">
      <slot></slot>
    </button>
  `,
  shadowOptions: { mode: 'open' } // Optional: use Shadow DOM
})
export class UiButton {
  @bindable variant: 'primary' | 'secondary' | 'danger' = 'primary';
  @bindable size: 'small' | 'medium' | 'large' = 'medium';
  @bindable disabled: boolean = false;

  get classes(): string {
    return `btn btn--${this.variant} btn--${this.size}`;
  }

  handleClick(event: MouseEvent): boolean {
    if (this.disabled) {
      event.preventDefault();
      event.stopPropagation();
      return false;
    }
    return true;
  }
}

Component Registration

Create a configuration helper for easy registration:

// src/configuration.ts
import { IContainer, IRegistration } from 'aurelia';
import { UiButton } from './components/button/button';
import { UiInput } from './components/input/input';
import { UiModal } from './components/modal/modal';

export const UILibraryConfiguration = {
  register(container: IContainer): IContainer {
    return container.register(
      UiButton,
      UiInput,
      UiModal
    );
  },

  // Individual component registration
  button: UiButton as IRegistration,
  input: UiInput as IRegistration,
  modal: UiModal as IRegistration
};

Barrel Exports

// src/index.ts
export { UiButton } from './components/button/button';
export { UiInput } from './components/input/input';
export { UiModal } from './components/modal/modal';
export { UILibraryConfiguration } from './configuration';

// Default configuration export
export default UILibraryConfiguration;

Styling Strategies

/* button.css */
.btn {
  --btn-padding: var(--ui-button-padding, 0.5rem 1rem);
  --btn-border-radius: var(--ui-button-border-radius, 0.25rem);
  --btn-font-weight: var(--ui-button-font-weight, 600);
  
  padding: var(--btn-padding);
  border-radius: var(--btn-border-radius);
  font-weight: var(--btn-font-weight);
  border: none;
  cursor: pointer;
  transition: all 0.2s ease;
}

.btn--primary {
  background: var(--ui-primary-color, #007bff);
  color: var(--ui-primary-text, white);
}

.btn--primary:hover {
  background: var(--ui-primary-hover, #0056b3);
}

Users can customize by setting CSS variables:

/* User's app styles */
:root {
  --ui-primary-color: #6366f1;
  --ui-button-border-radius: 0.5rem;
}

Shadow DOM + CSS Modules

@customElement({
  name: 'ui-button',
  template: `<button class="${classes}"><slot></slot></button>`,
  shadowOptions: { mode: 'open' }
})
export class UiButton {
  // Styles are encapsulated in Shadow DOM
}

Themeable Design System

// src/theme/theme.ts
export interface UITheme {
  colors: {
    primary: string;
    secondary: string;
    danger: string;
  };
  spacing: {
    xs: string;
    sm: string;
    md: string;
    lg: string;
  };
  typography: {
    fontFamily: string;
    fontSizes: Record<string, string>;
  };
}

export const defaultTheme: UITheme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    danger: '#dc3545'
  },
  spacing: {
    xs: '0.25rem',
    sm: '0.5rem',
    md: '1rem',
    lg: '1.5rem'
  },
  typography: {
    fontFamily: 'system-ui, sans-serif',
    fontSizes: {
      sm: '0.875rem',
      md: '1rem',
      lg: '1.125rem'
    }
  }
};

Build Configuration

TypeScript Configuration

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "node",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "skipLibCheck": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "examples"]
}
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import postcss from 'rollup-plugin-postcss';

export default {
  input: 'src/index.ts',
  external: [
    '@aurelia/kernel',
    '@aurelia/runtime',
    '@aurelia/runtime-html'
  ],
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs',
      sourcemap: true
    },
    {
      file: 'dist/index.esm.js',
      format: 'es',
      sourcemap: true
    }
  ],
  plugins: [
    nodeResolve(),
    typescript(),
    postcss({
      extract: 'styles.css',
      minimize: true
    })
  ]
};

Vite Build Alternative

// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'AureliaUILibrary',
      fileName: (format) => `index.${format}.js`
    },
    rollupOptions: {
      external: [
        '@aurelia/kernel',
        '@aurelia/runtime',
        '@aurelia/runtime-html'
      ],
      output: {
        globals: {
          '@aurelia/kernel': 'AureliaKernel',
          '@aurelia/runtime': 'AureliaRuntime',
          '@aurelia/runtime-html': 'AureliaRuntimeHtml'
        }
      }
    }
  }
});

Distribution Strategies

NPM Package

# Build and publish
npm run build
npm publish

# Scoped package
npm publish --access public

CDN Distribution

Build UMD bundles for CDN usage:

// Additional Rollup output for CDN
{
  file: 'dist/index.umd.js',
  format: 'umd',
  name: 'AureliaUILibrary',
  globals: {
    '@aurelia/kernel': 'AureliaKernel',
    '@aurelia/runtime': 'AureliaRuntime',
    '@aurelia/runtime-html': 'AureliaRuntimeHtml'
  }
}

Usage Examples

Include usage examples for better adoption:

// examples/basic-usage.ts
import { Aurelia } from 'aurelia';
import { UILibraryConfiguration } from '@yourorg/aurelia-ui-library';

const au = new Aurelia();
au.register(UILibraryConfiguration);
au.app({
  host: document.querySelector('#app'),
  component: MyApp
}).start();

Testing Your Library

Component Testing

// tests/button.spec.ts
import { createFixture } from '@aurelia/testing';
import { UiButton } from '../src/components/button/button';

describe('UiButton', () => {
  it('renders with default properties', async () => {
    const { startPromise, stop, appHost } = createFixture(
      '<ui-button>Click me</ui-button>',
      class App {},
      [UiButton]
    );

    await startPromise;

    const button = appHost.querySelector('button');
    expect(button).toBeTruthy();
    expect(button!.textContent).toBe('Click me');
    expect(button!.className).toContain('btn--primary');

    await stop(true);
  });

  it('handles click events', async () => {
    const clickSpy = jest.fn();
    
    const { startPromise, stop, trigger } = createFixture(
      '<ui-button click.trigger="handleClick()">Click me</ui-button>',
      class App { handleClick = clickSpy; },
      [UiButton]
    );

    await startPromise;

    trigger.click('button');
    expect(clickSpy).toHaveBeenCalled();

    await stop(true);
  });
});

Integration Testing

Test library configuration and registration:

describe('UILibraryConfiguration', () => {
  it('registers all components', async () => {
    const { container, startPromise, stop } = createFixture(
      '<div></div>',
      class App {},
      [UILibraryConfiguration]
    );

    await startPromise;

    expect(container.has(UiButton)).toBe(true);
    expect(container.has(UiInput)).toBe(true);
    expect(container.has(UiModal)).toBe(true);

    await stop(true);
  });
});

Documentation

README Template

# @yourorg/aurelia-ui-library

A collection of reusable UI components for Aurelia 2.

## Installation

```bash
npm install @yourorg/aurelia-ui-library

Usage

Register All Components

import { UILibraryConfiguration } from '@yourorg/aurelia-ui-library';

au.register(UILibraryConfiguration);

Register Individual Components

import { UiButton } from '@yourorg/aurelia-ui-library';

au.register(UiButton);

Components

Button

<ui-button variant="primary" size="large">Click me</ui-button>

Properties:

  • variant: 'primary' | 'secondary' | 'danger'

  • size: 'small' | 'medium' | 'large'

  • disabled: boolean

Theming

Override CSS custom properties:

:root {
  --ui-primary-color: #your-color;
  --ui-button-border-radius: 0.5rem;
}

## Versioning and Compatibility

### Semantic Versioning
- **Major**: Breaking changes (Aurelia version updates, API changes)
- **Minor**: New components or features
- **Patch**: Bug fixes and improvements

### Aurelia Version Support

```json
{
  "peerDependencies": {
    "aurelia": "^2.0.0"
  },
  "engines": {
    "node": ">=16"
  }
}

Maintenance Considerations

Bundle Size Optimization

  • Use tree-shaking friendly exports

  • Minimize dependencies

  • Provide both full and modular imports

TypeScript Support

  • Include comprehensive type definitions

  • Export component interfaces

  • Provide good IntelliSense experience

Accessibility

  • Include ARIA attributes by default

  • Support keyboard navigation

  • Provide accessible color contrasts

Performance

  • Lazy load heavy components

  • Optimize CSS delivery

  • Minimize runtime overhead

Publishing Checklist

This guide provides flexible patterns for component library development while avoiding prescriptive decisions about specific architectural choices. Choose the approaches that best fit your project's needs and constraints.

Last updated

Was this helpful?