@cerebral/angular
Angular integration for Cerebral state management.
Installation
npm install @cerebral/angular @angular/core @angular/platform-browser
Usage
AppService
AppService
is the core service that creates and exposes the Cerebral controller to your Angular application.
Setup in Angular App
import { ApplicationConfig } from '@angular/core'
import { provideAnimations } from '@angular/platform-browser/animations'
import { provideRouter } from '@angular/router'
import { AppService } from '@cerebral/angular'
import { SomeService } from './some.service'
import { routes } from './app.routes'
export function createCerebralApp(someService: SomeService) {
return new AppService({
state: {
count: 0,
items: []
},
sequences: {
increment: [
({ store, get }) => store.set('count', get(state`count`) + 1)
],
decrement: [({ store, get }) => store.set('count', get(state`count`) - 1)]
},
providers: {
someService
}
})
}
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideAnimations(),
{
provide: AppService,
useFactory: createCerebralApp,
deps: [SomeService]
}
]
}
Components
Using the connect
decorator
The connect
decorator connects Cerebral state and sequences to your components:
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
Inject
} from '@angular/core'
import { state, sequences } from 'cerebral'
import { connect, AppService, CerebralComponent } from '@cerebral/angular'
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>
<h2>Count: {{ count }}</h2>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
@connect({
count: state`count`,
increment: sequences`increment`,
decrement: sequences`decrement`
})
export class CounterComponent extends CerebralComponent {
count!: number
increment!: () => void
decrement!: () => void
constructor(
@Inject(ChangeDetectorRef) cdr: ChangeDetectorRef,
@Inject(AppService) app: AppService
) {
super(cdr, app)
}
}
Working with Parent/Child Components
For parent-child component relationships, ensure child components are properly imported:
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
Inject
} from '@angular/core'
import { state, sequences } from 'cerebral'
import { connect, AppService, CerebralComponent } from '@cerebral/angular'
import { ChildComponent } from './child.component'
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<div>
<h1>Parent</h1>
<app-child [data]="items"></app-child>
<button (click)="addItem()">Add Item</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
@connect({
items: state`items`,
addItem: sequences`addItem`
})
export class ParentComponent extends CerebralComponent {
items!: any[]
addItem!: () => void
constructor(
@Inject(ChangeDetectorRef) cdr: ChangeDetectorRef,
@Inject(AppService) app: AppService
) {
super(cdr, app)
}
}
Advanced Features
Using Angular Services in Cerebral Sequences
You can inject Angular services into your Cerebral sequences using providers:
import { HttpClient } from '@angular/common/http'
import { ApplicationConfig } from '@angular/core'
import { AppService } from '@cerebral/angular'
export function createCerebralApp(httpClient: HttpClient) {
return new AppService({
state: {
data: null,
loading: false,
error: null
},
sequences: {
fetchData: [
({ store }) => store.set('loading', true),
async ({ http }) => {
try {
const response = await http.get('/api/data').toPromise()
return { response }
} catch (error) {
return { error }
}
},
({ store, props }) => {
if (props.error) {
store.set('error', props.error)
} else {
store.set('data', props.response)
}
store.set('loading', false)
}
]
},
providers: {
http: httpClient
}
})
}
export const appConfig: ApplicationConfig = {
providers: [
{
provide: AppService,
useFactory: createCerebralApp,
deps: [HttpClient]
}
]
}
Using Computed Values
Cerebral uses functions to compute derived state values:
import { state } from 'cerebral'
import { AppService } from '@cerebral/angular'
type Item = { id: number; name: string }
type AppState = {
items: Item[]
filter: string
filteredItems?: Item[]
}
const filteredItems = (get: any) => {
const items = get(state`items`)
const filter = get(state`filter`)
return items.filter((item: Item) =>
filter ? item.name.includes(filter) : true
)
}
const app = new AppService({
state: {
items: [],
filter: '',
filteredItems
} as AppState,
sequences: {
}
})
@Component({
standalone: true
})
@connect({
filteredItems: state`filteredItems`
})
export class ListComponent extends CerebralComponent {
filteredItems!: Item[]
}
Computed values automatically track their dependencies and only recalculate when necessary, improving performance for derived state.
Troubleshooting
Common Issues
-
State updates not reflected in components
- Make sure your component uses
ChangeDetectionStrategy.OnPush
- Check that you've extended
CerebralComponent
- Verify you're using
@Inject()
for dependency injection
-
TypeScript errors with connected properties
- Always declare connected properties with the correct type and
!
assertion
- Example:
count!: number;
-
Angular directives not working
- When using standalone components, remember to import needed directives
- Example:
imports: [NgIf, NgFor, CommonModule]
-
Component not updating after state change
- Check if you need to call
app.flush()
after sequence execution in tests
- In components, sequences are automatically flushed