Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ export default defineConfig({
{ text: 'Introduction', link: '/guide/introduction' },
{ text: 'Installation', link: '/guide/installation' },
{ text: 'Navigation', link: '/guide/navigation' },
{ text: 'Bridge components', link: '/guide/bridge-components' },
],
},
{
text: 'Bridge components',
items: [
{ text: 'Overview', link: '/components/overview' },
{ text: 'Button', link: '/components/button' },
],
},
{
Expand Down
108 changes: 108 additions & 0 deletions docs/components/button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Button

A button rendered in the **native navigation bar**. The web side registers a
title; iOS draws a `UIBarButtonItem`; every tap is relayed back to the web side.

Outside of Hotwire Native nothing is registered and your own markup is rendered
instead, so the same page still works in a regular browser.

::: tip Copy the source
Both halves live in the
[hotwire-bridge-components](https://github.com/zumkorn/hotwire-bridge-components/tree/main/registry/button)
registry — `inertia/react.tsx` for the web and `native/ButtonComponent.swift`
for iOS. Copy them into your app; you own them from then on.
:::

## Web side

Copy `registry/button/inertia/react.tsx` into your app, then use it as a
component. It renders nothing when the native button is showing, and renders its
children as the web fallback when it is not:

```jsx
import { BridgeButton } from '@/bridge/BridgeButton'

function Article({ onSave }) {
return (
<>
<BridgeButton title="Save" onTap={onSave}>
<button type="button" onClick={onSave}>Save</button>
</BridgeButton>

{/* … */}
</>
)
}
```

| Prop | Type | Default | Purpose |
| --- | --- | --- | --- |
| `title` | `string` | — | Label on the native button |
| `side` | `'left' \| 'right'` | `'right'` | Which end of the navigation bar |
| `onTap` | `() => void` | — | Called on every tap |
| `children` | `ReactNode` | — | Web fallback, rendered only in a browser |

The file is TypeScript. Vite compiles `.tsx` with no configuration change, even
in a project that is otherwise plain `.jsx` — types are stripped by esbuild.
Without `typescript` and a `tsconfig.json` they are not checked, only removed.

## iOS side

Copy `registry/button/native/ButtonComponent.swift` into your app and register
it at launch:

```swift
Hotwire.registerBridgeComponents([
ButtonComponent.self,
// … your other components
])
```

Until it is registered, `supported` stays false on the web side and only the
fallback is rendered.

::: warning Minimum SDK
The Swift component requires **`hotwire-native-ios` 1.2.0 or newer**. In 1.2.0
`BridgeComponent.delegate` became a weak optional; the component reaches its
view controller through `delegate?.destination`, which does not compile against
1.1.x.
:::

The component overrides `name` as `override nonisolated class var name`. The
base declaration is `nonisolated`, so an app built with
`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` — the Xcode 26 default for new
projects — rejects a plain `override class var name` as an actor-isolation
mismatch.

## The contract

Component name: `button`.

### `connect` — web → native

Registers or re-registers the bar button. Sent on connect and whenever the title
or side changes.

```jsonc
{
"title": "Save", // string, required — button label
"side": "right" // "left" | "right", optional, default "right"
}
```

### `connect` reply — native → web

Native **replies to the same `connect` message** every time the button is
tapped. There is no separate tap event — the reply *is* the tap signal, and it
arrives once per tap rather than once per registration.

Because a reply can arrive many times, re-registering without dropping the
previous callback makes each tap fire twice. The registry component handles this;
see [Callback lifetime](/components/overview#callback-lifetime) if you write your
own.

## Android

Not covered here yet. The registry ships a `ButtonComponent.kt`, but it has not
been verified against a pinned Android SDK version — treat it as unversioned
until it has.
55 changes: 46 additions & 9 deletions docs/guide/bridge-components.md → docs/components/overview.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
# Bridge components
# Overview

Bridge components let your web pages drive native UI — a submit button in the
app bar, a native menu, and so on. They build on Hotwire Native's bridge, which
this package re-implements on the web side as `window.HotwireNative.web`.
Bridge components let your web pages drive native UI — a button in the
navigation bar, a native menu, and so on. They build on Hotwire Native's bridge,
which this package re-implements on the web side as `window.HotwireNative.web`.

Each component has two halves that must agree:

| Half | Where it lives | What it does |
| --- | --- | --- |
| Web | Your Inertia app | Registers the component and handles replies |
| Native | Your iOS app | Draws the native UI and reports interaction back |

If the native half is not registered, the web half stays inert and your ordinary
web control is rendered instead. This is what makes the same page work in a
regular browser.

## `useBridgeComponent`

Expand Down Expand Up @@ -36,11 +47,37 @@ function NativeMenu({ items, onSelect }) {
- `send(event, data?, callback?)` returns a message id; native replies invoke
the `callback`.

## Building a form submit button
## Callback lifetime

`useBridgeComponent` removes every callback it registered when the component
unmounts. It does **not** remove them when you re-register — so if you send
`connect` again from an effect that re-runs, drop the previous callback first:

```js
useEffect(() => {
if (!supported) return
const id = send('connect', { title }, () => onTapRef.current?.())
return () => window.HotwireNative?.web?.removeCallback(id)
}, [supported, title, send])
```

Without the cleanup, a changed prop leaves the old callback registered and every
subsequent interaction is reported twice.

## Ready-made components

Rather than writing each component from scratch, you can copy one from the
[hotwire-bridge-components](https://github.com/zumkorn/hotwire-bridge-components)
registry. It ships both halves — the web component and its Swift counterpart —
along with a contract describing the messages they exchange.

The model is copy-paste, not a package: you own the source once it is in your
app. See [Button](/components/button) for a worked example.

## Building your own

`useBridgeComponent` is the building block for specific components. A common one
is a native submit button wired to an Inertia form — implement it in your app on
top of the primitive:
`useBridgeComponent` is the building block. A native submit button wired to an
Inertia form looks like this:

```js
// hooks/useBridgeForm.js
Expand Down Expand Up @@ -94,4 +131,4 @@ function NewResource() {
```

The native app must register the matching bridge component (`form`, `menu`, …)
for `supported` to be true. See the native setup guides.
for `supported` to be true. See the [iOS guide](/native/ios).
2 changes: 1 addition & 1 deletion docs/guide/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ inspect (Android) to read it.
initHotwireNative({ debug: true })
```

Next: [Bridge components](/guide/bridge-components).
Next: [Bridge components](/components/overview).
21 changes: 20 additions & 1 deletion docs/native/ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,26 @@ Clone it to see a working setup end to end.
2. Point its `Navigator` at your app's URL.
3. Add a `path-configuration` JSON to control which routes present as modals,
pushes, etc.
4. Register the bridge components your web pages use (`form`, `menu`, …) if any.
4. Register the bridge components your web pages use (`button`, `form`, `menu`,
…) if any.

## Registering bridge components

Each bridge component your web pages use needs a Swift counterpart registered at
launch, otherwise the web side falls back to its ordinary markup:

```swift
Hotwire.registerBridgeComponents([
ButtonComponent.self,
FormComponent.self,
MenuComponent.self,
])
```

Ready-made components — Swift source plus the matching web half — live in the
[hotwire-bridge-components](https://github.com/zumkorn/hotwire-bridge-components)
registry. See [Button](/components/button) for a worked example, including the
minimum SDK version they require.

::: info Work in progress
A full step-by-step walkthrough is being written. For now, the demo app is the
Expand Down
1 change: 1 addition & 0 deletions docs/public/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/guide/bridge-components /components/overview 301
Loading