Angular Integration
Supports: Angular 12+ • TypeScript 4.0+ • Stencil v2.9.0+
Stencil can generate Angular component wrappers for your web components. This allows your Stencil components to be used within an Angular application. The benefits of using Stencil's component wrappers over the standard web components include:
- Angular component wrappers will be detached from change detection, preventing unnecessary repaints of your web component.
- Web component events will be converted to RxJS observables to align with Angular's
@Output()
and will not emit across component boundaries. - Optionally, form control web components can be used as control value accessors with Angular's reactive forms or
[ngModel]
. - It is not necessary to include the Angular
CUSTOM_ELEMENTS_SCHEMA
in all modules consuming your Stencil components.
Setup
Project Structure
We recommend using a monorepo structure for your component library with component wrappers. Your project workspace should contain your Stencil component library and the library for the generated Angular component wrappers.
An example project set-up may look similar to:
top-most-directory/
└── packages
├── stencil-library/
│ ├── stencil.config.js
│ └── src/components
└── angular-workspace/
└── projects/
└── component-library/
└── src/
├── lib/
└── public-api.ts
This guide uses Lerna for the monorepo, but you can use other solutions such as Nx, Turborepo, etc.
To use Lerna with this walk through, globally install Lerna:
- npm
- Yarn
- pnpm
npm install --global lerna
yarn global add lerna
pnpm add --global lerna
Creating a Monorepo
If you already have a monorepo, skip this section.
- npm
- Yarn
- pnpm
# From your top-most-directory/, initialize a workspace
lerna init
# install dependencies
npm install
# install typescript and node types
npm install typescript @types/node --save-dev
# From your top-most-directory/, initialize a workspace
lerna init
# install dependencies
yarn install
# install typescript and node types
yarn add typescript @types/node --dev
# From your top-most-directory/, initialize a workspace
lerna init
# install dependencies
pnpm install
# install typescript and node types
pnpm add typescript @types/node --save-dev
Creating a Stencil Component Library
If you already have a Stencil component library, skip this section.
In the packages/
directory, run the following commands to generate a Stencil component library:
- npm
- Yarn
- pnpm
npm init stencil components stencil-library
cd stencil-library
# Install dependencies
npm install
yarn create stencil components stencil-library
cd stencil-library
# Install dependencies
yarn install
pnpm create stencil components stencil-library
cd stencil-library
# Install dependencies
pnpm install
Creating an Angular Component Library
If you already have an Angular component library, skip this section.
The first time you want to create the component wrappers, you will need to have an Angular library package to write to.
In the packages/
directory, use the Angular CLI to generate a workspace and a library for your Angular component wrappers:
npx -p @angular/cli ng new angular-workspace --no-create-application
cd angular-workspace
npx -p @angular/cli ng generate library component-library
You can delete the component-library.component.ts
, component-library.service.ts
, and *.spec.ts
files.
You will also need to add your generated Stencil library as a peer-dependency so import references can be resolved correctly:
// packages/angular-workspace/projects/component-library/package.json
"peerDependencies": {
"@angular/common": "^15.1.0",
- "@angular/core": "^15.1.0"
+ "@angular/core": "^15.1.0",
+ "stencil-library": "*"
}
For more information, see the Lerna documentation on package dependency management.
The Angular CLI will install Jasmine as a dependency to your Angular workspace. However, Stencil uses Jest as it's unit testing solution. To avoid
type definition collisions when attempting to build your Stencil project, you can remove jasmine-core
and @types/jasmine
as dependencies in the Angular
workspace package.json
file:
- npm
- Yarn
- pnpm
# from `/packages/angular-workspace`
npm uninstall jasmine-core @types/jasmine
# from `/packages/angular-workspace`
yarn remove jasmine-core @types/jasmine
# from `/packages/angular-workspace`
pnpm remove jasmine-core @types/jasmine
Adding the Angular Output Target
Install the @stencil/angular-output-target
dependency to your Stencil component library package.
- npm
- Yarn
- pnpm
# Install dependency
npm install @stencil/angular-output-target --save-dev
# Install dependency
yarn add @stencil/angular-output-target --dev
# Install dependency
pnpm add @stencil/angular-output-target --save-dev
In your project's stencil.config.ts
, add the angularOutputTarget
configuration to the outputTargets
array:
import { angularOutputTarget } from '@stencil/angular-output-target';
export const config: Config = {
namespace: 'stencil-library',
outputTargets: [
// By default, the generated proxy components will
// leverage the output from the `dist` target, so we
// need to explicitly define that output alongside the
// Angular target
{
type: 'dist',
},
angularOutputTarget({
componentCorePackage: 'stencil-library',
outputType: 'component',
directivesProxyFile: '../angular-workspace/projects/component-library/src/lib/stencil-generated/components.ts',
directivesArrayFile: '../angular-workspace/projects/component-library/src/lib/stencil-generated/index.ts',
}),
],
};
The componentCorePackage
should match the name
field in your Stencil project's package.json
.
outputType
should be set to 'component'
for Stencil projects using the dist
output. Otherwise if using the custom elements output, outputType
should be set to 'scam'
or 'standalone'
.
The directivesProxyFile
is the relative path to the file that will be generated with all of the Angular component wrappers. You will replace the
file path to match your project's structure and respective names. You can generate any file name instead of components.ts
.
The directivesArrayFile
is the relative path to the file that will be generated with a constant of all the Angular component wrappers. This
constant can be used to easily declare and export all the wrappers.
See the API section below for details on each of the output target's options.
You can now build your Stencil component library to generate the component wrappers.
- npm
- Yarn
- pnpm
# Build the library and wrappers
npm run build
# Build the library and wrappers
yarn build
# Build the library and wrappers
pnpm run build
If the build is successful, you will now have contents in the file specified in directivesProxyFile
and directivesArrayFile
.
You can now finally import and export the generated component wrappers for your component library. For example, in your library's main Angular module:
import { DIRECTIVES } from './stencil-generated';
@NgModule({
declarations: [...DIRECTIVES],
exports: [...DIRECTIVES],
})
export class ComponentLibraryModule {}
Any components that are included in the exports
array should additionally be exported in your main entry point (either public-api.ts
or
index.ts
). Skipping this step will lead to Angular Ivy errors when building for production. For this guide, simply add the following line to the
automatically generated public-api.ts
file:
export * from './lib/component-library.module';
export { DIRECTIVES } from './lib/stencil-generated';
export * from './lib/stencil-generated/components';
Registering Custom Elements
The default behavior for this output target does not handle automatically defining/registering the custom elements. One strategy (and the approach the Ionic Framework takes) is to use the loader to define all custom elements during app initialization:
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { defineCustomElements } from 'stencil-library/loader';
@NgModule({
...,
providers: [
{
provide: APP_INITIALIZER,
useFactory: () => defineCustomElements,
multi: true
},
]
})
export class ComponentLibraryModule {}
See the documentation for more information on defining custom elements using the
dist
output target, or update the Angular output target to use dist-custom-elements
.
Link Your Packages (Optional)
If you are using a monorepo tool (Lerna, Nx, etc.), skip this section.
Before you can successfully build a local version of your Angular component library, you will need to link the Stencil package to the Angular package.
From your Stencil project's directory, run the following command:
- npm
- Yarn
- pnpm
# Link the working directory
npm link
# Link the working directory
yarn link
# Link the working directory
pnpm link
From your Angular component library's directory, run the following command:
- npm
- Yarn
- pnpm
# Link the package name
npm link name-of-your-stencil-package
# Link the package name
yarn link name-of-your-stencil-package
# Link the package name
pnpm link name-of-your-stencil-package
The name of your Stencil package should match the name
property from the Stencil component library's package.json
.
Your component libraries are now linked together. You can make changes in the Stencil component library and run npm run build
to propagate the
changes to the Angular component library.
As an alternative to npm link
, you can also run npm install
with a relative path to your Stencil component library. This strategy,
however, will modify your package.json
so it is important to make sure you do not commit those changes.
Consumer Usage
If you already have an Angular app, skip this section.
Angular with Modules
Creating a Consumer Angular App
From your Angular workspace (/packages/angular-workspace
), run the following command to generate an Angular application with modules:
npx -p @angular/cli ng generate app my-app --standalone=false
Consuming the Angular Wrapper Components
This section covers how developers consuming your Angular component wrappers will use your package and component wrappers in an Angular project using modules.
In order to use the generated component wrappers in the Angular app, you'll first need to build your Angular component library. From the root
of your Angular workspace (/packages/angular-workspace
), run the following command:
npx -p @angular/cli ng build component-library
- outputType: "component"
- outputType: "scam"
- outputType: "standalone"
Import your component library into your Angular app's module. If you distributed your components through a primary NgModule
, you can simply import that module into an implementation to use your components.
import { ComponentLibraryModule } from 'component-library';
@NgModule({
imports: [ComponentLibraryModule],
})
export class AppModule {}
Otherwise you will need to add the components to your module's declarations
and exports
arrays.
import { MyComponent } from 'component-library';
@NgModule({
declarations: [MyComponent],
exports: [MyComponent],
})
export class AppModule {}
You can now directly leverage your components in their template and take advantage of Angular template binding syntax.
<my-component first="Your" last="Name"></my-component>
Now you can reference your component library as a standard import. Each component will be exported as a separate module.
import { MyComponentModule } from 'component-library';
@NgModule({
imports: [MyComponentModule],
})
export class AppModule {}
You can now directly leverage your components in their template and take advantage of Angular template binding syntax.
<my-component first="Your" last="Name"></my-component>
Now you can import and reference your components in your consuming application in the same way you would with any other Angular components:
import { Component } from '@angular/core';
import { MyComponent } from 'component-library';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
standalone: true,
imports: [MyComponent],
})
export class AppComponent {}
You can now leverage your components in the template and take advantage of Angular template binding syntax.
<my-component first="Your" last="Name"></my-component>
From your Angular workspace (/packages/angular-workspace
), run npm start
and navigate to localhost:4200
. You should see the
component rendered correctly.
Angular with Standalone Components
In Angular CLI v17, the default behavior is to generate a new project with standalone components.
From your Angular workspace (/packages/angular-workspace
), run the following command to generate an Angular application:
npx -p @angular/cli ng generate app my-app
Consuming the Angular Wrapper Components
This section covers how developers consuming your Angular component wrappers will use your package and component wrappers.
In order to use the generated component wrappers in the Angular app, you'll first need to build your Angular component library. From the root
of your Angular workspace (/packages/angular-workspace
), run the following command:
npx -p @angular/cli ng build component-library
- outputType: "component"
- outputType: "scam"
- outputType: "standalone"
Import your component library into your component. You must distribute your components through a primary NgModule
to use your components in a standalone component.
import { Component } from '@angular/core';
import { ComponentLibraryModule } from 'component-library';
@Component({
selector: 'app-root',
standalone: true,
imports: [ComponentLibraryModule],
templateUrl: './app.component.html',
})
export class AppComponent {}
You can now directly leverage your components in their template and take advantage of Angular template binding syntax.
<my-component first="Your" last="Name"></my-component>
Now you can reference your component library as a standard import. Each component will be exported as a separate module.
import { Component } from '@angular/core';
import { MyComponentModule } from 'component-library';
@Component({
selector: 'app-root',
standalone: true,
imports: [MyComponentModule],
templateUrl: './app.component.html',
})
export class AppComponent {}
You can now directly leverage your components in their template and take advantage of Angular template binding syntax.
<my-component first="Your" last="Name"></my-component>
Now you can import and reference your components in your consuming application in the same way you would with any other standalone Angular components:
import { Component } from '@angular/core';
import { MyComponent } from 'component-library';
@Component({
selector: 'app-root',
standalone: true,
imports: [MyComponent],
templateUrl: './app.component.html',
})
export class AppComponent {}
You can now leverage your components in the template and take advantage of Angular template binding syntax.
<my-component first="Your" last="Name"></my-component>
From your Angular workspace (/packages/angular-workspace
), run npm start
and navigate to localhost:4200
. You should see the component rendered correctly.
API
componentCorePackage
Required
Type: string
The name of the Stencil package where components are available for consumers (i.e. the value of the name
property in your Stencil component library's package.json
).
This is used during compilation to write the correct imports for components.
For a starter Stencil project generated by running:
- npm
- Yarn
- pnpm
npm init stencil component my-component-lib
yarn create stencil component my-component-lib
pnpm create stencil component my-component-lib
The componentCorePackage
would be set to:
export const config: Config = {
...,
outputTargets: [
angularOutputTarget({
componentCorePackage: 'my-component-lib',
// ... additional config options
})
]
}
Which would result in an import path like:
import { MyComponent } from 'my-component-lib/components/my-component.js';
customElementsDir
Optional
Default: 'components'
Type: string
This option can be used to specify the directory where the generated custom elements live.