Skip to main content
Neural Nativev0.1.0-beta.1Documentation

Import

component.ts
import { NeuDataViewComponent } from '@neural-ui/native/data-view';

Examples

Import ExampleComponent into its parent and place <app-example /> inside your application’s root neu-theme. The preview supplies the device frame and safe areas; use the root setup from Installation in your app.

Basic

Use an Angular template for each row. This people list combines Avatar and text while Data View retains search, sorting and selection.

View and copy the complete example
basic.component.ts
import { Component, signal, computed } from '@angular/core';
import { NeuAvatarComponent } from '@neural-ui/native/avatar';
import { NeuDataViewComponent } from '@neural-ui/native/data-view';
import { View, Text } from '@ng-native/components';
import type { NeuOption } from '@neural-ui/native';

@Component({
  selector: 'app-example',
  imports: [NeuAvatarComponent, NeuDataViewComponent, View, Text],
  template: `
    <view class="people-header">
      <view class="people-heading">
        <text class="people-title">Team members</text>
        <text class="people-subtitle">Product workspace</text>
      </view>
      <view class="people-count"><text class="people-count-label">{{ people().length }}</text></view>
    </view>
    <ng-template #person let-item>
      <view class="person-row" [class.person-row-selected]="selected() === item.id">
        <neu-avatar [label]="item.label" size="md" />
        <view class="person-copy">
          <text class="person-name">{{ item.label }}</text>
          <text class="person-description">{{ item.description }}</text>
        </view>
        <view class="person-end" [accessibilityElementsHidden]="true" importantForAccessibility="no-hide-descendants">
          @if (selected() === item.id) {
            <view class="person-selected-mark"><text class="person-check">✓</text></view>
          } @else {
            <view class="person-chevron" />
          }
        </view>
      </view>
    </ng-template>
    <neu-data-view [items]="people()" [itemTemplate]="person" searchLabel="Search people" [(query)]="query" [(descending)]="descending" (selected)="selected.set($event)" (refresh)="refreshPeople()" />
    <view class="people-footer">
      <text class="people-status" accessibilityLiveRegion="polite">{{ selected() ? selectedName() + ' selected' : 'Select a team member' }}</text>
      @if (refreshed()) { <text class="people-updated" accessibilityLiveRegion="polite">Up to date</text> }
    </view>
  `,
  styles: `
.actions { gap: 12px; align-items: flex-start; } text { color: var(--neu-text, #172033); font-size: 16px; }

.people-header { flex-direction: row; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.people-heading { flex: 1; min-width: 0; gap: 4px; }
.people-title { font-size: 22px; font-weight: 700; }
.people-subtitle { font-size: 13px; color: var(--neu-muted, #596477); }
.people-count { min-width: 28px; padding: 4px 9px; align-items: center; border-radius: 999px; background-color: var(--neu-tonal, #eff6ff); }
.people-count-label { font-size: 12px; font-weight: 600; color: var(--neu-accent, #006ee6); }
.person-row { flex-direction: row; align-items: center; gap: 12px; padding: 12px; border-width: 1px; border-color: var(--neu-border, #a5afbf); border-radius: 12px; background-color: var(--neu-surface, #ffffff); box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); transition: background-color 180ms ease-out, border-color 180ms ease-out; }
@media (prefers-reduced-motion: reduce) { .person-row { transition: none; } }
.person-row-selected { border-color: var(--neu-accent, #006ee6); background-color: var(--neu-tonal, #eff6ff); }
.person-copy { flex: 1; min-width: 0; gap: 4px; }
.person-name { font-size: 15px; font-weight: 600; }
.person-description { font-size: 13px; color: var(--neu-muted, #596477); }
.person-end { width: 20px; height: 24px; align-items: center; justify-content: center; }
.person-chevron { width: 6px; height: 6px; border-top-width: 1.5px; border-right-width: 1.5px; border-color: var(--neu-muted, #596477); transform: rotate(45deg); opacity: 0.6; }
.person-selected-mark { width: 20px; height: 20px; align-items: center; justify-content: center; border-radius: 10px; background-color: var(--neu-accent, #006ee6); }
.person-check { font-size: 12px; font-weight: 600; color: var(--neu-on-accent, #ffffff); }
.people-footer { flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; min-height: 20px; margin-top: 12px; }
.people-status { font-size: 12px; color: var(--neu-muted, #596477); }
.people-updated { font-size: 12px; color: var(--neu-accent, #006ee6); }

  `,
})
export class ExampleComponent {
  readonly initialPeople: readonly NeuOption[] = [
    {
      "id": "ada",
      "label": "Ada Lovelace",
      "description": "Product designer"
    },
    {
      "id": "grace",
      "label": "Grace Hopper",
      "description": "Mobile engineer"
    },
    {
      "id": "linus",
      "label": "Linus Torvalds",
      "description": "Platform engineer"
    }
  ];
  readonly people = signal(this.initialPeople);
  readonly query = signal('');
  readonly descending = signal(false);
  readonly selected = signal('');
  readonly refreshed = signal(false);
  readonly selectedName = computed(() => this.people().find(person => person.id === this.selected())?.label ?? 'None yet');
  refreshPeople(): void {
    this.people.set([...this.initialPeople]);
    this.refreshed.set(true);
  }
}

Declarative item template

Use the typed declarative template instead of passing itemTemplate. Its index follows the filtered and sorted list.

View and copy the complete example
declarative-item-template.component.ts
import { Component, signal } from '@angular/core';
import { NeuAvatarComponent } from '@neural-ui/native/avatar';
import { NeuDataViewComponent } from '@neural-ui/native/data-view';
import { View, Text, ScrollView } from '@ng-native/components';
import { NeuDataViewItemDirective } from '@neural-ui/native/data-view';

@Component({
  selector: 'app-example',
  imports: [NeuAvatarComponent, NeuDataViewComponent, NeuDataViewItemDirective, View, Text, ScrollView],
  template: `
    <scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
    <neu-data-view [items]="people" searchLabel="Search members" (selected)="selected.set($event)">
      <ng-template neuDataViewItem let-person let-index="index">
        <view class="member-row"><neu-avatar [label]="person.label" size="md" /><view class="member-copy"><text class="usage-heading">{{ person.label }}</text><text class="usage-caption">{{ person.description }}</text></view><text class="usage-caption">{{ index + 1 }}</text></view>
      </ng-template>
    </neu-data-view>
    <text class="usage-caption" accessibilityLiveRegion="polite">{{ selected() ? 'Member selected: ' + selected() : 'Select a team member' }}</text>
    </view></scroll-view>
  `,
  styles: `
.actions { gap: 12px; align-items: flex-start; } text { color: var(--neu-text, #172033); font-size: 16px; }

.usage-scroll { flex: 1; }
.usage-stack { gap: 20px; padding-bottom: 20px; }
.usage-section { gap: 12px; }
.usage-heading { color: var(--neu-text, #172033); font-size: 16px; font-weight: 600; }
.usage-caption { color: var(--neu-muted, #64748b); font-size: 14px; }
.usage-row { flex-direction: row; flex-wrap: wrap; gap: 12px; align-items: center; }
.usage-actions { flex-direction: row; flex-wrap: wrap; gap: 8px; align-items: center; }

.member-row { flex-direction: row; align-items: center; gap: 12px; padding: 12px; border-radius: 12px; border-width: 1px; border-color: var(--neu-border, #a5afbf); background-color: var(--neu-surface, #ffffff); } .member-copy { flex: 1; min-width: 0; gap: 4px; }
  `,
})
export class ExampleComponent {
  readonly people = [{ id: 'alex', label: 'Alex Morgan', description: 'Product designer' }, { id: 'sam', label: 'Sam Rivera', description: 'Mobile developer' }];
  readonly selected = signal('');
}
Interactive playground

Change the settings and try the result.

Device captures shown in English.

Data View · android · native capture
AndroidNative screenshot
Data View · ios · native capture
iOSNative screenshot

Personalization

Import these directives together with the component and add them to Component.imports. The examples above include the complete setup.

personalization-imports.ts
import { NeuDataViewItemDirective } from '@neural-ui/native/data-view';
Template or regionContextPurpose
ng-template[neuDataViewItem]$implicit: NeuOption; index: numberReplaces each row’s visual content. The row keeps filtering, ordering, disabled behavior and its selected event. index follows the filtered and sorted list.

Template contexts

template-contexts.ts
import type { NeuDataViewItemContext } from '@neural-ui/native/data-view';

let-item reads $implicit; named fields use let-index="index". Use the exported context types for reusable TemplateRef declarations. Keep nested content presentational: the parent retains its accessible action and selection behavior.

Reusable templates

You can also declare <ng-template #row let-item> and pass [itemTemplate]="row". An explicit itemTemplate takes precedence over the projected directive; if neither is supplied, the default content is shown.

API reference

Inputs

NameTypeDefaultBindingDescription
itemsreadonly NeuOption[][]InputOrdered data for the component. Use the data contract below and stable, unique item IDs.
itemTemplateTemplateRef<NeuDataViewItemContext> | undefinedundefinedInputAngular template for each visible row. let-item receives the original item; let-index="index" receives its filtered and sorted position. The template owns the visual content; Data View keeps selection and disabled handling. Without a template, the default text card is used.
loadingbooleanfalseInputReplaces the result list with a skeleton and disables refresh while the application loads data. Search and order remain available.
errorstring''InputReplaces the results with an alert containing this message when loading is false. Clear the message to show results again.
searchLabelstring'Search items'InputText or accessible name used for search. It does not perform the action by itself.
sortLabelstring'Reverse order'InputAccessible name of the icon action that reverses the list order. The action is aligned to the right of the toolbar.
refreshLabelstring'Refresh'InputAccessible name of the refresh icon. It emits refresh so your application can reload the items, and is disabled while loading.
emptyLabelstring'No items'InputText or accessible name used for empty state. It does not perform the action by itself.

Models

NameTypeDefaultBindingDescription
querystring''Model · two-wayCurrent search text used by the control. Bind a writable signal with [(query)].
descendingbooleanfalseModel · two-wayWhether the requested sort direction is descending. Bind a writable signal with [(descending)].

Outputs

NamePayloadWhen it emits
queryChangestringEmits the updated query value. [(query)] binds this output together with its model input.
descendingChangebooleanEmits the updated descending value. [(descending)] binds this output together with its model input.
selectedstringEmits the selected item ID.
refreshvoidRequests a refresh; load the new data in your application.

Model state

Bind the signal with [(modelName)] without calling it. Use modelName() to read its value.

model-fields.ts
import { signal } from '@angular/core';

// Fields inside your component class
readonly query = signal<string>('');
readonly descending = signal<boolean>(false);

Types and data interfaces

Use the imports in your application. The definitions below are provided for reference.

NeuOption

NeuOption.ts
import type { NeuOption } from '@neural-ui/native';

// Public definition for reference only. Import this type; do not redeclare it.
export interface NeuOption {
    readonly id: string;
    readonly label: string;
    readonly description?: string;
    readonly disabled?: boolean;
}
FieldTypeRequirementMeaning
idstringRequiredStable, unique identifier used for tracking and selection.
labelstringRequiredHuman-readable item name.
descriptionstringOptionalOptional supporting text; rendering depends on the control.
disabledbooleanOptionalPrevents activating or selecting this item.

NeuDataViewItemContext

NeuDataViewItemContext.ts
import type { NeuDataViewItemContext } from '@neural-ui/native/data-view';
import type { NeuOption } from '@neural-ui/native';

// Public definition for reference only. Import this type; do not redeclare it.
export interface NeuDataViewItemContext {
    readonly $implicit: NeuOption;
    /** Position in the currently filtered and sorted collection. */
    readonly index: number;
}
FieldTypeRequirementMeaning
$implicitNeuOptionRequiredOriginal item supplied to the row template; available as let-item.
indexnumberRequiredZero-based position in the displayed collection; available as let-index="index". The personalization section describes how this component handles filtering, sorting or recycled rows.