Guide · Angular

Zumly with Angular

A view can be an Angular component. Zumly hands you an empty element; you create the component on it and destroy it when the view goes away.

Live example · AngularOpen ↗

Each planet and the list are Angular components in their own views. Like a few times, open a planet, then come back: the count is still there. Here the templates compile in the browser and run without zone.js; in an app, the Angular CLI compiles them ahead of time.

The pattern

Make the view a function. It receives a context; mount a component in target.

import Zumly from 'zumly'
import 'zumly/style.css'
import { createComponent } from '@angular/core'
import { createApplication } from '@angular/platform-browser'
import { DetailComponent } from './detail.component'

// One Angular application hosts every view
const ng = await createApplication({ providers: [] })

const app = new Zumly({
  mount: '.canvas',
  initialView: 'home',
  views: {
    home: '<div class="z-view"><div class="zoom-me" data-to="detail" data-id="42">Open</div></div>',
    detail: ({ target, props, onCleanup }) => {
      const ref = createComponent(DetailComponent, { environmentInjector: ng.injector, hostElement: target })
      ref.setInput('id', props.id)
      ng.attachView(ref.hostView)   // change detection
      onCleanup(() => ref.destroy())
    }
  }
})
app.init()

A list that opens a detail

Zumly is created once, in the component that owns the canvas, and destroyed with it. The list renders the triggers; any element with zoom-me and data-to works, whoever rendered it.

app.component.ts

import { AfterViewInit, ApplicationRef, Component, ElementRef, EnvironmentInjector,
  OnDestroy, Type, ViewChild, createComponent, inject } from '@angular/core'
import Zumly, { type ViewFunction } from 'zumly'
import { ListComponent } from './list.component'
import { DetailComponent } from './detail.component'

@Component({
  selector: 'app-root',
  standalone: true,
  // The canvas fills its nearest positioned ancestor: give that a height
  template: `<div style="position: relative; height: 100vh"><div #canvas class="zumly-canvas"></div></div>`
})
export class AppComponent implements AfterViewInit, OnDestroy {
  @ViewChild('canvas') canvas!: ElementRef<HTMLElement>
  private appRef = inject(ApplicationRef)
  private injector = inject(EnvironmentInjector)
  private zumly?: Zumly

  // Each view creates its component inside this app, so services are shared
  private mountView (component: Type<unknown>, inputs: (props: Record<string, unknown>) => Record<string, unknown> = () => ({})): ViewFunction {
    return ({ target, props, onCleanup }) => {
      const ref = createComponent(component, { environmentInjector: this.injector, hostElement: target })
      for (const [name, value] of Object.entries(inputs(props))) ref.setInput(name, value)
      this.appRef.attachView(ref.hostView)
      onCleanup(() => ref.destroy())
    }
  }

  ngAfterViewInit () {
    this.zumly = new Zumly({
      mount: this.canvas.nativeElement,
      initialView: 'list',
      views: {
        list: this.mountView(ListComponent),
        detail: this.mountView(DetailComponent, props => ({ id: props.id }))
      }
    })
    this.zumly.init()
  }

  ngOnDestroy () {
    this.zumly?.destroy()
  }
}

list.component.ts

import { Component } from '@angular/core'

@Component({
  selector: 'app-list',
  standalone: true,
  template: `
    <div class="list">
      <h1>Subsystems</h1>
      @for (item of items; track item.id) {
        <div class="zoom-me" data-to="detail" [attr.data-id]="item.id">{{ item.name }}</div>
      }
    </div>`
})
export class ListComponent {
  items = [
    { id: '1', name: 'Engine' },
    { id: '2', name: 'Battery' },
    { id: '3', name: 'Brakes' }
  ]
}

When a view comes back

Sharing services and state

Views are created with your app's EnvironmentInjector, so they get the same services, HttpClient and router as the rest of the app. There is nothing to pass.

Things to know

Next

Where views come from, in step 4. The same pattern for React · Vue · Svelte, and for radial layouts with Orbit.

Open this example ↗ Learn in six steps