# Angular guide

> How to write SymbioteNative apps with the Angular adapter.

<Aside type="caution" title="One deliberate exception">
  Full component parity, the renderer seam, and the native bootstrap are
  implemented and tested against `examples/angular`, and Angular is on the live
  framework switcher on the landing page. Every component event is a real
  `@Output()` `EventEmitter` now, with one **permanent** exception: the
  scroll-family events (`onScroll`, `onScrollBeginDrag`, `onScrollEndDrag`,
  `onMomentumScrollBegin`, `onMomentumScrollEnd`) stay callback `@Input()`s on
  `ScrollView` and the list components forever, because they can carry an
  `Animated.event(...)` marker for native-driven scroll, and `@Output()` can
  only bind a template listener expression, not an arbitrary value. Wrapper
  packages have followed: [Slider](/docs/packages/slider/) ships an Angular
  build too — see the [status page](/docs/project/status/) for the exact line.
</Aside>

The Angular adapter drives the same native Fabric engine as React, Vue, Svelte,
and Solid through a custom `Renderer2`/`RendererFactory2` — Angular's own
framework-agnostic rendering seam. Bootstrap is DOM-less: no
`platform-browser`, no zone.js. Angular renderer operations become engine
mutations, then Fabric commits real native views.

<video
  poster={`${import.meta.env.BASE_URL}/angular-demo.webp`}
  aria-label="Angular driving a native SymbioteNative demo"
  width="260"
  height="564"
  preload="none"
  autoplay
  muted
  loop
  playsinline
>
  <source
    src={`${import.meta.env.BASE_URL}/angular-demo.mp4`}
    type="video/mp4"
  />
  <source
    src={`${import.meta.env.BASE_URL}/angular-demo.webm`}
    type="video/webm"
  />
</video>

_The `examples/angular` canary — the same component surface and engine as
the React, Vue, Svelte, and Solid canaries, driven through `Renderer2` instead._

## Minimal component

```ts
import { Component } from '@angular/core';
import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
import './app.css';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [SYMBIOTE_ELEMENTS],
  template: `
    <view class="root">
      <pressable (press)="increment()">
        <text class="label">Count is {{ count }}</text>
      </pressable>
    </view>
  `,
})
export class App {
  count = 0;

  increment(): void {
    this.count += 1;
  }
}
```

```css
/* app.css */
.root {
  flex: 1;
  align-items: center;
  justify-content: center;
}
.label {
  font-size: 24px;
}
```

`StyleSheet.create` works identically if you'd rather keep style objects
inline instead of a CSS file — see the [Styling guide](/docs/learn/styling/)
for both paths.

## Angular contract

Angular is a standalone-components-only adapter:

- the 19 primitive tags (`view`, `pressable`, `text`, `text-input`, `switch`, …)
  are Angular **element directives**, not components — `SYMBIOTE_ELEMENTS`
  (or an individual `<Name>Element`, e.g. `ViewElement`, `PressableElement`)
  declared in the component's `imports` array is what lets `ngtsc` accept the
  lowercase tag with typed props; templates write the tag directly
  (`<view>`/`<pressable>`/`<text>`), matching every other adapter one-for-one;
- every non-primitive SymbioteNative component (`FlatList`, `SectionList`,
  `VirtualizedList`, `VirtualizedSectionList`, `KeyboardAvoidingView`, …) is
  `standalone: true` and imported directly the same way — there is no
  `NgModule`;
- every component event is a real Angular `@Output()` `EventEmitter` —
  `(press)="onTap($event)"`, `(longPress)="..."`, `(hoverIn)="..."`,
  `(valueChange)="..."`, `(accessibilityAction)="..."`, and so on across
  `Pressable`, `Button`, `Touchable*`, `Switch`, `TextInput`, `Modal`, and
  every other component. An event nobody templates costs nothing: an unbound
  `(longPress)` still skips arming the long-press timer, the same as an
  absent callback prop used to. `TextInput` is the one component with two
  outputs for what React/Vue fold into one callback: `(valueChange)` stays
  text-only (an `EventEmitter` carries one value, and text-only keeps
  `[(value)]` two-way binding working) while `(change)` is a second, separate
  output for the raw native event;
- the one **permanent** exception is the scroll family — `onScroll`,
  `onScrollBeginDrag`, `onScrollEndDrag`, `onMomentumScrollBegin`,
  `onMomentumScrollEnd` stay callback **inputs** (`[onScroll]="handler"`) on
  `ScrollView` and the list components, because they can carry an
  `Animated.event(...)` marker for native-driven scroll and `@Output()` can't
  bind an arbitrary value, only a template listener expression;
- imperative handles use `@ViewChild(..., { read: ElementRef })` — the
  `nativeElement` is the SymbioteNative host node, not a DOM element;
- change detection is zoneless: the adapter runs on Angular's own
  `ApplicationRef.tick()`, wired in via the internal
  `ɵprovideZonelessChangeDetectionInternal()` helper rather than the public
  `provideZonelessChangeDetection()` (which assumes a `platform-browser`
  bootstrap this DOM-less renderer doesn't use), so template updates batch the
  same way Vue's `requestCommit()` does, without zone.js patching Hermes
  globals.

Read the exact API surface in the [Angular API reference](/docs/api/angular/).
