Build your first app
Build Pocket notes: search a short list, open a note, edit it in a native modal and return the saved text. A leave guard protects an unsaved draft. Back returns to the same list with its query intact.
This tutorial uses explicit routes and public APIs. It assumes basic Vue single-file components and the NativeScript-Vue project setup. The notes are fictional and in memory; this example does not implement persistence, authentication or a backend.
1. Create the files
After installing the router, put these seven complete files under the app's src directory. Replace the starter app entry/root with the files below; keep its native resources and build configuration. Code is embedded directly from the runnable tutorial source, so all language guides use the same implementation.
src/
app.ts
App.vue
router.ts
notes.ts
pages/
Home.vue
Note.vue
Edit.vueThe root RouterView creates a native Frame. Each route component renders a Page. Do not wrap this RouterView in another Frame.
src/router.ts — / owns the list, :id selects a note, and modal: true gives the editor its own Frame. props: true passes the route params into the page.
import { createRouter } from 'vue-router-ns'
export const router = createRouter({
routes: [
{ path: '/', component: () => import('./pages/Home.vue') },
{
path: '/notes/:id',
component: () => import('./pages/Note.vue'),
props: true,
},
{
path: '/notes/:id/edit',
component: () => import('./pages/Edit.vue'),
props: true,
modal: true,
},
],
})src/app.ts — install the router before starting the app.
import { createApp } from 'nativescript-vue'
import App from './App.vue'
import { router } from './router'
createApp(App).use(router).start()src/App.vue — keep the navigation host mounted.
<template>
<RouterView />
</template>2. Add data and the list
src/notes.ts — application data shared by the list, detail and editor. Saving changes this reactive array; it does not write to disk.
import { reactive } from 'nativescript-vue'
export const notes = reactive([
{ id: 'coast', title: 'Along the coast', body: 'A quiet path beside the sea.' },
{ id: 'forest', title: 'Into the forest', body: 'Follow the shade and slow down.' },
])src/pages/Home.vue — query belongs to this page. Opening a note covers Home; it does not destroy the page or its query.
<script setup lang="ts">
import { computed, ref } from 'nativescript-vue'
import { useRouter } from 'vue-router-ns'
import { notes } from '../notes'
const router = useRouter()
const query = ref('')
const filtered = computed(() =>
notes.filter((note) => note.title.toLowerCase().includes(query.value.toLowerCase())),
)
</script>
<template>
<Page>
<ActionBar title="Pocket notes" />
<StackLayout padding="24">
<Label text="Your first native flow" fontSize="26" textWrap="true" />
<TextField
v-model="query"
hint="Find a note"
accessibilityLabel="Find a note"
marginTop="16"
/>
<Button
v-for="note in filtered"
:key="note.id"
:text="note.title"
@tap="router.push(`/notes/${note.id}`)"
/>
<Label v-if="!filtered.length" text="No notes found." />
</StackLayout>
</Page>
</template>This two-item list uses normal native Buttons. For a large catalog, use a virtualized list. The TextField is a simple beginner search input; native header search is a later enhancement.
3. Open a detail and wait for a result
src/pages/Note.vue — the id prop belongs to this route entry. pushForResult<string> waits for the editor; closing without a result returns undefined. Checking explicitly preserves an intentionally empty saved string.
<script setup lang="ts">
import { useRouter } from 'vue-router-ns'
import { notes } from '../notes'
const props = defineProps<{ id: string }>()
const router = useRouter()
const note = notes.find((item) => item.id === props.id)
async function edit() {
const result = await router.pushForResult<string>(`/notes/${props.id}/edit`)
if (note && result !== undefined) note.body = result
}
</script>
<template>
<Page>
<ActionBar :title="note?.title ?? 'Note not found'">
<NavigationButton
text="Back"
android.systemIcon="ic_menu_back"
@tap="router.back()"
/>
</ActionBar>
<StackLayout v-if="note" padding="24">
<Label :text="note.body" fontSize="22" textWrap="true" marginBottom="24" />
<Button text="Edit in a modal" @tap="edit" />
</StackLayout>
<Label v-else text="This note does not exist." margin="24" />
</Page>
</template>The originating detail stays mounted while the modal is open. The generic type describes the contract between these two pages; it does not validate arbitrary data at runtime.
4. Protect the draft, then save
src/pages/Edit.vue — the draft is local to the editor. A conditional leave guard asks only while the draft differs from the saved note. Saving temporarily allows departure and returns the draft through back({ result }).
<script setup lang="ts">
import { computed, ref } from 'nativescript-vue'
import { Dialogs } from '@nativescript/core'
import { onBeforeRouteLeave, useRouter } from 'vue-router-ns'
import { notes } from '../notes'
const props = defineProps<{ id: string }>()
const router = useRouter()
const note = notes.find((item) => item.id === props.id)
const draft = ref(note?.body ?? '')
const saving = ref(false)
const dirty = computed(() => !!note && !saving.value && draft.value !== note.body)
onBeforeRouteLeave(
() =>
Dialogs.confirm({
title: 'Discard changes?',
message: 'Your unsaved text will be lost.',
okButtonText: 'Discard',
cancelButtonText: 'Keep editing',
}),
{ when: dirty },
)
async function save() {
if (!note) return
saving.value = true
try {
await router.back({ result: draft.value })
} finally {
saving.value = false
}
}
</script>
<template>
<Page>
<ActionBar title="Edit note">
<ActionItem text="Cancel" ios.position="left" @tap="router.back()" />
</ActionBar>
<StackLayout padding="24">
<TextView
v-model="draft"
accessibilityLabel="Note draft"
automationText="Note draft"
height="180"
fontSize="20"
/>
<Label :text="dirty ? 'Unsaved changes' : 'No unsaved changes'" marginTop="12" />
<Button text="Save note" :isEnabled="!!note && !saving" @tap="save" />
</StackLayout>
</Page>
</template>Cancel requests navigation through the guard. Choosing Keep editing keeps the editor mounted with its draft. Choosing Discard closes it without changing the note. Saving returns the value to the detail page, where application code updates notes.
A leave guard is about removing a page, not covering it. On iOS, an active guard changes which gestures can dismiss or pop a page. See the guard platform matrix rather than assuming every gesture can wait for a dialog.
5. Run and check the complete flow
In the NativeScript-Vue project you created:
ns run ios
# or:
ns run android- Type
coastin Find a note. Only Along the coast remains. - Open that note, then tap Edit in a modal.
- Change the text. Tap Cancel → Keep editing and confirm the draft remains.
- Tap Save note. The original detail page shows the returned text.
- Use native Back. Home still contains
coast, and its filtered list is unchanged. - Open the editor again, change the text and choose Cancel → Discard. The last saved text stays unchanged.
If you prefer the repository version, follow the checkout/resource setup, then run npm run tutorial:ios or npm run tutorial:android from demo. Pass -- --device <id> to choose a device. The tutorial shares the demo application ID, so installation replaces the other demo entry on that device.
What to build next
You now have one retained stack, a modal stack, a page-local draft and an application-owned result. Revisit the interactive stack model to see those lifetimes separately.
Continue with Fieldnotes to add a drawer, page-owned native search, shared header/menu actions and explicit navigation focus. Choose tabs or split navigation when the app needs a different structure. Add persistent storage/restoration when state must survive process death.