Skip to content

Commit

Permalink
Merge pull request #822 from arkivanov/update-docs
Browse files Browse the repository at this point in the history
Added docs for the new Web Navigation API
  • Loading branch information
arkivanov authored Dec 8, 2024
2 parents 4d91560 + fdd9293 commit 363192a
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 8 deletions.
13 changes: 5 additions & 8 deletions docs/navigation/stack/browser-history.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Web browser history
# Web Browser History

!!!warning

This API is likely to be removed in the future. Please see the successor of this experimental API: [Web Navigation API](../web-navigation.md).

By default `Child Stack` navigation does not affect URLs in the browser address bar. But sometimes it is necessary to have different URLs for
different `Child Stack` destinations. For this purpose Decompose provides an **experimental** API - [WebHistoryController](https://github.com/arkivanov/Decompose/blob/master/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryController.kt).
Expand Down Expand Up @@ -56,10 +60,3 @@ Using `WebHistoryController` is easy:
3. In the component, call the `WebHistoryController.attach` method and supply all arguments.
4. In the JS app, pass an initial deeplink to the component.
5. Use the deeplink in the component to generate an initial back stack.

### Example

The sample project demonstrates the use of `WebHistoryController`:

- [Main.kt](https://github.com/arkivanov/Decompose/blob/master/sample/app-js/src/main/kotlin/com/arkivanov/sample/app/Main.kt) - demonstrates passing `WebHistoryController` and a deeplink via constructor to `RootComponent`
- [RootComponent](https://github.com/arkivanov/Decompose/blob/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt) - demonstrates generating the initial stack from the deeplink, as well as calling `WebHistoryController.attach` and supplying the arguments
232 changes: 232 additions & 0 deletions docs/navigation/web-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# Web Browser Navigation

!!!warning

This API is experimental, available since version `3.3.0-alpha01`.

The Web Navigation API is a successor of the old [Web Browser History](stack/browser-history.md) API. It is a more flexible and powerful tool for managing browser URLs and history in a web application. The API is designed to work with different navigation models and provides a way to synchronize the browser URL and history with the navigation state. Currently, the following navigation models are supported:

- [Child Stack](stack/overview.md)
- [Child Pages](pages/overview.md)
- [Child Panels](panels/overview.md)

The `Child Slot` navigation model is currently not supported. It will likely require additional API changes, might be implemented in the future.

## WebNavigation and WebNavigationOwner interfaces

The API consists of two main interfaces:

- [WebNavigation](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigation.kt) - A two-way navigation controller for Web browsers that connects a navigation model (e.g. `Child Stack`) with the browser's navigation history.
- [WebNavigationOwner](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigationOwner.kt) - An interface that represents a holder of `WebNavigation`, typically implemented by a Decompose component.

You don't need to implement the `WebNavigation` interface directly. Instead, you should implement the `WebNavigationOwner` interface in your component using one of the provided functions available for each supported navigation model:

- [childStackWebNavigation](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackWebNavigation.kt) - For `Child Stack` navigation model.
- [childPagesWebNavigation](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesWebNavigation.kt) - For `Child Pages` navigation model.
- [childPanelsWebNavigation](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsWebNavigation.kt) - For `Child Panels` navigation model.

Only one instance of the `WebNavigation` controller is allowed per component. The new API also supports nested navigation, there can only be up to one (zero or one) child `WebNavigationOwner` at a time.

## Enabling the Web Navigation

To enable the Web Navigation, just use the `withWebHistory {}` function available for `js` ([link](https://github.com/arkivanov/Decompose/blob/master/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt)) and `wasmJs` ([link](https://github.com/arkivanov/Decompose/blob/master/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt)) targets separately. The function provides two parameters: the current URL and an instance of `StateKeeper`. The URL should typically be used as a deep link to initialize the navigation state. The `StateKeeper` automatically saves and restores the navigation state on page reloads, it should be used to create the root `ComponentContext`.

Once the Web Navigation is enabled, the browser URL and history will be automatically synchronized with the navigation state. The browser history automatically follows the navigation state changes, and the navigation is automatically performed following the browser history changes.

## Configuring the application

Using Web Navigation in a single page application requires additional configuration - a catch-all strategy to return the same html resource for all paths. This strategy will be different for different server configurations.

### Development configuration

The Kotlin/JS `browser` target uses [webpack-dev-server](https://github.com/webpack/webpack-dev-server) as a local development server.
It can be configured to use the same `index.html` file (or your primary html file) for all paths, by setting the [devServer.historyApiFallback](https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback) flag. The Gradle DSL for Kotlin
webpack currently does not support the `historyApiFallback` flag, so a special configuration file should be used instead.

First, create a directory named `webpack.config.d` in the JS app module's directory. Then create a new file named `devServerConfig.js`
inside that directory. Finally, put the following content to the file:

```javascript
// <js app module>/webpack.config.d/devServerConfig.js

config.devServer = {
...config.devServer, // Merge with other devServer settings
"historyApiFallback": true
};
```

## Web Navigation with Child Stack

```kotlin title="Component interface"
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.webhistory.WebNavigationOwner
import com.arkivanov.decompose.value.Value

interface MyStackComponent : WebNavigationOwner {
val stack: Value<ChildStack<*, Child>>

sealed class Child {
// Omitted code
}
}

```

```kotlin title="Component implementation"
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.childStackWebNavigation
import com.arkivanov.decompose.router.webhistory.WebNavigation
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Serializable

class DefaultMyStackComponent(
componentContext: ComponentContext,
deepLinkUrl: String?, // Or your favourite data structure, like Uri, etc.
) : MyStackComponent, ComponentContext by componentContext {

private val nav = StackNavigation<Config>()

private val _stack: Value<ChildStack<Config, MyStackComponent.Child>> =
childStack(
source = nav,
serializer = Config.serializer(),
initialStack = { TODO("Use the deepLinkUrl parameter to initialize the stack") },
childFactory = { ... },
)

override val stack: Value<ChildStack<*, MyStackComponent.Child>> = _stack

override val webNavigation: WebNavigation<*> =
childStackWebNavigation(
navigator = nav,
stack = _stack,
serializer = Config.serializer(),
pathMapper = { child -> TODO("Return a path for the child") }, // Optional
parametersMapper = { child -> TODO("Return a Map with parameters for the child") }, // Optional
childSelector = { child -> TODO("Return a WebNavigationOwner for the child") }, // Optional
)

@Serializable
private sealed interface Config {
// Omitted code
}
}
```

## Web Navigation with Child Pages

```kotlin title="Component interface"
import com.arkivanov.decompose.router.pages.ChildPages
import com.arkivanov.decompose.router.webhistory.WebNavigationOwner
import com.arkivanov.decompose.value.Value

interface MyPagesComponent : WebNavigationOwner {
val pages: Value<ChildPages<*, ...>>
}
```

```kotlin title="Component implementation"
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.pages.ChildPages
import com.arkivanov.decompose.router.pages.PagesNavigation
import com.arkivanov.decompose.router.pages.childPages
import com.arkivanov.decompose.router.pages.childPagesWebNavigation
import com.arkivanov.decompose.router.webhistory.WebNavigation
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Serializable

class DefaultMyPagesComponent(
componentContext: ComponentContext,
deepLinkUrl: String?, // Or your favourite data structure, like Uri, etc.
) : MyPagesComponent, ComponentContext by componentContext {

private val nav = PagesNavigation<Config>()

private val _pages: Value<ChildPages<Config, ...>> =
childPages(
source = nav,
serializer = Config.serializer(),
initialPages = { TODO("Use the deepLinkUrl parameter to initialize the navigation") },
childFactory = { ... },
)

override val pages: Value<ChildPages<*, ...>> = _pages

override val webNavigation: WebNavigation<*> =
childPagesWebNavigation(
navigator = nav,
pages = _pages,
serializer = Config.serializer(),
pathMapper = { pages -> TODO("Return a path for the navigation state") }, // Optional
parametersMapper = { pages -> TODO("Return a Map with parameters for the navigation state") }, // Optional
childSelector = { child -> TODO("Return a WebNavigationOwner for the child") }, // Optional
)

@Serializable
private data class Config(...)
}
```

## Web Navigation with Child Panels

```kotlin title="Component interface"
import com.arkivanov.decompose.router.panels.ChildPanels
import com.arkivanov.decompose.router.webhistory.WebNavigationOwner
import com.arkivanov.decompose.value.Value

interface MyPanelsComponent : WebNavigationOwner {
val panels: Value<ChildPanels<...>>
}
```

```kotlin title="Component implementation"
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.panels.ChildPanels
import com.arkivanov.decompose.router.panels.PanelsNavigation
import com.arkivanov.decompose.router.panels.childPanels
import com.arkivanov.decompose.router.panels.childPanelsWebNavigation
import com.arkivanov.decompose.router.webhistory.WebNavigation
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Serializable

class DefaultMyPanelsComponent(
componentContext: ComponentContext,
deepLinkUrl: String?, // Or your favourite data structure, like Uri, etc.
) : MyPanelsComponent, ComponentContext by componentContext {

private val nav = PanelsNavigation<MainConfig, DetailsConfig, Nothing>()

private val _panels: Value<ChildPanels<...>> =
childPanels(
source = nav,
serializers = MainConfig.serializer() to DetailsConfig.serializer(),
initialPanels = { TODO("Use the deepLinkUrl parameter to initialize the navigation") },
mainFactory = { ... },
detailsFactory = { ... },
)

override val panels: Value<ChildPanels<...>> = _panels

override val webNavigation: WebNavigation<*> =
childPanelsWebNavigation(
navigator = nav,
panels = _panels,
serializers = MainConfig.serializer() to DetailsConfig.serializer(),
pathMapper = { panels -> TODO("Return a path for the navigation state") }, // Optional
parametersMapper = { panels -> TODO("Return a Map with parameters for the navigation state") }, // Optional
childSelector = { panels -> TODO("Return a WebNavigationOwner for the navigation state") }, // Optional
)

@Serializable
private data class MainConfig(...)

@Serializable
private data class DetailsConfig(...)
}
```
## Example

Please refer to the [main sample](https://github.com/arkivanov/Decompose/tree/master/sample) for a complete example of using the Web Navigation API.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ nav:
- Navigation: navigation/panels/navigation.md
- Generic Navigation:
- Overview: navigation/children/overview.md
- Web Navigation: navigation/web-navigation.md

- Extensions:
- Overview: extensions/overview.md
Expand Down

0 comments on commit 363192a

Please sign in to comment.