diff --git a/@zapp/core/src/Navigator.ts b/@zapp/core/src/Navigator.ts index 19196e1..80b3b1c 100644 --- a/@zapp/core/src/Navigator.ts +++ b/@zapp/core/src/Navigator.ts @@ -1,11 +1,26 @@ +export interface RegisteredCallback { + targetPage: string + callbackPath: string[] + result?: Record + ready: boolean +} + export interface NavigatorInterface { + readonly currentPage: string navigate(route: string, params?: Record): void goBack(): void + registerResultCallback(page: string, path: string[]): void + tryPoppingLauncherResult(page: string, path: string[]): RegisteredCallback | undefined + finishWithResult(params: Record): void } let navigator: NavigatorInterface export abstract class Navigator { + public static get currentPage(): string { + return navigator.currentPage + } + public static navigate(route: string, params?: Record): void { navigator.navigate(route, params) } @@ -13,6 +28,18 @@ export abstract class Navigator { public static goBack(): void { navigator.goBack() } + + public static registerResultCallback(page: string, path: string[]) { + navigator.registerResultCallback(page, path) + } + + public static tryPoppingLauncherResult(page: string, path: string[]): RegisteredCallback | undefined { + return navigator.tryPoppingLauncherResult(page, path) + } + + public static finishWithResult(params: Record) { + navigator.finishWithResult(params) + } } export function setNavigator(navigatorInstance: NavigatorInterface): void { diff --git a/@zapp/core/src/index.ts b/@zapp/core/src/index.ts index 128802d..2b991f2 100644 --- a/@zapp/core/src/index.ts +++ b/@zapp/core/src/index.ts @@ -1,6 +1,7 @@ export type { ConfigBuilderArg } from './working_tree/props/Config.js' export { remember } from './working_tree/effects/remember.js' +export { rememberLauncherForResult } from './working_tree/effects/rememberLauncherForResult.js' export { RememberedMutableValue } from './working_tree/effects/RememberedMutableValue.js' export { sideEffect } from './working_tree/effects/sideEffect.js' export { Arc } from './working_tree/views/Arc.js' @@ -37,5 +38,5 @@ export { ViewManager } from './renderer/ViewManager.js' export { EventManager } from './renderer/EventManager.js' export { NodeType } from './NodeType.js' export { ZappInterface, Zapp, setZappInterface as __setZappInterface } from './ZappInterface.js' -export { NavigatorInterface, Navigator, setNavigator as __setNavigator } from './Navigator.js' +export { NavigatorInterface, Navigator, RegisteredCallback, setNavigator as __setNavigator } from './Navigator.js' export { SavedTreeState } from './working_tree/SavedTreeState.js' diff --git a/@zapp/core/src/working_tree/SavedTreeState.ts b/@zapp/core/src/working_tree/SavedTreeState.ts index 9b27a80..f2e83b8 100644 --- a/@zapp/core/src/working_tree/SavedTreeState.ts +++ b/@zapp/core/src/working_tree/SavedTreeState.ts @@ -46,7 +46,7 @@ export class SavedTreeState { return result } - } else if (node instanceof RememberNode) { + } else if (node instanceof RememberNode && node.remembered.shouldBeSaved()) { return { id: node.id, state: node.remembered.value, diff --git a/@zapp/core/src/working_tree/effects/RememberedValue.ts b/@zapp/core/src/working_tree/effects/RememberedValue.ts index f2c8b6e..670decb 100644 --- a/@zapp/core/src/working_tree/effects/RememberedValue.ts +++ b/@zapp/core/src/working_tree/effects/RememberedValue.ts @@ -17,4 +17,9 @@ export class RememberedValue { public switchContext(context: RememberNode) { this.context = context } + + /** @internal */ + public shouldBeSaved() { + return typeof this._value !== 'function' + } } diff --git a/@zapp/core/src/working_tree/effects/remember.ts b/@zapp/core/src/working_tree/effects/remember.ts index 48a1edb..74b6bb4 100644 --- a/@zapp/core/src/working_tree/effects/remember.ts +++ b/@zapp/core/src/working_tree/effects/remember.ts @@ -30,7 +30,6 @@ export function remember(value: T): RememberedMutableValue { const result = savedRemembered === null ? new RememberedMutableValue(value, context) : savedRemembered context.remembered = result - current.children.push(context) return result diff --git a/@zapp/core/src/working_tree/effects/rememberLauncherForResult.ts b/@zapp/core/src/working_tree/effects/rememberLauncherForResult.ts new file mode 100644 index 0000000..c659e9e --- /dev/null +++ b/@zapp/core/src/working_tree/effects/rememberLauncherForResult.ts @@ -0,0 +1,29 @@ +import { ViewNode } from '../ViewNode.js' +import { WorkingTree } from '../WorkingTree.js' +import { Navigator } from '../../Navigator.js' + +type CallbackType = (result: Record | undefined) => void + +interface LauncherForResult { + launch: (params?: Record) => void +} + +export function rememberLauncherForResult(page: string, callback: CallbackType): LauncherForResult { + const current = WorkingTree.current as ViewNode + const context = current.remember() + + const result = Navigator.tryPoppingLauncherResult(page, context.path.concat(context.id)) + if (result !== undefined && result.ready) { + callback(result.result) + } + + // we don't need to push created node to parent context as we only need the path to trigger + // the correct callback when the opened screen finishes + + return { + launch: (params: Record) => { + Navigator.registerResultCallback(page, context.path.concat(context.id)) + Navigator.navigate(page, params) + }, + } +} diff --git a/@zapp/watch/src/Navigator.ts b/@zapp/watch/src/Navigator.ts index d4bf93b..59eea5b 100644 --- a/@zapp/watch/src/Navigator.ts +++ b/@zapp/watch/src/Navigator.ts @@ -1,6 +1,10 @@ -import { NavigatorInterface } from '@zapp/core' +import { NavigatorInterface, RegisteredCallback } from '@zapp/core' export class Navigator implements NavigatorInterface { + get currentPage(): string { + throw new Error('Method not implemented.') + } + public navigate(page: string, params: Record) { hmApp.gotoPage({ url: page, param: JSON.stringify(params) }) } @@ -8,4 +12,16 @@ export class Navigator implements NavigatorInterface { public goBack() { hmApp.goBack() } + + registerResultCallback(page: string, path: string[]): void { + throw new Error('Method not implemented.') + } + + tryPoppingLauncherResult(page: string, path: string[]): RegisteredCallback | undefined { + throw new Error('Method not implemented.') + } + + finishWithResult(params: Record): void { + throw new Error('Method not implemented.') + } } diff --git a/@zapp/web/src/HashNavigator.ts b/@zapp/web/src/HashNavigator.ts index 3d96ab7..09275fa 100644 --- a/@zapp/web/src/HashNavigator.ts +++ b/@zapp/web/src/HashNavigator.ts @@ -1,40 +1,84 @@ -import { SavedTreeState, WorkingTree } from '@zapp/core' +import { SavedTreeState, WorkingTree, NavigatorInterface, RegisteredCallback } from '@zapp/core' const historyStack: SavedTreeState[] = [] +const registeredCallbacks: RegisteredCallback[] = [] // not using common interface for now due to platform specific differences -export abstract class HashNavigator { - private static routes: Record) => void> - private static _currentRoute: string +export class HashNavigator implements NavigatorInterface { + private routes: Record) => void> + private currentRoute: string - public static register(startingRoute: string, routes: Record) => void>) { + public register(startingRoute: string, routes: Record) => void>) { const routeToRender = window.location.hash.length === 0 ? startingRoute : window.location.hash.substring(1) - HashNavigator.routes = routes - HashNavigator.changeRoute(routeToRender) + this.routes = routes + this.changeRoute(routeToRender) history.replaceState(undefined, '', `#${routeToRender}`) window.addEventListener('popstate', (e) => { WorkingTree.restoreState(historyStack.pop()!) - HashNavigator.changeRoute(window.location.hash.substring(1), e.state) + this.changeRoute(window.location.hash.substring(1), e.state) }) } - public static navigate(route: string, params?: Record) { - if (HashNavigator._currentRoute !== route && HashNavigator.routes[route] !== undefined) { + public navigate(route: string, params?: Record) { + if (this.currentRoute !== route && this.routes[route] !== undefined) { historyStack.push(WorkingTree.saveState()) - HashNavigator.changeRoute(route, params) + this.changeRoute(route, params) history.pushState(params, '', `#${route}`) } } - private static changeRoute(route: string, params?: Record) { - HashNavigator._currentRoute = route + public goBack(): void { + history.back() + } + + registerResultCallback(page: string, path: string[]): void { + registeredCallbacks.push({ + targetPage: page, + callbackPath: path, + ready: false, + }) + } + + tryPoppingLauncherResult(page: string, path: string[]): RegisteredCallback | undefined { + if (registeredCallbacks.length > 0) { + const top = registeredCallbacks[registeredCallbacks.length - 1] + + if (top.ready && page === top.targetPage && top.callbackPath.length === path.length) { + for (let i = 0; i < path.length; i++) { + if (path[i] !== top.callbackPath[i]) { + return undefined + } + } + + return registeredCallbacks.pop() + } + } + + return undefined + } + + finishWithResult(params: Record): void { + if (registeredCallbacks.length > 0) { + const top = registeredCallbacks[registeredCallbacks.length - 1] + + if (top.targetPage === this.currentPage) { + top.ready = true + top.result = params + } + } + + this.goBack() + } + + private changeRoute(route: string, params?: Record) { + this.currentRoute = route WorkingTree.dropAll() - HashNavigator.routes[route](params) + this.routes[route](params) } - public static get currentRoute(): string { - return HashNavigator._currentRoute + public get currentPage(): string { + return this.currentRoute } } diff --git a/@zapp/web/src/index.ts b/@zapp/web/src/index.ts index 2d2dfb6..e460005 100644 --- a/@zapp/web/src/index.ts +++ b/@zapp/web/src/index.ts @@ -1,8 +1,17 @@ -import { __setViewManager, __setZappInterface } from '@zapp/core' +import { __setViewManager, __setZappInterface, __setNavigator } from '@zapp/core' import { WebViewManager } from './WebViewManager.js' import { ZappWeb } from './ZappWeb.js' +import { HashNavigator } from './HashNavigator.js' -export { HashNavigator } from './HashNavigator.js' +const navigator = new HashNavigator() __setZappInterface(new ZappWeb()) __setViewManager(new WebViewManager()) +__setNavigator(navigator) + +export function registerNavigationRoutes( + startingRoute: string, + routes: Record) => void> +) { + navigator.register(startingRoute, routes) +} diff --git a/web-test/src/NavBar.ts b/web-test/src/NavBar.ts index 992f544..808942b 100644 --- a/web-test/src/NavBar.ts +++ b/web-test/src/NavBar.ts @@ -8,13 +8,13 @@ import { Custom, remember, ColumnConfig, + Navigator, } from '@zapp/core' -import { HashNavigator } from '@zapp/web' function NavButton(text: string, route: string) { Custom(ColumnConfig(`wrapperbutton#${route}`).padding(5, 0), {}, () => { - const defaultColor = route === HashNavigator.currentRoute ? 0x555555 : 0x333333 - const pressedColor = route === HashNavigator.currentRoute ? 0x666666 : 0x444444 + const defaultColor = route === Navigator.currentPage ? 0x555555 : 0x333333 + const pressedColor = route === Navigator.currentPage ? 0x666666 : 0x444444 const pressed = remember(false) const background = remember(defaultColor) @@ -35,7 +35,7 @@ function NavButton(text: string, route: string) { pressed.value = false background.value = defaultColor - HashNavigator.navigate(route, { from: HashNavigator.currentRoute }) + Navigator.navigate(route, { from: Navigator.currentPage }) } }) .onPointerLeave(() => { diff --git a/web-test/src/index.ts b/web-test/src/index.ts index 9661a05..2a190c4 100644 --- a/web-test/src/index.ts +++ b/web-test/src/index.ts @@ -1,4 +1,4 @@ -import { HashNavigator } from '@zapp/web' +import { registerNavigationRoutes } from '@zapp/web' import { ActivityIndicator } from '@zapp/ui' import { WorkingTree, @@ -24,6 +24,8 @@ import { Arc, ArcConfig, Zapp, + rememberLauncherForResult, + Navigator, } from '@zapp/core' import { NavBar, RouteInfo } from './NavBar' import { Page } from './Page' @@ -37,6 +39,7 @@ const routesInfo: RouteInfo[] = [ { displayName: 'Column example', routeName: 'column' }, { displayName: 'Row example', routeName: 'row' }, { displayName: 'Animation example', routeName: 'animation' }, + { displayName: 'StartForResult example', routeName: 'startForResult' }, ] function StackExample() { @@ -433,10 +436,58 @@ function DynamicLayoutExample() { }) } -HashNavigator.register('dynamicLayout', { +function StartForResultExample() { + Page(routesInfo, () => { + Column(ColumnConfig('column').alignment(Alignment.Center).arrangement(Arrangement.Center), () => { + const value = remember('nothing') + const anim = remember(0) + const launcher = rememberLauncherForResult('picker', (result) => { + value.value = result!.res as string + }) + + sideEffect(() => { + anim.value = withTiming(400, { duration: 1000 }) + }) + + Stack(StackConfig('box').width(100).height(100).background(0xff0000).offset(0, anim.value)) + + Text(TextConfig('value-text').textColor(0xffffff).textSize(40), value.value) + + Button(Config('btn'), 'Open', () => { + launcher.launch() + }) + + Button(Config('clear'), 'Clear', () => { + value.value = 'nothing' + }) + }) + }) +} + +function NumberPickerExample() { + Page(routesInfo, () => { + Column(ColumnConfig('column').alignment(Alignment.Center).arrangement(Arrangement.Center), () => { + Button(Config('btn1'), 'Send 1', () => { + Navigator.finishWithResult({ res: '1' }) + }) + + Button(Config('btn2'), 'Send 2', () => { + Navigator.finishWithResult({ res: '2' }) + }) + + Button(Config('btn3'), 'Send 3', () => { + Navigator.finishWithResult({ res: '3' }) + }) + }) + }) +} + +registerNavigationRoutes('dynamicLayout', { dynamicLayout: DynamicLayoutExample, stack: StackExample, column: ColumnExample, row: RowExample, animation: AnimationExample, + startForResult: StartForResultExample, + picker: NumberPickerExample, })