@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 

// app.config.ts
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'

// Create a Cerebral app factory
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: {
      // Provide Angular services to Cerebral
      someService
    }
  })
}

// Provide AppService in your app config
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, // In Angular 19+ is this the default
  template: `
    <div>
      <h2>Count: {{ count }}</h2>
      <button (click)="increment()">+</button>
      <button (click)="decrement()">-</button>
    </div>
  `,
  // OnPush change detection is required for optimal performance
  changeDetection: ChangeDetectionStrategy.OnPush
})
@connect({
  count: state`count`,
  increment: sequences`increment`,
  decrement: sequences`decrement`
})
export class CounterComponent extends CerebralComponent {
  // Properly declare connected properties with TypeScript
  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>
  `,
  // Change detection needs to be set to OnPush
  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:

// app.config.ts
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'

// 1. Define types for better type safety
type Item = { id: number; name: string }
type AppState = {
  items: Item[]
  filter: string
  filteredItems?: Item[]
}

// 2. Define the computed function
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
  )
}

// 3. Create your app with computed properties
const app = new AppService({
  state: {
    items: [],
    filter: '',
    filteredItems // Attach computed to state
  } as AppState,
  sequences: {
    // Define your sequences here
  }
})

// 4. Use in component
@Component({
  standalone: true
  // ...component config
})
@connect({
  filteredItems: state`filteredItems` // Access via state tag or proxy
})
export class ListComponent extends CerebralComponent {
  // Declare with proper type
  filteredItems!: Item[]
}

Computed values automatically track their dependencies and only recalculate when necessary, improving performance for derived state.

Troubleshooting 

Common Issues 

  1. 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
  2. TypeScript errors with connected properties

    • Always declare connected properties with the correct type and ! assertion
    • Example: count!: number;
  3. Angular directives not working

    • When using standalone components, remember to import needed directives
    • Example: imports: [NgIf, NgFor, CommonModule]
  4. 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