Scroll view

<scroll-view> wraps its content in an internal container the way React Native's own ScrollView.js does, because native scroll views have no equivalent to contentContainerStyle of their own. contentContainerStyle styles that inner container - padding and gaps on the scrollable content go there, not on [style], which styles the scroll view's own frame.

Import
import { ScrollView, RefreshControl } from '@ng-native/components';
Template
<scroll-view><refresh-control> once they are in the component's imports
<scroll-view #list [contentContainerStyle]="{ padding: 16, gap: 12 }">
  @for (item of items(); track item.id) {
  <text>{{ item.label }}</text>
  }
</scroll-view>

Horizontal scrolling

horizontal changes the scroll axis, and it changes two things to do it: the scroll view's own flex direction and the content container's. Setting only one leaves the content stretched to the scroll view's width with nothing to scroll to, which looks exactly like a horizontal scroll view that simply does not have enough in it to scroll - horizontal handles both, so this only matters if something is overriding the content container's own layout.

Keyboard taps

keyboardShouldPersistTaps controls what a tap elsewhere in the scroll view does while a <text-input> has focus: 'never' (the default) dismisses the keyboard immediately and nothing else happens with the tap, 'handled' lets whatever was tapped handle it first and only dismisses if nothing did, and 'always' never dismisses at all.

Sticky headers

stickyHeaderIndices pins children of the content to the top while the rest scroll under them, each until the next sticky child pushes it off. React Native does this in JavaScript rather than natively, by translating the child against the scroll offset, and so does this; the one difference is that RN's translation runs on the native animation driver, where this one is written from the scroll event, so a pinned header can trail a fast fling by a frame.

Methods

scrollTo({ x, y, animated }), scrollToEnd({ animated }), flashScrollIndicators() and, on iOS, zoomToRect(rect, animated) are available as methods through a template reference.

A scroll handler and change detection

A (scroll) handler runs change detection for its component on every scroll event, and Angular checks every row an @for in that component's template has rendered, whatever the handler changed. A header that fades as the page scrolls, over a thousand rows in the same template, re-checks all thousand rows every frame. Put the list in a component of its own and the same event checks one input instead: with a thousand rows, a scroll event costs about 0.05ms of JavaScript rather than about 2ms.

<scroll-view (scroll)="fadeHeader($event)">
  <view [style]="{ opacity: headerOpacity() }"><text>Inbox</text></view>
  <app-message-rows [messages]="messages()" />
</scroll-view>
scroll-viewcomponent@ng-native/components
import { ScrollView } from '@ng-native/components';and add ScrollView to the component's imports

A scrolling container. Commits as RCTScrollView , with the children inside a content view.

Inputs

contentContainerStyle
Record<string, unknown>

Styles for the view that holds the children, e.g. padding and gap.

horizontal
unknown

Lay children out in a row and scroll sideways.

contentOffset
Point

Where the content starts, before the user scrolls.

maintainVisibleContentPosition
{ readonly minIndexForVisible: number; readonly autoscrollToTopThreshold?: number; }

Keep what is on screen still when content is added above it.

keyboardShouldPersistTaps
KeyboardShouldPersistTaps = 'never'

What a tap does with the keyboard up. See the class note. Defaults to never .

nestedScrollEnabled
unknown

Android: cooperate with a scrolling ancestor.

stickyHeaderIndices
readonly number[] = []

Children of the content, by position, that stick to the leading edge while it scrolls.

Outputs

contentSizeChange
Size

The content view's size changed; the value is its new width and height.

Methods

scrollTo(options: { x?: number; y?: number; animated?: boolean }): void

Scroll to a position.

flashScrollIndicators(): void

Briefly show the scroll indicators, to hint that there is more.

zoomToRect(rect: Rect, animated = true): void

iOS: zoom so that a rectangle of the content fills the viewport.

Refresh control

<refresh-control> is a pull-to-refresh spinner, used as a direct child of a <scroll-view> or <virtual-list>. refreshing is a two-way model: native shows the spinner the instant the user pulls, before the app has said anything, and the app is expected to set refreshing to true when the (refresh) output fires and back to false once the work is done. If an app never clears it, the spinner keeps spinning indefinitely - there is no timeout.

<scroll-view>
  <refresh-control [(refreshing)]="loading" (refresh)="reload()" />
  @for (item of items(); track item.id) {
  <text>{{ item.label }}</text>
  }
</scroll-view>

iOS also reads tintColor, title and titleColor for a label under the spinner. Android reads colors (the colours the spinner cycles through), progressBackgroundColor, size ('default' or 'large') and enabled (whether pulling does anything at all, default true). progressViewOffset sets how far from the top the spinner sits, on both platforms.

On Android the native swipe layout has to be the scroll view's parent, not its child, or a pull does nothing. It is still written as a child, as on iOS, and moved into place: the committed tree has AndroidSwipeRefreshLayout where the scroll view was, with the scroll view inside it. The layout half of the scroll view's inline style (size, margins, flex, position, transform) moves to the swipe layout, the way React Native's ScrollView.js splits it, and the scroll view keeps the rest. Layout that comes from a class stays on the scroll view.

refresh-controldirective@ng-native/components
import { RefreshControl } from '@ng-native/components';and add RefreshControl to the component's imports

Pull to refresh, as a child of a scroll view. Commits as PullToRefreshView on iOS and AndroidSwipeRefreshLayout on Android.

Inputs

progressViewOffset
unknown

How far from the top the spinner sits.

tintColor
string

iOS: the spinner's colour.

title
string

iOS: a label under the spinner.

titleColor
string

iOS: the label's colour.

colors
readonly string[]

Android: the colours the spinner cycles through.

enabled
unknown

Android: whether pulling does anything. Defaults to true.

progressBackgroundColor
string

Android: the spinner's background.

size
'default' | 'large'

Android: the spinner's size.

Two-way

refreshing
boolean = false

Whether the spinner shows. Two-way: [(refreshing)] .

Angular components as native iOS and Android views, built and shipped with Expo.

An alpha. MIT licensed. Sponsor its development.

An independent project, not affiliated with or endorsed by Google, the Angular team or Expo. Angular is a trademark of Google LLC.