Skip to content

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.

Terminal window
pnpm add @symbiote-native/angular react-native @angular/core

react-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.

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
@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.

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.

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();
}
}
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.

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.

@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);

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.

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.

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.

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.