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
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ npm install @object-ui/react @object-ui/components

```tsx
import React from 'react'
import { SchemaRenderer } from '@object-ui/react'
import { PredicateScopeProvider, SchemaRenderer } from '@object-ui/react'
// Importing the package registers every default renderer as a side effect —
// there is no separate registration call.
import '@object-ui/components'
Expand All @@ -92,16 +92,28 @@ const schema = {
}

function App() {
const data = {
// Every key of this object becomes a root the schema's expressions can read —
// `stats` here is what answers `${stats.users}`.
const scope = {
stats: { users: 1234, revenue: "$56,789", orders: 432 }
}

return <SchemaRenderer schema={schema} data={data} />
return (
<PredicateScopeProvider scope={scope}>
<SchemaRenderer schema={schema} />
</PredicateScopeProvider>
)
}

export default App
```

Expression scope reaches the renderer through the provider, never through a prop on the
element. `SchemaRenderer` declares exactly one prop, `schema`, and forwards every other prop
it is handed to the component the schema names — so a value passed as `data={…}` is neither
read nor refused, and the expression that wanted it is returned as its own source text, with
nothing thrown and one line on the console.

### Bring your own backend

Use the shell and views without the full console infrastructure — your routing, your auth, your API:
Expand Down Expand Up @@ -171,6 +183,10 @@ docs render, a smoke test mounts, and AI agents use as a few-shot corpus.

## Copy-Paste Schemas

A `${name.…}` in any of these reads the root `name` off the scope the host published — see
["Basic Usage"](#basic-usage) for the provider that publishes one. A head name nothing
published is not an error: the expression is returned as its own source text.

#### 📝 Contact Form

```json
Expand Down Expand Up @@ -258,9 +274,9 @@ Object UI talks to any backend through one `DataSource` interface.
npm install @object-ui/data-objectstack
```

```typescript
```tsx
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
import { SchemaRenderer } from '@object-ui/react';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
import type { BaseSchema } from '@object-ui/types';

// Your page schema — "Render a schema" above writes one out in full.
Expand All @@ -271,10 +287,18 @@ const dataSource = createObjectStackAdapter({
token: 'your-auth-token'
});

// Use with any component
<SchemaRenderer schema={schema} dataSource={dataSource} />
// The adapter is injected through the provider — `SchemaRenderer` does not read
// a `dataSource` prop, it forwards it to the component the schema names.
<SchemaRendererProvider dataSource={dataSource}>
<SchemaRenderer schema={schema} />
</SchemaRendererProvider>
```

⛔ `dataSource` is the **adapter** — the object data renderers call `find()` on — and not an
expression root. It is a different channel from the expression scope above: publish the values
your `${…}` expressions read with `PredicateScopeProvider`, and inject the adapter your data
components query with `SchemaRendererProvider`.

### Custom Data Sources

Adapt any backend (REST, GraphQL, Firebase, …) by implementing `DataSource`:
Expand Down
38 changes: 31 additions & 7 deletions content/docs/guide/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ ObjectUI is organized as a PNPM monorepo with clear separation of concerns:
- **Contains**: Schema validation, expression evaluation, registries
- **Constraint**: No UI library dependencies, logic only
- **Features**:
- Expression engine (`visible: "${data.age > 18}"`)
- Expression engine (`visible: "${record.age > 18}"`)
- Schema registry and validation
- Event system

Expand Down Expand Up @@ -132,14 +132,19 @@ A backend system sends a JSON schema:

The `SchemaRenderer` component:

1. Receives the schema + data context
1. Receives the schema, and reads the expression scope off the context above it
2. Evaluates expressions (`${user.name}`)
3. Looks up the component type in the registry
4. Recursively renders child schemas
5. Handles events and state updates

The scope does **not** arrive as a prop. `SchemaRenderer` declares exactly one prop, `schema`,
and forwards everything else it is handed to the component the schema names — so a `data={…}`
written on the element is neither read nor refused. The host publishes its values with
`PredicateScopeProvider`, and every key it publishes becomes a root the expressions can read:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import { PredicateScopeProvider, SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
Expand All @@ -153,12 +158,25 @@ const schema: BaseSchema = {
}

function App() {
const data = { user: { name: 'Alice' } }
const scope = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
return (
<PredicateScopeProvider scope={scope}>
<SchemaRenderer schema={schema} />
</PredicateScopeProvider>
)
}
```

An app built on `@object-ui/app-shell` does not mount this provider itself: the shell's
`ExpressionProvider` already feeds the same channel with the signed-in `user` (also readable as
`current_user`) and `features`. On top of what the host published, the renderer supplies
`record` — the row a record surface is bound to — and `page`, the page-local variables.

⛔ `SchemaRendererProvider`'s `dataSource` is **not** an expression root. It carries the host's
`DataSource` *adapter*, the object data renderers call `find()` on; the two are different
channels on purpose.

### 3. Component Registry Lookup

The registry maps type strings to React components:
Expand Down Expand Up @@ -244,11 +262,17 @@ ObjectUI includes a powerful expression engine for dynamic UIs:
{
"type": "button",
"label": "Submit",
"visible": "${form.isValid && !form.isSubmitting}",
"disabled": "${form.isSubmitting}"
"visible": "${current_user.role === 'admin'}",
"disabled": "${record.status === 'locked'}"
}
```

`current_user` is the signed-in user the host's `ExpressionProvider` publishes; `record` is the
row a record surface is bound to, and is the only spelling a row field has — the bare shorthand
(`status`) and the wrong-layer `data.status` were both retired on runtime record surfaces
(objectui#5330 phase 2). A head name outside the scope is not refused: the predicate is
unevaluable, this surface fails soft, and the node is shown on every row.

A button's text key is `label`, and `text` is not a `ButtonSchema` key at all. Nothing
refuses the misspelling either: `BaseSchema` is `.passthrough()`, so the validator KEEPS
the unknown key, and the renderer — which reads `schema.label` — never looks at it.
Expand Down
Loading