Skip to content

Commit 50abaa6

Browse files
committed
Enhance engine documentation with advanced scenarios, dynamic registry, and expression engine support
1 parent 5acafe3 commit 50abaa6

File tree

1 file changed

+116
-3
lines changed

1 file changed

+116
-3
lines changed

content/prompts/development/engine.prompt.md

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,12 @@ import { PageComponent } from '@objectstack/spec/ui';
9898

9999
function withEngine(Widget: React.ComponentType) {
100100
return (props: { config: PageComponent }) => {
101-
// 1. Auto-handle Visibility
101+
// 1. Auto-handle Visibility (Expressions like: "record.status == 'closed'")
102102
if (!evalVisibility(props.config.visibility)) return null;
103103

104-
// 2. Error Boundary
104+
// 2. Error Boundary per functionality block
105105
return (
106-
<ErrorBoundary>
106+
<ErrorBoundary fallback={({error}) => <AtomError message={error.message} />}>
107107
<Suspense fallback={<AtomSpinner />}>
108108
<Widget {...props} />
109109
</Suspense>
@@ -112,3 +112,116 @@ function withEngine(Widget: React.ComponentType) {
112112
};
113113
}
114114
```
115+
116+
---
117+
118+
## 4. Advanced Scenarios & Solutions
119+
120+
### A. Dynamic Registry (Lazy Loading)
121+
Don't bundle entire component libraries. Use `React.lazy` mapping.
122+
```typescript
123+
// registry.ts
124+
export const widgetRegistry = {
125+
'page:tabs': lazy(() => import('@objectstack/components/structure/PageTabs')),
126+
'record:details': lazy(() => import('@objectstack/components/record/RecordDetails')),
127+
// ... extended by plugins
128+
};
129+
```
130+
131+
### B. Inter-Component Communication (Event Bus)
132+
Start Scenario: A "Save" button in `page:header` needs to submit a form in `record:details`.
133+
* **Solution:** Use a scoped `EventBus` provided by `PageContext`.
134+
* **Protocol:** `bus.emit('record:save')` -> `RecordDetails` listens and submits.
135+
136+
### C. Form State (React Hook Form + Zod)
137+
Scenario: Validating fields based on metadata rules (`maxLength`, `min`, `regex`).
138+
* **Solution:** Automatically generate Zod Validation Schema from `field.zod.ts` definitions at runtime.
139+
* **Pattern:** `useZodForm(objectSchema)` hook that bridges Metadata -> React Hook Form.
140+
141+
### D. Field-Level Security (FLS)
142+
Scenario: User sees the record but lacks permission to view 'Salary' field.
143+
* **Solution:** `DataEngine` must filter restricted fields *before* they reach the UI component, or `RecordDetails` component must check `perm.canRead(field)` for each user.
144+
145+
### E. Theme Injection
146+
Scenario: App branding (`branding.primaryColor`) must apply to all Buttons.
147+
* **Solution:** Convert `App.branding` values into CSS Variables (`--os-primary: #ff0000`) injected at the root `<LayoutProvider>`. All atoms (`atom:button`) consume these variables.
148+
149+
---
150+
151+
## 5. The Expression Engine
152+
153+
The engine must support "Metadata Expressions" to allow logic without compilation.
154+
155+
### A. Syntax
156+
Use generic string interpolation `{ !... }` or specific prefixes.
157+
* **Binding:** `"{!record.amount}"` (Values)
158+
* **Logic:** `"{!record.status} == 'closed'"` (Boolean)
159+
* **Global:** `"{!user.name}"`, `"{!today}"`
160+
161+
### B. Implementation (Safe Eval)
162+
Use a specialized parser (like `filtrex` or `jsep`) instead of `eval()`.
163+
```typescript
164+
// utils/eval.ts
165+
export function evalExpression(expr: string, context: any): any {
166+
if (!expr.startsWith('{!') || !expr.endsWith('}')) return expr; // Literal
167+
const logic = expr.slice(2, -1);
168+
return safeEvaluate(logic, context); // context = { record, user, global }
169+
}
170+
```
171+
172+
### C. Reactive Binding
173+
Expressions must re-evaluate when `context` changes.
174+
* **Hook:** `useExpression(expressionString, dependencyArray)`
175+
176+
---
177+
178+
## 6. Slot & Component Injection
179+
180+
Layouts often nest components. The engine must map `PageComponent.children` (Array) to React `props.children`.
181+
182+
### A. The "Slot" Prop
183+
Standard blocks (like `page:card` or `page:tabs`) accept a `children` prop in strict React, but in metadata, they are just nested IDs.
184+
185+
### B. Recursive Rendering Logic
186+
The `PageRenderer` must handle the recursion, not the component.
187+
```typescript
188+
// Correct Pattern
189+
const CardBlock = ({ config, children }) => (
190+
<div className="card">
191+
<div className="card-body">
192+
{/* The engine has already converted config.children into React Elements */}
193+
{children}
194+
</div>
195+
</div>
196+
);
197+
```
198+
199+
### C. Named Slots (Advanced)
200+
Some components have multiple areas (e.g., `header`, `footer`).
201+
* **Metadata:** `regions: [{ name: "header", components: [...] }, { name: "footer", components: [...] }]`
202+
* **React:** `<PageHeader actions={<RegionRenderer region="header" />} />`
203+
204+
---
205+
206+
## 7. Dirty State Management
207+
208+
Protocol for handling unsaved changes across independent widgets.
209+
210+
### A. Distributed State
211+
Each form widget manages its own state (e.g., `react-hook-form`).
212+
213+
### B. The "Dirty" Registry
214+
A context where widgets register their dirty status.
215+
```typescript
216+
// DirtyContext.tsx
217+
const registerDirty = (id: string, isDirty: boolean) => { ... }
218+
219+
// Usage in Widget
220+
useEffect(() => {
221+
registerDirty(props.id, formState.isDirty);
222+
}, [formState.isDirty]);
223+
```
224+
225+
### C. Navigation Guard
226+
Listen to `beforeunload` and Router events. If `dirtyRegistry` has any `true` values, block navigation.
227+

0 commit comments

Comments
 (0)