Angular component library for embedded components in Heretto CCMS. This project provides reusable UI components designed to be deployed to CDN and integrated into existing CCMS, initially supporting gradual migration from GWT to Angular.
- Overview
- Architecture
- Prerequisites
- Getting Started
- Local Development
- Storybook
- Building for Production
- CDN Deployment
- GWT Integration
- Best Practices
- Testing
- Project Structure
- Contributing
This monorepo workspace contains:
- Shared Library (
projects/shared): Reusable UI components, services, and models - Elements Runtime (
projects/elements): Bootstraps Angular and registers custom elements (deployed to CDN, loaded by ezd)
- Angular: v20.3.0+ (latest with default standalone components)
- Node.js: v20.19.0+ (required)
- TypeScript: v5.9.2 with strict mode
- Package Manager: npm
- Build System: Angular CLI with esbuild
- Testing: Karma + Jasmine, AXE for accessibility
- Linting: ESLint with Angular best practices
- Change Detection: Zoneless - no zone.js runtime dependency
All code follows these mandatory principles from docs/llms/angular-best-practices.md:
- ✅ Standalone components (no NgModules)
- ✅ Signal-based state management
- ✅ OnPush change detection
- ✅
input()andoutput()functions (not decorators) - ✅
inject()function for dependency injection - ✅ Native control flow (
@if,@for,@switch) - ✅ TypeScript strict mode (no
anytypes) - ✅ WCAG AA accessibility compliance
- ✅ Google style guide adherence
Zoneless Change Detection
This project uses Angular's zoneless change detection (provideZonelessChangeDetection). This reduces bundle size (~15-20KB) and improves performance. All components use signals and OnPush change detection, making them fully zoneless-compatible.
No Angular Forms Dependency
This project intentionally does not use @angular/forms (Reactive Forms or Template-Driven Forms). Instead, we use signal-based state management directly. This approach was chosen because:
- Signal efficiency: Signals provide fine-grained reactivity without the overhead of form abstractions
- Simpler mental model: Direct signal binding (
[value]="signal()"+(input)="update()") is more explicit than form control wiring - Smaller bundle size: No additional forms module dependency (~40KB+ savings)
- Future-proof: Angular is introducing Signal Forms in v21 (November 2025) as an experimental feature, which will integrate signals natively with forms. Our current signal-based approach aligns with this direction and will make migration straightforward.
- Node.js: v20.19.0 or newer (Download)
- npm: v11.6.2+ (comes with Node.js)
- Angular CLI: v20.3.9+ (install globally)
# Verify Node.js version
node --version # Should be v20.19.0+
# Install Angular CLI globally
npm install -g @angular/cli@latest
# Verify Angular CLI installation
ng version# Navigate to project directory
cd ccms-components-angular
# Install dependencies
npm install
# Verify installation
npm run lintThe primary development workflow uses the watch script, which builds the shared library and serves the elements runtime:
npm run watchWhat this does:
- Builds the shared library once
- Watches for changes and rebuilds the library automatically
- Serves the elements runtime at
http://localhost:4200(ezd.html loads this to register custom elements)
Testing your changes:
- Run
npm run watchin this repository - Start the GWT application (ezd)
- Trigger Angular components from GWT UI (e.g., right-click → Find)
- Make changes to components - they'll rebuild and hot reload automatically
The elements project bootstraps Angular and registers custom elements (via customElements.define()). It's the production runtime deployed to CDN and loaded by ezd - not a standalone dev app.
- Dev: Served at
localhost:4200bynpm run watch, loaded by ezd.html - Prod: Built bundle deployed to CDN, loaded by production ezd.html
Limitation: Cannot run standalone (CORS issues with TurboDITA). Component development requires either running ezd or using Storybook.
💡 Tip: For isolated component development without running the full ezd application, see the Storybook section below.
# Generate component in shared library
ng generate component button --project=shared
# Follow the example in projects/shared/src/lib/button/When creating new components, ensure:
- Standalone component (default, don't set
standalone: true) -
changeDetection: ChangeDetectionStrategy.OnPush - Use
input()andoutput()functions - Signal-based state with
signal()andcomputed() - Host bindings in
@Componentdecorator (not@HostBinding) - Native control flow in templates (
@if,@for, not*ngIf,*ngFor) - ARIA attributes for accessibility
- CSS with WCAG AA color contrast
- Minimum 44px touch targets
- Unit tests with accessibility checks
- TSDoc comments for public APIs
# Primary development workflow
npm run watch # Build shared + watch + serve elements (recommended)
# Alternative commands
npm run build:elements # Build elements runtime for CDN deployment
npm run deploy:ezd # Build elements and copy to ezd project
# Code Formatting
npm run format # Auto-format all files (ts, html, css)
npm run format:check # Check formatting (used in CI)
# Testing
ng test shared # Run shared library tests
ng test shared --code-coverage # Run tests with coverage
npm run lint # Lint all projects
# Note: The elements build automatically includes all shared library code
# No need to build the shared library separately for deploymentStorybook provides isolated component development and documentation without requiring the full GWT application.
Storybook is integrated for component-driven development:
- ✅ Isolated Development: Develop components independently without running ezd
- ✅ Interactive Documentation: Auto-generated docs with props tables and usage examples
- ✅ Visual Testing: Preview all component variants and states
- ✅ No CORS Issues: Built-in webpack proxy forwards
/api/*requests tolocalhost:8080 - ✅ Hot Reload: Changes to components reflect immediately
- ✅ Angular 20 Support: Full support for signals, standalone components, and zoneless change detection
# Start Storybook development server
npm run storybook
# Access at http://localhost:6006Stories are colocated with components using the .stories.ts extension:
// Example: component-name.stories.ts
import type { Meta, StoryObj } from '@storybook/angular';
import { MyComponent } from './my-component';
const meta: Meta<MyComponent> = {
title: 'Components/MyComponent',
component: MyComponent,
tags: ['autodocs'],
argTypes: {
myInput: {
control: 'text',
description: 'Description of the input',
},
},
parameters: {
docs: {
description: {
component: `
## Overview
Component description with markdown support.
### Features
- Feature 1
- Feature 2
`,
},
},
},
};
export default meta;
type Story = StoryObj<MyComponent>;
export const Default: Story = {
args: {
myInput: 'Hello World',
},
render: (args) => ({
props: args,
template: `<ccms-my-component [myInput]="myInput" />`,
}),
};Automatic Documentation
- Component descriptions rendered as markdown
- Auto-generated props tables from TypeScript types
- Interactive controls for testing inputs
- Multiple story variants for different states
API Proxy
- Configured in
.storybook/main.ts - Proxies
/api/*requests tohttp://localhost:8080 - Start ezd backend locally for components that need real API data
- No CORS issues when developing components
Story files follow the pattern *.stories.ts and are colocated with components:
projects/shared/src/lib/components/
└── resource-chip/
├── resource-chip.ts
├── resource-chip.html
├── resource-chip.css
└── resource-chip.stories.ts ← Story file
Build a static version of Storybook for deployment:
npm run build-storybook
# Output: storybook-static/The GitHub Actions workflow generates downloadable Storybook artifacts for all builds. See the Building for Production section for details.
Main Configuration (.storybook/main.ts)
- Story file pattern:
projects/shared/src/lib/**/*.stories.ts - Addon:
@storybook/addon-docsfor documentation - Webpack proxy for API requests
Preview Configuration (.storybook/preview.ts)
- Global autodocs enabled
- Zoneless change detection
- Control matchers for color and date inputs
- Colocate stories with components for maintainability
- Use descriptive story names that indicate the variant/state
- Document with markdown in
parameters.docs.description - Configure argTypes for better interactive controls
- Create multiple stories for different component states
- Use the
autodocstag to generate documentation pages
# Build everything (recommended)
npm run build:all # Builds elements + storybook
# Build individual artifacts
npm run build:shared # Shared library (build-time dependency)
npm run build:elements # Elements runtime (includes all components)
npm run build:storybook # Storybook static site
# Deploy to ezd for local testing
npm run deploy:ezd # Build elements + copy to ../ezddist/
└── elements/ # Elements runtime (CDN-ready)
└── browser/
├── main-[hash].js # Complete bundle with all components (~203KB)
├── styles-[hash].css # Compiled styles
└── 3rdpartylicenses.txt
storybook-static/ # Storybook documentation site (deployable)
└── ...
Note: The elements bundle includes all components from the shared library.
Build Dependencies:
- Local: Elements can build directly from shared library source (no pre-build needed)
- CI/CD: Shared library must be built first so elements can resolve TypeScript imports
- Deployment: Only the elements bundle is deployed (shared is a build-time dependency)
Every push to main automatically triggers the build workflow (.github/workflows/build-artifacts.yml):
What it does:
- ✅ Builds elements runtime (complete bundle with all components)
- ✅ Builds Storybook documentation site
- ✅ Generates bundle size reports
- ✅ Uploads timestamped artifacts with retention
Access build artifacts:
- Go to GitHub Actions tab
- Click on latest "Build Artifacts" workflow run
- Download artifacts from the "Artifacts" section:
elements-runtime-YYYYMMDD-HHMMSS(90-day retention)storybook-site-YYYYMMDD-HHMMSS(30-day retention)bundle-size-report-YYYYMMDD-HHMMSS(30-day retention)
Production builds include:
- Tree-shaking for minimal bundle size
- Minification and optimization
- Source maps for debugging
- AOT compilation
- Output hashing for cache busting
The libraries are designed to be deployed to a CDN and consumed by other applications.
- Build library for production (see above)
- Upload
dist/directory contents to your CDN - Version management: Use semantic versioning in URLs
- Configure consuming application to load from CDN
https://your-cdn.com/
├── ccms-components/
│ ├── v1.0.0/
│ │ └── shared/
│ └── latest/ # Symlink to latest version
Angular components are integrated into the GWT application using Web Components (Custom Elements) via @angular/elements. All components share a single Angular runtime and can communicate through shared services.
Key Integration Points:
- Components loaded as native custom elements in GWT
- GWT creates elements using standard DOM API (
createElement,setAttribute) - Data passed via HTML attributes (kebab-case)
- Components self-remove when closed
For complete integration details, architecture, and code examples, see:
- docs/gwt-integration.md - Complete integration guide
This project follows:
- Angular Official Best Practices
- Google TypeScript Style Guide
- Custom rules in
.eslintrc.json
// ✅ DO: Use signals for state
protected count = signal<number>(0);
protected doubled = computed(() => this.count() * 2);
// ✅ DO: Use update() or set() to modify signals
this.count.update(n => n + 1);
this.count.set(0);
// ❌ DON'T: Use mutate()
this.count.mutate(n => n++); // NOT ALLOWED// ✅ DO: Use input() and output() functions
value = input<number>(0);
valueChange = output<number>();
// ❌ DON'T: Use decorators
@Input() value: number = 0; // NOT ALLOWED
@Output() valueChange = ...; // NOT ALLOWED// ✅ DO: Use inject() function
export class MyService {
private http = inject(HttpClient);
private logger = inject(LoggerService);
}
// ❌ DON'T: Use constructor injection
constructor(private http: HttpClient) {} // NOT ALLOWED// ✅ DO: Use native control flow
@if (showContent) {
<p>Content</p>
}
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
}
// ❌ DON'T: Use structural directives
<p *ngIf="showContent">...</p> // NOT ALLOWED
<div *ngFor="let item of items"> // NOT ALLOWEDAll components MUST:
- Pass AXE accessibility checks
- Meet WCAG AA standards
- Include proper ARIA attributes
- Support keyboard navigation
- Have sufficient color contrast
- Use semantic HTML
- Provide text alternatives
# Run tests for specific project
ng test shared
# Run with coverage
ng test shared --code-coverage
# Run in headless mode (CI)
ng test shared --browsers=ChromeHeadless --watch=falseComponents use jasmine-axe for automated accessibility testing:
import { toHaveNoViolations } from 'jasmine-axe';
expect(jasmine).toHaveNoViolations();
const result = await axe(fixture.nativeElement);
expect(result).toHaveNoViolations();- Unit tests for all public APIs
- Accessibility tests with AXE
- Keyboard navigation tests
- ARIA attribute validation
- Edge cases and error handling
- Signal state transitions
- Event emission verification
ccms-components-angular/
├── .storybook/ # Storybook configuration
│ ├── main.ts # Storybook main config (addons, webpack proxy)
│ ├── preview.ts # Preview config (global decorators, autodocs)
│ └── tsconfig.json # TypeScript config for Storybook
├── projects/
│ ├── shared/ # Shared library (components, services, models)
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── components/ # UI Components
│ │ │ │ │ ├── resource-chip/
│ │ │ │ │ │ ├── resource-chip.ts
│ │ │ │ │ │ ├── resource-chip.html
│ │ │ │ │ │ ├── resource-chip.css
│ │ │ │ │ │ └── resource-chip.stories.ts ← Storybook story
│ │ │ │ │ ├── button/
│ │ │ │ │ │ ├── button.component.ts
│ │ │ │ │ │ ├── button.component.html
│ │ │ │ │ │ ├── button.component.css
│ │ │ │ │ │ └── button.component.spec.ts
│ │ │ │ │ └── find-replace/
│ │ │ │ │ └── ...
│ │ │ │ ├── services/ # Injectable services
│ │ │ │ │ └── find-replace/
│ │ │ │ │ ├── find-replace.service.ts
│ │ │ │ │ └── models/
│ │ │ │ └── models/ # TypeScript interfaces
│ │ │ │ └── resource-file.interface.ts
│ │ │ └── public-api.ts # Public API surface
│ │ └── package.json
│ └── elements/ # Angular Elements runtime (registers custom elements)
│ ├── src/
│ │ ├── app/
│ │ │ ├── app.ts # Root component
│ │ │ ├── app.config.ts
│ │ │ └── app.routes.ts
│ │ ├── main.ts
│ │ └── index.html
│ └── tsconfig.app.json
├── dist/ # Build output (CDN-ready)
├── storybook-static/ # Built Storybook (for deployment)
├── docs/ # Documentation
│ └── llms/
│ ├── angular-best-practices.md # Mandatory coding standards
│ └── angular-llms-full.md # Angular documentation
├── angular.json # Angular workspace config
├── package.json # Dependencies & scripts
├── tsconfig.json # TypeScript config (strict mode)
├── .eslintrc.json # ESLint configuration
└── README.md # This file
- Read
docs/llms/angular-best-practices.md- ALL rules are mandatory - Follow the example component in
projects/shared/src/lib/components/button/ - Run linter before committing:
npm run lint - Write tests for all new components/services
- Check accessibility with AXE
- Document public APIs with TSDoc comments