Saltar al contenido principal

Analog SFCs

Note:

This file format and API is experimental, is a community-driven initiative, and is not an officially proposed change to Angular. Use it at your own risk.

The .analog file extension denotes a new file format for Single File Components (SFCs) that aims to simplify the authoring experience and provide Angular-compatible components and directives.

Together, it combines:

  • Colocated template, script, and style tags
  • Use of Angular Signal APIs without decorators
  • Performance-first defaults (OnPush change detection, no accesss to ngDoCheck, etc.)

Usage

To use the Analog SFC, you need to use the Analog Vite plugin or the Analog Astro plugin with an additional flag to enable its usage:

import { defineConfig } from 'vite';
import analog from '@analogjs/platform';

export default defineConfig({
// ...
plugins: [
analog({
vite: {
// Required to use the Analog SFC format
experimental: {
supportAnalogFormat: true,
},
},
}),
],
});

You must also uncomment the type information in the src/vite-env.d.ts file. This is temporary while the Analog SFC is experimental.

Additional Configuration

If you are using .analog files outside a project's root you need to specify all paths of .analog files using globs, like so:

export default defineConfig(({ mode }) => ({
// ...
plugins: [
analog({
vite: {
experimental: {
supportAnalogFormat: {
include: ['/libs/shared/ui/**/*', '/libs/some-lib/ui/**/*'],
},
},
},
}),
],
}));

IDE Support

To support syntax highlighting and other IDE functionality with .analog files, you need to install an extension to support the format for:

Support for VSCode is coming! Please see this issue for more details.

Authoring an SFC

Here's a demonstration of the Analog format building a simple counter:

<script lang="ts">
// counter.analog
import { signal } from '@angular/core';

const count = signal(0);

function add() {
count.set(count() + 1);
}
</script>

<template>
<div class="container">
<button (click)="add()">{{count()}}</button>
</div>
</template>

<style>
.container {
display: flex;
justify-content: center;
}

button {
font-size: 2rem;
padding: 1rem 2rem;
border-radius: 0.5rem;
background-color: #f0f0f0;
border: 1px solid #ccc;
}
</style>

See the defineMetadata section for adding additional component metadata.

Metadata

While class decorators are used to add metadata to a component or directive in the traditional Angular authoring methods, they're replaced in the Analog format with the defineMetadata global function:

defineMetadata({
host: { class: 'block articles-toggle' },
});

This supports all of the decorator properties of @Component or @Directive with a few exceptions.

Disallowed Metadata Properties

The following properties are not allowed on the metadata fields:

  • template: Use the SFC <template> or defineMetadata.templateUrl instead
  • standalone: Always set to true
  • changeDetection: Always set to OnPush
  • styles: Use the SFC <style> tag
  • outputs: Use the output signal API instead
  • inputs: Use the input signal API instead

Host Metadata

As shown above, you can add host metadata to your component using the host field:

defineMetadata({
host: { class: 'block articles-toggle' },
});

Another way to add host metadata is to use the <template> tag

<template class="block articles-toggle"></template>

You can also have Property Binding and Event Binding in the <template> tag:

<script lang="ts">
import { signal } from '@angular/core';

const bg = signal('black');

function handleClick() {}
</script>

<template [style.backgroundColor]="bg()" (click)="handleClick()"></template>

Using an External Template and Styles

If you like the developer experience of Analog's <script> to build your logic, but don't want your template and styling in the same file, you can break those out to their own files using:

  • templateUrl
  • styleUrl
  • styleUrls

In defineMetadata, like so:

<script lang="ts">
defineMetadata({
selector: 'app-root',
templateUrl: './test.html',
styleUrl: './test.css',
});

onInit(() => {
alert('Hello World');
});
</script>

Using Components

When using the Analog format, you do not need to explicitly export anything; the component is the default export of the .analog file:

import { bootstrapApplication } from '@angular/platform-browser';
import App from './app/app.analog';
import { appConfig } from './app/app.config';

bootstrapApplication(App, appConfig).catch((err) => console.error(err));

To use the components you need to add them to your imports (alternatively, you can use import attributes as explained in the following section):

<!-- layout.analog -->
<script lang="ts">
import { inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { AuthStore } from '../shared-data-access-auth/auth.store';
import LayoutFooter from '../ui-layout/layout-footer.analog';
import LayoutHeader from '../ui-layout/layout-header.analog';

defineMetadata({ imports: [RouterOutlet, LayoutFooter, LayoutHeader] });

const authStore = inject(AuthStore);
</script>

<template>
<LayoutHeader
[isAuthenticated]="authStore.isAuthenticated()"
[username]="authStore.username()"
/>
<router-outlet />
<LayoutFooter />
</template>

A component's selector is not determined by the imported name, but rather determined by the name of the file. If you change your imported name to:

<script lang="ts">
import LayoutHeaderHeading from '../ui-layout/layout-header.analog';
</script>

<template>
<LayoutHeaderHeading />
</template>

It would not work as expected. To solve this, you'll need the name of the default import to match the file name of the .analog file.

An official solution for this problem, from Angular, has been hinted by the Angular team and may come in a future version of Angular.

Import Attributes

To avoid the necessity of manually adding components to the imports metadata, you can also use import attributes

<script lang="ts">
import YourComponent from './your-component.analog' with { analog: 'imports' };
</script>

Using the import attribute method adds the component to your metadata's imports and can be used for other imports you want to add to the metadata, like so:

<script lang="ts">
// This adds to the `providers` array in your metadata
import { MyService } from './my.service' with { analog: 'providers' };
// This adds the `ExternalEnum` field to your component's constructor so that you can use it in your template
import { ExternalEnum } from './external.model' with { analog: 'exposes' };
// ...
</script>

Lifecycle Methods

Currently, only two lifecycle methods from Angular are available to .analog SFCs:

  • onInit
  • onDestroy

You use these lifecycle methods like so:

<!-- app.analog -->
<script lang="ts">
onInit(() => {
console.log('I am mounting');
});

onDestroy(() => {
console.log('I am unmounting');
});
</script>

This encourages best practices when using Angular signals since many of the other lifecycle methods can introduce performance issues or are easily replaced with other APIs.

Inputs and Outputs

To add inputs and outputs to an Analog component, you use the new Angular signals API.

Let's explore what that looks like in practical terms.

Inputs

Inputs can be added to a component or directive in the Analog format using the new input signal API:

const namedInput = input();

This adds an input with the name of namedInput that can be used in the template like so:

<template>
<SomeComponent [namedInput]="someValue" />
</template>

Outputs

Outputs are added in the Analog format like so:

<script lang="ts">
// my-item.analog
const itemSelected = output();

function selectItem(id: number) {
itemSelected.emit(id);
}
</script>

And can be used in the template like so:

<template>
<h2>My Item</h2>

<button (click)="selectItem(1)">Select</button>
</template>

The output is consumed outside the component

<script lang="ts">
function doSomething(id: number) {
console.log('Item Selected' + id);
}
</script>

<template>
<MyItem (itemSelected)="doSomething($event)" />
</template>

Models

Models are added in the Analog format like so:

<script lang="ts">
// some-component.analog
const myValue = model();
</script>

And can be used in the template like so:

<template>
<SomeComponent [myValue]="val" (myValueChange)="doSomething($event)" />
</template>

Authoring Directives

Any .analog file without a <template> tag or usage of templateUrl in the defineMetadata function are treated as Angular Directives.

Here's an example of a directive that focuses an input and has two lifecycle methods:

<script lang="ts">
import { inject, ElementRef, afterNextRender, effect } from '@angular/core';

defineMetadata({
selector: 'input[directive]',
});

const elRef = inject(ElementRef);

afterNextRender(() => {
elRef.nativeElement.focus();
});

onInit(() => {
console.log('init code');
});

effect(() => {
console.log('just some effect');
});
</script>

Authoring SFCs using Markdown

If you'd like to write Markdown as your template rather than Angular-enhanced HTML, you can add lang="md" to your <template> tag in an .analog file:

<template lang="md"> # Hello World </template>

This can be used in combination with the other SFC tags: <script> and <style>.

Using Components in Markdown

lang="md" templates in Analog also support Analog and Angular components in their templates:

<script lang="ts">
import Hello from './hello.analog' with { analog: 'imports' };
</script>

<template lang="md">
# Greeting

<Hello />

> You might want to say "Hello" back!
</template>

Using SFCs as Interactive Content Files

You can also create content files with frontmatter within the src/content folder using the Analog SFC format by using the .agx extension instead of .analog. This provides an experience similar to MDX for authoring content:

---
title: Hello World
slug: 'hello'
---

<script lang="ts">
// src/content/post.agx
const name = 'Analog';
</script>

<template lang="md"> My First Post on {{ name }} </template>

Just like with .md files you can dynamically search and filter .agx content files using injectContentFiles and you can render content within a component using injectContent and the MarkdownComponent:

<script lang="ts">
// posts.[slug].page.analog
import { injectContent } from '@analogjs/content';
import { MarkdownComponent } from '@analogjs/content' with { analog: 'imports' }
import { toSignal } from '@angular/core/rxjs-interop';

import { PostAttributes } from './models';

// inject content file based on current slug
const post$ = injectContent<PostAttributes>();
const post = toSignal(post$);
</script>

<template>
@if(post()){
<analog-markdown [content]="post().content"></analog-markdown>
}
</template>

Limitations

There are a few limitations to the Analog format:

  • You cannot use decorator APIs (@Input, @Component, @ViewChild)
  • You must have lang="ts" present in the <script> tag