Astro is a modern web framework designed for building fast, content-focused websites, compatible with all major frontend frameworks. Though primarily a static site generation (SSG) tool, it can also integrate dynamic components called "islands", which support partial hydration.
This package allows rendering Angular components as islands in Astro.
Use the astro add command to install the integration
Using npm:
npx astro add @analogjs/astro-angularUsing pnpm:
pnpm astro add @analogjs/astro-angularUsing yarn:
yarn astro add @analogjs/astro-angularThis command:
- Installs the
@analogjs/astro-angularpackage. - Adds the
@analogjs/astro-angularintegration to theastro.config.mjsfile. - Installs the necessary dependencies to render Angular components on the server and client, and common Angular dependencies, such as
@angular/common.
The integration needs a tsconfig.app.json at the root of the project for compilation.
Create a tsconfig.app.json in the root of the project.
{
"extends": "./tsconfig.json",
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"noEmit": false,
"target": "es2020",
"module": "es2020",
"lib": ["es2020", "dom"],
"skipLibCheck": true
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true,
"allowJs": false
},
"files": [],
"include": ["src/**/*.ts", "src/**/*.tsx"]
}Go to Defining A Component to set up an Angular component to use in an Astro component.
The integration can also be installed manually
yarn add @analogjs/astro-angularnpm install @angular/build @angular/{animations,common,compiler-cli,compiler,core,language-service,forms,platform-browser,platform-server} rxjs tslib --saveAdd the integration to the astro.config.mjs
import { defineConfig } from 'astro/config';
import angular from '@analogjs/astro-angular';
export default defineConfig({
integrations: [angular()],
});Go to Defining A Component
Provide an option object to configure the @analogjs/vite-plugin-angular powering this plugin.
import { defineConfig } from 'astro/config';
import angular from '@analogjs/astro-angular';
export default defineConfig({
integrations: [
angular({
vite: {
inlineStylesExtension: 'scss|sass|less',
},
}),
],
});For better compatibility when integrating with other plugins such as Starlight, put the Angular components in a specific folder and use the transformFilter callback function to only transform those files.
import { defineConfig } from 'astro/config';
import angular from '@analogjs/astro-angular';
export default defineConfig({
integrations: [
angular({
vite: {
transformFilter: (_code, id) => {
return id.includes('src/components'); // <- only transform Angular TypeScript files
},
},
}),
],
});To ensure Angular libraries are transformed during Astro's SSR process, add them to the ssr.noExternal array in the Vite config.
import { defineConfig } from 'astro/config';
import angular from '@analogjs/astro-angular';
export default defineConfig({
integrations: [angular()],
vite: {
ssr: {
// transform these packages during SSR. Globs supported
noExternal: ['@rx-angular/**'],
},
},
});Angular components can use style scoped to each component instance. By default, these style tags will be inserted adjacent to the component's HTML by @analogjs/astro-angular. While this will typically work for modern browsers, it is technically invalid HTML.
To force these component styles to the document head, enable the strictStylePlacement option in the integration config.
Warning: enabling this option will disable Astro's streaming mode under SSR.
import { defineConfig } from 'astro/config';
import angular from '@analogjs/astro-angular';
export default defineConfig({
integrations: [angular({ strictStylePlacement: true })],
});By default, @analogjs/astro-angular performs hydration by bootstrapping the component on the client, replacing the DOM that was rendered on the server.
To opt-in to Angular's client hydration, enable the experimental.useAngularHydration option in the integration config. This will switch the hydration strategy to use provideClientHydration.
import { defineConfig } from 'astro/config';
import angular from '@analogjs/astro-angular';
export default defineConfig({
integrations: [angular({ useAngularHydration: true })],
});Use the ngSkipHydration attribute on any components which do not work properly with hydration enabled. Read more here.
The Astro Angular integration only supports rendering standalone components:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-hello',
template: `
<p>Hello from Angular!!</p>
@if (show()) {
<p>{{ helpText() }}</p>
}
<button (click)="toggle()">Toggle</button>
`,
})
export class HelloComponent {
helpText = input('help');
show = signal(false);
toggle() {
this.show.update((show) => !show);
}
}Add the Angular component to the Astro component template. This only renders the HTML from the Angular component.
---
import { HelloComponent } from '../components/hello.component';
const helpText = "Helping binding";
---
<HelloComponent />
<HelloComponent helpText="Helping" />
<HelloComponent helpText={helpText} />To hydrate the component on the client, use one of the Astro client directives:
---
import { HelloComponent } from '../components/hello.component';
---
<HelloComponent client:visible />Find more information about Client Directives in the Astro documentation.
Outputs can be emitted by the Angular component are forwarded as HTML events to the Astro island.
To enable this feature, add a client directive and a unique [data-analog-id] property to each Angular component:
---
import { HelloComponent } from '../components/hello.component';
---
<HelloComponent client:visible data-analog-id="hello-component-1" />Then, listen to the event in the Astro component using the addOutputListener function:
---
import { HelloComponent } from '../components/hello.component';
---
<HelloComponent client:visible data-analog-id="hello-component-1" />
<script>
import { addOutputListener } from '@analogjs/astro-angular/utils';
addOutputListener('hello-component-1', 'outputName', (event) => {
console.log(event.detail);
});
</script>Additional providers can be added to a component for static rendering and client hydration.
These are renderProviders and clientProviders respectively. These providers are defined as static arrays on the Component class, and are registered when the component is rendered, and hydrated on the client.
import { Component, OnInit, inject } from '@angular/core';
import { provideHttpClient, HttpClient } from '@angular/common/http';
interface Todo {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-todos',
template: `
<h2>Todos</h2>
<ul>
@for (todo of todos(); track todo.id) {
<li>
{{ todo.title }}
</li>
}
</ul>
`,
})
export class TodosComponent implements OnInit {
static clientProviders = [provideHttpClient()];
static renderProviders = [TodosComponent.clientProviders];
http = inject(HttpClient);
todos = signal<Todo[]>([]);
ngOnInit() {
this.http
.get<Todo[]>('https://jsonplaceholder.typicode.com/todos')
.subscribe((todos) => this.todos.set(todos));
}
}First, make sure the experimental Angular client hydration option is enabled in the integration config. Read more here.
To add Angular hydration features, add a static property to the component class named hydrationFeatures. This should be a function that returns an array of hydration features to enable.
The example below adds the event replay feature to the component.
import { Component, input, signal } from '@angular/core';
import { withEventReplay } from '@angular/platform-browser';
@Component({
selector: 'app-hello',
template: `
<p>Hello from Angular!!</p>
@if (show()) {
<p>{{ helpText() }}</p>
}
<button (click)="toggle()">Toggle</button>
`,
})
export class HelloComponent {
static hydrationFeatures = () => [withEventReplay()];
helpText = input('help');
show = signal(false);
toggle() {
this.show.update((show) => !show);
}
}To use components with MDX pages, you must install and configure MDX support by following the Astro integration of @astrojs/mdx. Your astro.config.mjs should now include the @astrojs/mdx integration.
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import angular from '@analogjs/astro-angular';
export default defineConfig({
integrations: [mdx(), angular()],
});Create an .mdx file inside the src/pages directory and add the Angular component import below the frontmatter.
---
layout: '../../layouts/BlogPost.astro'
title: 'Using Angular in MDX'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Sep 22 2022'
---
import { HelloComponent } from "../../components/hello.component.ts";
<HelloComponent />
<HelloComponent helpText="Helping" />To hydrate the component on the client, use one of the Astro client directives:
---
layout: '../../layouts/BlogPost.astro'
title: 'Using Angular in MDX'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Sep 22 2022'
---
import { HelloComponent } from "../../components/hello.component.ts";
<HelloComponent client:load />
<HelloComponent client:visible helpText="Helping" />Important: In
.mdxfiles the component import must end with the.tssuffix. Otherwise the dynamic import of the component will fail and the component won't be hydrated.
Children passed to an Angular component in an Astro file are projected into the component's ng-content slots. Angular's own select semantics apply, so existing components work unchanged.
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card__header">
<ng-content select="[question]"></ng-content>
</div>
<div class="card__body">
<ng-content></ng-content>
</div>
</div>
`,
})
export class CardComponent {}---
import { CardComponent } from '../components/card.component';
---
<CardComponent client:visible>
<p question>Is content projection cool?</p>
<p>Let's learn about content projection!</p>
</CardComponent>Content that does not match any select is projected into the default <ng-content>. Without a default slot it is dropped, as in Angular. ngProjectAs is honored as well.
Astro's slot attribute is not needed to target a slot. Astro removes the attribute before rendering, so use Angular selectors such as attributes, classes, or element names instead.
Note: for hydrated islands, Astro also emits the projected content in an inert
<template data-astro-template>so it is available on the client. The slot markup therefore appears twice in the HTML response of hydrated components.
Angular components used inside the slot are rendered and hydrated as islands of their own, the same way nested React, Vue or Svelte components behave in Astro. The parent component only receives their markup, so @ContentChild, @ContentChildren, input and output bindings, and dependency injection do not cross the island boundary.
---
import { CardComponent } from '../components/card.component';
import { BadgeComponent } from '../components/badge.component';
---
<!-- Two separate islands: the card cannot query or bind to the badge -->
<CardComponent client:visible>
<BadgeComponent client:visible label="New" />
</CardComponent>For a real parent and child relationship, compose the components in an Angular template and use that component as the island.
import { Component } from '@angular/core';
import { CardComponent } from './card.component';
import { BadgeComponent } from './badge.component';
@Component({
selector: 'app-card-with-badge',
imports: [CardComponent, BadgeComponent],
template: `
<app-card>
<app-badge label="New" />
</app-card>
`,
})
export class CardWithBadgeComponent {}- Only standalone Angular components in version v14.2+ are supported
- Angular components inside projected content are separate islands. Content queries, bindings and dependency injection from the parent component do not reach them, see Components in Projected Content