Angular API
Import app-facing APIs from @symbiote-native/angular:
import { Pressable, ScrollView, StyleSheet, Text, TextInput, View } from '@symbiote-native/angular';Every SymbioteNative component is a standalone: true Angular component — add it to
the consuming component’s imports array, there is no NgModule to register.
Installation
Section titled “Installation”pnpm add @symbiote-native/angular react-native @angular/corereact-native and @angular/core (>=20, for stable zoneless change
detection) stay your app’s own top-level dependencies — react-native is the
Metro version anchor, so it has to be pinned at the app root.
@symbiote-native/engine is a peer dependency and installs alongside. The AOT
pipeline (ngc --watch alongside Metro) and the Metro config still have to be
wired by hand; examples/angular is the reference.
Component shape
Section titled “Component shape”| Concern | Angular API |
|---|---|
| Event callbacks | Real @Output() EventEmitters everywhere ((press), (longPress), (hoverIn), (valueChange), (accessibilityAction), …), except the scroll family (onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd), which stays a plain callback input ([onScroll]) permanently — it must accept an Animated.event(...) marker, not just a template listener. TextInput alone keeps a second output, (change), for the raw native event alongside text-only (valueChange) |
| Children | <ng-content></ng-content> (projected content) |
| Selectors | Dual: PascalCase alias + symbiote-*, e.g. Pressable, symbiote-pressable |
| Refs | @ViewChild('host', { read: ElementRef }), .nativeElement is the SymbioteNative host node |
| Styles | [style]="styles.root" with React Native-style objects |
Common events
Section titled “Common events”@Component({ standalone: true, imports: [Pressable, Switch, TextInput, View], template: ` <Pressable (press)="onPress($event)" (longPress)="onLongPress($event)" /> <Switch [value]="enabled" (valueChange)="setEnabled($event)" /> <TextInput [value]="text" (valueChange)="setText($event)" (change)="onChange($event)" /> <View (layout)="onLayout($event)" /> `,})Pressable, Button, and TouchableOpacity/TouchableHighlight/
TouchableWithoutFeedback/TouchableNativeFeedback expose press, pressIn,
pressOut, pressMove, longPress, hoverIn, and hoverOut (Button only
exposes press) as EventEmitter<ISymbioteEvent> — bind them with Angular’s
own event syntax, not a property binding. Every other component’s events
(Switch’s (valueChange), TextInput’s (valueChange)/(change), the
accessibility callbacks on the components above, …) are EventEmitters the
same way, driven by the exact same handler shape the engine and the shared
@symbiote-native/components state machines already define (IPressHandler,
(value: boolean) => void, …). TextInput is the one component with two
outputs where React/Vue fold their payload into one callback argument list:
(valueChange) stays text-only, since an EventEmitter carries exactly 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 across every component is the scroll-family events
(onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin,
onMomentumScrollEnd) on ScrollView and the list components — they stay
callback @Input()s ([onScroll]="handler") because they can carry an
Animated.event(...) marker for native-driven scroll, and @Output() only
binds a template listener expression, never an arbitrary value.
Forms (@angular/forms)
Section titled “Forms (@angular/forms)”TextInput and Switch each implement ControlValueAccessor directly — the
same shape @angular/material‘s MatInput/MatSelect use — so
@angular/forms’ own directives work with no extra import beyond the one
you’d already reach for on a native <input>:
import { Component } from '@angular/core';import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';import { Switch, TextInput } from '@symbiote-native/angular';
@Component({ standalone: true, imports: [ReactiveFormsModule, Switch, TextInput], template: ` <TextInput [formControl]="email" placeholder="email" /> <Switch [formControl]="notify" /> `,})export class Example { readonly email = new FormControl('', [Validators.required, Validators.email]); readonly notify = new FormControl(false);}[(ngModel)] works the same way with FormsModule. Only value-bearing
components implement ControlValueAccessor — [formControl] on any other
component is a plain unbound @Input, not a forms integration.
Slider is the same story, one level down in the
package that ships it — see its own Angular usage. React and Vue have no
equivalent (their controlled-value idiom is plain value/onChangeText and
v-model) — see their own API references.
Refs and handles
Section titled “Refs and handles”import { Component, ViewChild, type ElementRef } from '@angular/core';import { TextInput, type ITextInputHandle } from '@symbiote-native/angular';
@Component({ standalone: true, imports: [TextInput], template: `<TextInput #input [value]="''" (valueChange)="onChangeText($event)" />`,})export class Example { @ViewChild('input', { read: ElementRef }) input?: ElementRef<ITextInputHandle>;
focus(): void { this.input?.nativeElement.focus(); }}Portals
Section titled “Portals”import { Component, signal } from '@angular/core';import { PortalDirective, PortalOutletDirective, Text, View } from '@symbiote-native/angular';
@Component({ standalone: true, imports: [View, Text, PortalDirective, PortalOutletDirective], template: ` <View portalOutlet #overlayHost="portalOutlet" /> @if (toastVisible()) { <View *portal="overlayHost"> <Text>Rendered under overlayHost, not here</Text> </View> } `,})export class Example { readonly toastVisible = signal(false);}*portal="overlayHost" renders its host element into whichever
PortalOutletDirective overlayHost refers to — the same primitive as
React’s createPortal and Vue’s Teleport. overlayHost comes from marking
the destination with portalOutlet and exporting it to a template variable
(#overlayHost="portalOutlet"), the same #form="ngForm" idiom Angular’s own
forms directives use — which also replaces the runtime isSymbioteNode guard
React/Vue need: strictTemplates rejects anything but a real
PortalOutletDirective here at compile time, so there’s nothing left to
validate at runtime. There is no @angular/cdk dependency behind this: it
creates the embedded view directly inside the destination’s own
ViewContainerRef rather than moving already-created nodes, which would
desync Angular’s own view bookkeeping. Same scope as React/Vue: the
target must live in the same surface as the *portal call site.
Cross-surface content (createTunnel)
Section titled “Cross-surface content (createTunnel)”import { Component, signal } from '@angular/core';import { createTunnel, Text, TunnelInDirective, TunnelOut, View } from '@symbiote-native/angular';
const overlayTunnel = createTunnel(); // module-level singleton, importable from both surfaces
@Component({ standalone: true, imports: [View, Text, TunnelInDirective, TunnelOut], template: ` <!-- inside the surface that should paint the content --> <tunnel-out [tunnel]="tunnel" />
<!-- inside any other component, in any surface --> @if (toastVisible()) { <View *tunnelIn="tunnel"> <Text>Toast</Text> </View> } `,})export class Example { readonly tunnel = overlayTunnel; readonly toastVisible = signal(false);}*portal only reaches a target in the same surface. createTunnel is for
two independently mount()-ed surfaces that share no Fabric tree at all.
Angular can’t synthesize a fresh component per createTunnel() call the way
React/Vue do — there’s no runtime JIT under Metro/Hermes — so createTunnel()
here returns a plain signal-backed store, and TunnelInDirective
(*tunnelIn="tunnel") / TunnelOut (<tunnel-out [tunnel]="tunnel" />) are
one static, pre-authored, AOT-compilable pair parameterized by that store —
the same relationship the list directives have to their per-cell templates.
React and Vue have the same primitive — see their own API references.
App entry point (AppRegistry)
Section titled “App entry point (AppRegistry)”@symbiote-native/angular exports the same AppRegistry/setHostRegistrar entry
point described in the
Core API. One Angular-specific
detail: Angular has no runtime template synthesis (no JIT under AOT/Metro), so
a component passed to setWrapperComponentProvider must be a pre-authored
standalone @Component whose template renders <ng-content> — the Angular
idiom for “render my children” — rather than a closure built at call time:
import { Component } from '@angular/core';
@Component({ selector: 'theme-wrapper', standalone: true, template: `<ng-content></ng-content>`,})export class ThemeWrapper {}import { AppRegistry } from '@symbiote-native/angular';import { ThemeWrapper } from './theme-wrapper';
AppRegistry.setWrapperComponentProvider(() => ThemeWrapper);AppRegistry.registerComponent('MyApp', () => App);Runtime modules and services
Section titled “Runtime modules and services”The adapter re-exports the same stable runtime utilities the React and Vue adapters expose, so app code keeps one import root:
import { Alert, Dimensions, Platform, StyleSheet } from '@symbiote-native/angular';Two runtime reads are DI-injectable services instead of hooks/composables — the Angular-idiomatic shape for reactive lifecycle state:
import { inject } from '@angular/core';import { ColorSchemeService, WindowDimensionsService } from '@symbiote-native/angular';
const colorScheme = inject(ColorSchemeService);const dimensions = inject(WindowDimensionsService);The rest of the runtime-module surface re-exports the same way as React/Vue:
PixelRatio, PlatformColor, DynamicColorIOS, Share, Linking,
Keyboard, Vibration, ActionSheetIOS, BackHandler, ToastAndroid,
PermissionsAndroid, AccessibilityInfo, I18nManager, Settings,
LayoutAnimation, InteractionManager, StatusBar, and findNodeHandle.
On Android, Keyboard and Settings do nothing until you also install
@symbiote-native/android:
keyboardDidShow/keyboardDidHide never fire, because Android’s only stock
keyboard-event source is ReactRootView’s layout listener and the bridgeless
surface SymbioteNative mounts never triggers it, and Settings reads back null,
because RN’s Settings has no stock Android implementation at all. That package
supplies both native modules (KeyboardObserver, SettingsManager), is picked
up by Gradle autolinking, and exports nothing from JS — installing it is the
whole integration.
Animations and gestures
Section titled “Animations and gestures”Animated (both the JS and native driver, plus the AnimatedView/
AnimatedText/AnimatedImage/AnimatedScrollView/AnimatedFlatList/
AnimatedSectionList named exports AOT requires) and PanResponder are
re-exported from @symbiote-native/angular — see the
Animations guide for the full surface and why the
named exports exist.
Change detection
Section titled “Change detection”Bootstrap runs Angular’s own zoneless change detection — ApplicationRef.tick(),
scheduled by Angular’s ChangeDetectionSchedulerImpl — wired in through the
internal ɵprovideZonelessChangeDetectionInternal() helper rather than the
public provideZonelessChangeDetection(), which assumes a platform-browser
bootstrap this DOM-less renderer does not use.
A native event or markForCheck() still walks dirty flags up to every ancestor
to the root, same as any zoneless Angular app — an unrelated press re-running
the whole tree isn’t a SymbioteNative quirk, it’s why demo screens are split into
real child components instead of @if/@for blocks, which always re-execute
with their containing view.
Boundary
Section titled “Boundary”Do not pass React component packages to the Angular adapter. A third-party React Native package that ships a JavaScript React component still uses the React dispatcher internally. Non-React adapters need native-view wrappers instead — see the Slider package for the reference implementation, shipping on React, Vue, and Angular alike.