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.
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()
targetis a fresh element that becomes the view. Zumly gives it thez-viewclass; add your own classes or a size to it if the view should not fill the canvas.propsholds the trigger'sdata-*attributes.data-id="42"arrives asprops.id, a string.onCleanupruns once, when the view is discarded. Unmount there; removing the DOM alone does not.
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
- The views behind the current one stay mounted, so their component state survives a zoom in and out. Nothing renders again.
- If something changed meanwhile (a store, a fetch), repaint in
onActivate(fn), which runs each time the view becomes current again. - Siblings you leave are discarded, unless
lateralNav: { keepAlive: true }keeps them while you stay at that level.
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
- Use
[attr.data-id]on triggers: a plain[data-id]binding sets a property, not the attribute Zumly reads. - Do not render another
z-viewinsidetarget:targetalready is the view. Zumly warns in the console if you do. - Views are plain DOM, not Shadow DOM: your global styles apply inside them.
- Zumly ships its own types;
ViewFunctionandViewContexttype the view context.
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