# Programmatic control

The fluidbox opens, moves and closes the viewer from code. Its state tells you what is open, and events tell you what changed and why.

#### React

`programmatic.tsx`

```tsx
import { useRef, useState, type RefObject } from "react";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidbox } from "@ogdakke/fluidbox/react";
import { photos } from "./data";

function Toolbar({ lock }: { lock: RefObject<HTMLInputElement | null> }) {
  const open = useFluidbox(({ open }) => open);
  const status = useFluidbox(({ status, index }) => `${status} ${index ?? "-"}`);
  return (
    <div className="pattern-toolbar">
      <button type="button" onClick={() => open(2)}>
        Open the third photo
      </button>
      <label>
        <input ref={lock} type="checkbox" name="lock" /> Lock navigation
      </label>
      <output>{status}</output>
    </div>
  );
}

export function Programmatic() {
  const lock = useRef<HTMLInputElement>(null);
  const [log, setLog] = useState<string[]>([]);
  const record = (event: Event) => setLog((types) => [...types, event.type].slice(-20));
  return (
    <Fluidbox.Root
      items={photos}
      // beforeitemchange comes before every move between items; canceling it keeps the viewer still.
      onBeforeItemChange={(event) => {
        if (lock.current?.checked) event.preventDefault();
        record(event);
      }}
      onBeforeToggle={record}
      onToggle={record}
      onItemChange={record}
      onSettle={record}
    >
      <Toolbar lock={lock} />
      <div className="pattern-grid">
        {photos.map((photo) => (
          <Fluidbox.Trigger
            key={photo.src}
            item={photo}
            className="pattern-thumb"
          >
            <img src={photo.src} alt={itemLabel(photo)} />
          </Fluidbox.Trigger>
        ))}
      </div>
      <ol className="pattern-log">
        {log.map((type, index) => (
          <li key={index}>{type}</li>
        ))}
      </ol>
    </Fluidbox.Root>
  );
}
```

#### Vue

`Programmatic.vue`

```vue
<script setup lang="ts">
import { ref } from "vue";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, type FluidboxItemChangeEvent } from "@ogdakke/fluidbox/vue";
import { photos } from "./data";

const locked = ref(false);
const log = ref<string[]>([]);

const record = (event: Event) => {
  log.value = [...log.value, event.type].slice(-20);
};

// beforeitemchange comes before every move between items; canceling it keeps the viewer still.
function onBeforeItemChange(event: FluidboxItemChangeEvent) {
  if (locked.value) event.preventDefault();
  record(event);
}
</script>

<template>
  <Fluidbox.Root
    v-slot="{ status, index, open }"
    :items="photos"
    @beforetoggle="record"
    @toggle="record"
    @beforeitemchange="onBeforeItemChange"
    @itemchange="record"
    @settle="record"
  >
    <div class="pattern-toolbar">
      <button type="button" @click="open(2)">Open the third photo</button>
      <label>
        <input v-model="locked" type="checkbox" name="lock" /> Lock navigation
      </label>
      <output>{{ status }} {{ index ?? "-" }}</output>
    </div>
    <div class="pattern-grid">
      <Fluidbox.Trigger
        v-for="photo in photos"
        :key="photo.id"
        :item="photo"
        class="pattern-thumb"
      >
        <img :src="photo.src" :alt="itemLabel(photo)" />
      </Fluidbox.Trigger>
    </div>
    <ol class="pattern-log">
      <li v-for="(type, entry) in log" :key="entry">{{ type }}</li>
    </ol>
  </Fluidbox.Root>
</template>
```

#### Solid

`programmatic.tsx`

```tsx
import { createSignal, For } from "solid-js";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidbox } from "@ogdakke/fluidbox/solid";
import { photos } from "./data";

function Toolbar(props: { locked: boolean; onLockedChange: (locked: boolean) => void }) {
  const fluidbox = useFluidbox();
  return (
    <div class="pattern-toolbar">
      <button type="button" onClick={() => fluidbox.open(2)}>
        Open the third photo
      </button>
      <label>
        <input
          type="checkbox"
          name="lock"
          checked={props.locked}
          onChange={(event) => props.onLockedChange(event.currentTarget.checked)}
        />{" "}
        Lock navigation
      </label>
      <output>
        {fluidbox.status} {fluidbox.index ?? "-"}
      </output>
    </div>
  );
}

export function Programmatic() {
  const [locked, setLocked] = createSignal(false);
  const [log, setLog] = createSignal<string[]>([]);
  const record = (event: Event) => setLog((types) => [...types, event.type].slice(-20));
  return (
    <Fluidbox.Root
      items={photos}
      // beforeitemchange comes before every move between items; canceling it keeps the viewer still.
      onBeforeItemChange={(event) => {
        record(event);
        if (locked()) event.preventDefault();
      }}
      onBeforeToggle={record}
      onToggle={record}
      onItemChange={record}
      onSettle={record}
    >
      <Toolbar locked={locked()} onLockedChange={setLocked} />
      <div class="pattern-grid">
        <For each={photos}>
          {(photo) => (
            <Fluidbox.Trigger item={photo} class="pattern-thumb">
              <img src={photo.src} alt={itemLabel(photo)} />
            </Fluidbox.Trigger>
          )}
        </For>
      </div>
      <ol class="pattern-log">
        <For each={log()}>{(type) => <li>{type}</li>}</For>
      </ol>
    </Fluidbox.Root>
  );
}
```

#### Angular

`programmatic.ts`

```ts
import { Component, signal } from "@angular/core";
import { itemLabel } from "@ogdakke/fluidbox";
import { FluidboxModule, type FluidboxItemChangeEvent } from "@ogdakke/fluidbox/angular";
import { photos } from "./data";

@Component({
  selector: "pattern-programmatic",
  standalone: true,
  imports: [FluidboxModule],
  template: `
    <div class="pattern-toolbar">
      <button type="button" (click)="fb.open(2)">
        Open the third photo
      </button>
      <label>
        <input
          type="checkbox"
          name="lock"
          #lock
          (change)="locked.set(lock.checked)"
        />
        Lock navigation
      </label>
      <output>{{ fb.state().status }} {{ fb.state().index ?? "-" }}</output>
    </div>
    <div
      fluidboxRoot
      #fb="fluidbox"
      [items]="photos"
      class="pattern-grid"
      (beforeToggle)="log($event)"
      (toggle)="log($event)"
      (beforeItemChange)="guard($event)"
      (itemChange)="log($event)"
      (settle)="log($event)"
    >
      @for (photo of photos; track photo.id) {
        <button [fluidboxTrigger]="photo" class="pattern-thumb">
          <img [src]="photo.src" [alt]="label(photo)" />
        </button>
      }
    </div>
    <ol class="pattern-log">
      @for (type of events(); track $index) {
        <li>{{ type }}</li>
      }
    </ol>
  `,
})
export class Programmatic {
  readonly photos = photos;
  readonly label = itemLabel;
  readonly locked = signal(false);
  readonly events = signal<string[]>([]);

  log(event: Event): void {
    this.events.update((events) => [...events, event.type].slice(-20));
  }

  // beforeItemChange comes before every move between items; canceling it keeps the viewer still.
  guard(event: FluidboxItemChangeEvent): void {
    this.log(event);
    if (this.locked()) event.preventDefault();
  }
}
```

#### Elements

`programmatic.html`

```html
<div class="pattern-toolbar">
  <button type="button" data-action="open-third">Open the third</button>
  <label><input type="checkbox" name="lock" /> Lock navigation</label>
  <output name="status">closed -</output>
</div>
<fluidbox-gallery class="pattern-grid">
  <fluidbox-item item-id="photo-aurora">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/aurora.svg" alt="Aurora over the tundra" />
    </button>
    <template slot="content">
      <img src="/art/aurora.svg" alt="Aurora over the tundra" width="1100" height="1650" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-ridge">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/ridge.svg" alt="Night ridge" />
    </button>
    <template slot="content">
      <img src="/art/ridge.svg" alt="Night ridge" width="1200" height="1500" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-fjord">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/fjord.svg" alt="Fjord in the blue hour" />
    </button>
    <template slot="content">
      <img src="/art/fjord.svg" alt="Fjord in the blue hour" width="1200" height="1600" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-lagoon">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/lagoon.svg" alt="Lagoon after sunset" />
    </button>
    <template slot="content">
      <img src="/art/lagoon.svg" alt="Lagoon after sunset" width="1800" height="1200" />
    </template>
  </fluidbox-item>
</fluidbox-gallery>
<ol class="pattern-log"></ol>
```

`programmatic.ts`

```ts
export function programmatic(root: HTMLElement) {
  const gallery = root.querySelector("fluidbox-gallery")!;
  const lock = root.querySelector<HTMLInputElement>('input[name="lock"]')!;
  const status = root.querySelector<HTMLOutputElement>('output[name="status"]')!;
  const log = root.querySelector<HTMLOListElement>(".pattern-log")!;

  root.querySelector('[data-action="open-third"]')!.addEventListener("click", () => {
    void gallery.open(2);
  });

  // beforeitemchange comes before every move between items; canceling it keeps the viewer still.
  gallery.addEventListener("beforeitemchange", (event) => {
    if (lock.checked) event.preventDefault();
  });

  for (const type of ["beforetoggle", "toggle", "beforeitemchange", "itemchange", "settle"]) {
    gallery.addEventListener(type, () => {
      log.append(Object.assign(document.createElement("li"), { textContent: type }));
      while (log.children.length > 20) log.firstElementChild!.remove();
    });
  }

  gallery.subscribe(({ status: current, index }) => {
    status.value = `${current} ${index ?? "-"}`;
  });
}
```

#### JavaScript

`programmatic.ts`

```ts
import { createFluidbox, itemLabel } from "@ogdakke/fluidbox";
import { photos } from "./data";

export function mountProgrammatic(root: HTMLElement) {
  root.insertAdjacentHTML(
    "beforeend",
    `<div class="pattern-toolbar">
      <button type="button" data-action="open-third">Open the third photo</button>
      <label><input type="checkbox" name="lock" /> Lock navigation</label>
      <output name="status"></output>
    </div>
    <div class="pattern-grid"></div>
    <ol class="pattern-log"></ol>`,
  );
  const openThird = root.querySelector<HTMLButtonElement>('[data-action="open-third"]')!;
  const lock = root.querySelector<HTMLInputElement>('input[name="lock"]')!;
  const status = root.querySelector<HTMLOutputElement>('output[name="status"]')!;
  const grid = root.querySelector<HTMLElement>(".pattern-grid")!;
  const log = root.querySelector<HTMLOListElement>(".pattern-log")!;

  const fluidbox = createFluidbox({ items: photos });

  // beforeitemchange comes before every move between items; canceling it keeps the viewer still.
  fluidbox.addEventListener("beforeitemchange", (event) => {
    if (lock.checked) event.preventDefault();
  });
  for (const type of [
    "beforetoggle",
    "toggle",
    "beforeitemchange",
    "itemchange",
    "settle",
  ] as const) {
    fluidbox.addEventListener(type, (event) => {
      const entry = document.createElement("li");
      entry.textContent = event.type;
      log.append(entry);
      while (log.children.length > 20) log.firstElementChild!.remove();
    });
  }

  const showStatus = () => {
    const { status: current, index } = fluidbox.state;
    status.value = `${current} ${index ?? "-"}`;
  };
  showStatus();
  fluidbox.subscribe(showStatus);
  openThird.addEventListener("click", () => void fluidbox.open(2));

  photos.forEach((photo, index) => {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "pattern-thumb";
    button.append(Object.assign(new Image(), { src: photo.src, alt: itemLabel(photo) }));
    grid.append(button);
    fluidbox.bind(button, index);
  });

  return () => {
    fluidbox.destroy();
    root.replaceChildren();
  };
}
```

- **Commands.** `open(target)`, `next()`, `previous()` and `close()` return a promise that resolves `true` when the command happened and `false` when it didn't. A target is an index or an id: `open(2)`, `open("photo-fjord")`. While open, `open` moves to the item.
- **State.** `status` (`closed`, `opening`, `open`, `closing`), `id`, `index`, `item`, `count`, `hasPrevious` and `hasNext`. Read it with `useFluidbox()` in the frameworks, `gallery.state` and `gallery.subscribe()` on custom elements, or `fluidbox.state` and `fluidbox.subscribe()` in plain JavaScript.
- **Events.** `beforetoggle` and `toggle` as the viewer opens and closes, like a popover's; `beforeitemchange`, `itemchange` and `settle` as it moves between items. Each has a `reason`: `"keyboard"`, `"pointer"`, `"trigger"`, `"api"` and so on. The framework roots take them as event props.
- **Veto.** `beforetoggle` and `beforeitemchange` fire before every command, whether it comes from a key, a gesture or your code. Call `event.preventDefault()` to cancel it.
