Skip to content
Open
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
12 changes: 12 additions & 0 deletions framework/core/composables/useBlade/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface UseBladeReturn<TOptions = Record<string, unknown>> {
// Identity (read-only) — runtime error outside blade context
readonly id: ComputedRef<string>;
readonly param: ComputedRef<string | undefined>;
/** param of the direct child blade opened from this blade, or undefined. */
readonly activeChildParam: ComputedRef<string | undefined>;
readonly options: ComputedRef<TOptions | undefined>;
readonly query: ComputedRef<Record<string, string> | undefined>;
readonly closable: ComputedRef<boolean>;
Expand Down Expand Up @@ -146,6 +148,15 @@ export function useBlade<TOptions = Record<string, unknown>>(): UseBladeReturn<T
return descriptor!.value.name;
});

// param of the direct child blade (the entity currently open from this blade).
// Reactive over the stack and reload-safe: on reload the child blade is
// restored with its param. Returns undefined outside blade context.
const activeChildParam = computed<string | undefined>(() => {
if (!descriptor) return undefined;
const selfId = descriptor.value.id;
return bladeStack.blades.value.find((b) => b.parentId === selfId)?.param;
});

// ── Blade Data ──────────────────────────────────────────────────────────

let _bladeDataProvided = false;
Expand Down Expand Up @@ -319,6 +330,7 @@ export function useBlade<TOptions = Record<string, unknown>>(): UseBladeReturn<T
return {
id,
param,
activeChildParam,
options,
query,
closable,
Expand Down
43 changes: 34 additions & 9 deletions framework/core/composables/useBlade/useBlade.docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,16 @@ Without the generic, `options.value` defaults to `Record<string, unknown> | unde

These are reactive `ComputedRef` values that reflect the current blade's state. Accessing them outside blade context throws a runtime error.

| Property | Type | Description |
| ---------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `ComputedRef<string>` | The current blade's unique ID within the blade stack |
| `param` | `ComputedRef<string \| undefined>` | Route parameter passed when the blade was opened |
| `options` | `ComputedRef<TOptions \| undefined>` | Additional options object passed to the blade. Type is `Record<string, unknown>` by default; provide a generic to get a typed ref: `useBlade<MyOptions>()` |
| `query` | `ComputedRef<Record<string, string> \| undefined>` | URL query parameters scoped to this blade |
| `closable` | `ComputedRef<boolean>` | `true` when this blade has a parent (i.e., can be closed) |
| `expanded` | `ComputedRef<boolean>` | `true` when this blade is the active (rightmost) blade |
| `name` | `ComputedRef<string>` | The blade's registered component name |
| Property | Type | Description |
| ------------------ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `ComputedRef<string>` | The current blade's unique ID within the blade stack |
| `param` | `ComputedRef<string \| undefined>` | Route parameter passed when the blade was opened |
| `activeChildParam` | `ComputedRef<string \| undefined>` | The `param` of the direct child blade opened from this blade (the entity currently shown in the details pane), or `undefined`. Reactive and reload-safe. Bind it to a list table's `:active-item-id` to highlight the open entity's row — it stays correct across reloads, unlike a local ref. |
| `options` | `ComputedRef<TOptions \| undefined>` | Additional options object passed to the blade. Type is `Record<string, unknown>` by default; provide a generic to get a typed ref: `useBlade<MyOptions>()` |
| `query` | `ComputedRef<Record<string, string> \| undefined>` | URL query parameters scoped to this blade |
| `closable` | `ComputedRef<boolean>` | `true` when this blade has a parent (i.e., can be closed) |
| `expanded` | `ComputedRef<boolean>` | `true` when this blade is the active (rightmost) blade |
| `name` | `ComputedRef<string>` | The blade's registered component name |

#### Navigation Methods (work everywhere)

Expand Down Expand Up @@ -387,6 +388,30 @@ onDeactivated(() => {
});
```

### Highlight the row of the open child blade

Bind `activeChildParam` to the table's `active-item-id` so the row of the currently open details blade is highlighted — including after a page reload:

```vue
<script setup lang="ts">
const { openBlade, activeChildParam } = useBlade();

function onRowClick(e: { data: { id: string } }) {
openBlade({ name: "Details", param: e.data.id });
}
</script>

<template>
<VcDataTable
:items="items"
:active-item-id="activeChildParam"
@row-click="onRowClick"
>
<!-- columns -->
</VcDataTable>
</template>
```

### Error Management

Display or clear error banners on the blade:
Expand Down
67 changes: 67 additions & 0 deletions framework/core/composables/useBlade/useBlade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,70 @@ describe("useBlade() lifecycle hooks", () => {
expect(() => result.onDeactivated(() => {})).toThrow(/onDeactivated\(\) requires blade context/);
});
});

// ── activeChildParam ──────────────────────────────────────────────────────────

describe("useBlade() activeChildParam", () => {
it("returns the param of the direct child blade", () => {
const stack = createMockBladeStack();
(stack.blades as unknown as { value: unknown[] }).value = [
{ id: "test-blade", parentId: "root", visible: true },
{ id: "child", parentId: "test-blade", param: "order-7", visible: true },
];
const { result } = mountWithBladeContext(() => useBlade(), {
descriptor: { id: "test-blade" },
stack,
});
expect(result.activeChildParam.value).toBe("order-7");
});

it("is undefined when the blade has no child", () => {
const stack = createMockBladeStack();
(stack.blades as unknown as { value: unknown[] }).value = [{ id: "test-blade", parentId: "root", visible: true }];
const { result } = mountWithBladeContext(() => useBlade(), {
descriptor: { id: "test-blade" },
stack,
});
expect(result.activeChildParam.value).toBeUndefined();
});

it("is undefined when the child blade has no param (e.g. add-new)", () => {
const stack = createMockBladeStack();
(stack.blades as unknown as { value: unknown[] }).value = [
{ id: "test-blade", parentId: "root", visible: true },
{ id: "child", parentId: "test-blade", param: undefined, visible: true },
];
const { result } = mountWithBladeContext(() => useBlade(), {
descriptor: { id: "test-blade" },
stack,
});
expect(result.activeChildParam.value).toBeUndefined();
});

it("reacts to a child blade being opened and closed", async () => {
const stack = createMockBladeStack();
const blades = stack.blades as unknown as { value: unknown[] };
blades.value = [{ id: "test-blade", parentId: "root", visible: true }];
const { result } = mountWithBladeContext(() => useBlade(), {
descriptor: { id: "test-blade" },
stack,
});
expect(result.activeChildParam.value).toBeUndefined();

blades.value = [
{ id: "test-blade", parentId: "root", visible: true },
{ id: "child", parentId: "test-blade", param: "p-1", visible: true },
];
await nextTick();
expect(result.activeChildParam.value).toBe("p-1");

blades.value = [{ id: "test-blade", parentId: "root", visible: true }];
await nextTick();
expect(result.activeChildParam.value).toBeUndefined();
});

it("is undefined outside blade context (no throw)", () => {
const { result } = mountWithoutBladeContext(() => useBlade());
expect(result.activeChildParam.value).toBeUndefined();
});
});
Loading