softr-vibe-coding 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/SKILL.md +480 -0
- package/bin/cli.js +140 -0
- package/datasources/airtable.md +91 -0
- package/datasources/bigquery.md +36 -0
- package/datasources/clickup.md +82 -0
- package/datasources/coda.md +44 -0
- package/datasources/fields.md +203 -0
- package/datasources/google-sheets.md +46 -0
- package/datasources/hubspot.md +51 -0
- package/datasources/monday.md +52 -0
- package/datasources/notion.md +56 -0
- package/datasources/overview.md +41 -0
- package/datasources/reading.md +222 -0
- package/datasources/rest-api.md +206 -0
- package/datasources/shared-patterns.md +9 -0
- package/datasources/smartsuite.md +39 -0
- package/datasources/softr-database.md +47 -0
- package/datasources/sql-database.md +48 -0
- package/datasources/supabase.md +38 -0
- package/datasources/writing.md +256 -0
- package/datasources/xano.md +37 -0
- package/package.json +40 -0
- package/references/advanced-integrations.md +69 -0
- package/references/airtable-automations.md +350 -0
- package/references/anti-patterns.md +86 -0
- package/references/common-patterns.md +102 -0
- package/references/helper-blocks.md +370 -0
- package/references/quick-reference.md +207 -0
- package/ui-ux-guidelines.md +746 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Writing Data
|
|
2
|
+
|
|
3
|
+
Record mutations, file uploads, linked record format, and cross-table operations.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [How Actions Work (Studio's Actions Tab)](#how-actions-work-studios-actions-tab)
|
|
8
|
+
- [Record Mutations](#record-mutations)
|
|
9
|
+
- [File Uploads](#file-uploads)
|
|
10
|
+
- [Linked Record Format for Mutations](#linked-record-format-for-mutations)
|
|
11
|
+
- [Writing to Field Types](#writing-to-field-types)
|
|
12
|
+
- [Cross-Table Operations](#cross-table-operations)
|
|
13
|
+
|
|
14
|
+
## How Actions Work (Studio's Actions Tab)
|
|
15
|
+
|
|
16
|
+
Every Vibe Coding block in Softr Studio has an **Actions tab** alongside Chat / Source / Content / Visibility. The Actions tab is a **read-only inspector** of the Create / Update / Delete operations the platform inferred from your code.
|
|
17
|
+
|
|
18
|
+
Each Action's "FIELDS USED" list mirrors the aliases in your `q.select()` mapping. Actions are not a separately-managed system:
|
|
19
|
+
|
|
20
|
+
- The platform parses your block's source on every save
|
|
21
|
+
- For each `useRecordCreate` / `useRecordUpdate` / `useRecordDelete` hook, it auto-derives a corresponding Action
|
|
22
|
+
- The fields list comes from your `q.select` aliases used in the mutation payload
|
|
23
|
+
- Cosmetic AND structural code edits both update the Action automatically -- you do NOT need to re-prompt the AI assistant after editing code
|
|
24
|
+
- There is no manual delete control; to remove an Action, remove the mutation hook from the code
|
|
25
|
+
|
|
26
|
+
The `enabled` boolean on a mutation hook is a combined signal — it's `true` only when BOTH conditions are met:
|
|
27
|
+
|
|
28
|
+
1. **The Action was successfully derived from the code** (parser side). Causes of failure here:
|
|
29
|
+
- The hook is declared after a conditional `return`, so it doesn't run on every render
|
|
30
|
+
- `q.select` is built dynamically rather than from string-literal mappings
|
|
31
|
+
- The block hasn't been connected to a data source yet
|
|
32
|
+
- `useRecordUpdate.mutate()` is called with a flat payload (`{ recordId, status: ... }`) instead of the nested shape (`{ recordId, fields: { status: ... } }`) -- the parser ignores flat field references, so no Action gets created
|
|
33
|
+
|
|
34
|
+
2. **The currently logged-in (or previewed-as) user has permission to perform the action on the data source** (permissions side). Per the official Softr docs, "`enabled` reflects user permissions." So even with a perfectly-derived Action, `enabled` will be `false` if the previewed user's group lacks write access to the connected table.
|
|
35
|
+
|
|
36
|
+
**Critical debugging implication:** when `enabled` stays `false` and Studio's Actions tab DOES show the action listed (parser succeeded), the cause is permissions — not code. Three places to check, in order:
|
|
37
|
+
|
|
38
|
+
- **Block's Visibility tab** (right panel) — confirm the previewed user's group is allowed to see / interact with this block.
|
|
39
|
+
- **Studio → Users → Data Restrictions → Global data restrictions** — an app-wide layer that limits what data users can interact with across the whole app, applied on top of block-level settings. Easy to miss because it's under Users (not on the block). If a Global restriction exists on the target table for the user's group, every mutation against that table in every block silently fails — no error, just `enabled: false`. Open this tab and either confirm there are no restrictions or that the worker / target group has the access they need.
|
|
40
|
+
- **Data-source connection PAT scope** — if the PAT was granted with read-only scope, every write fails regardless of UI permissions. Reconnect with write scope if needed.
|
|
41
|
+
|
|
42
|
+
Verified by direct experiment (April 2026): adding a new field to `q.select` + `mutate()` payload and saving the code propagates to the Actions tab automatically and writes successfully to the database with no manual configuration.
|
|
43
|
+
|
|
44
|
+
Because the parser only inspects your hooks and `q.select` mappings (not the JSX tree), inputs rendered conditionally inside `<Dialog>`, `<Sheet>`, or any subtree gated by state are still bound to the Action correctly. Verified by direct experiment, May 2026.
|
|
45
|
+
|
|
46
|
+
## Record Mutations
|
|
47
|
+
|
|
48
|
+
All mutation hooks expose an `enabled` boolean. You must check it before rendering any mutation UI or calling the mutate function.
|
|
49
|
+
|
|
50
|
+
### useRecordCreate
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
import { useRecordCreate, q } from "@/lib/datasource";
|
|
54
|
+
import { toast } from "sonner";
|
|
55
|
+
|
|
56
|
+
var createRecord = useRecordCreate({
|
|
57
|
+
fields: q.select({ name: "FIELD_ID1", email: "FIELD_ID2" }),
|
|
58
|
+
onSuccess: function(newRecord) { toast.success("Created!"); },
|
|
59
|
+
onError: function(error) { toast.error(error.message); },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Usage (gate on enabled):
|
|
63
|
+
if (createRecord.enabled) {
|
|
64
|
+
createRecord.mutate({ name: "Jane", email: "jane@example.com" });
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### useRecordUpdate
|
|
69
|
+
|
|
70
|
+
```jsx
|
|
71
|
+
import { useRecordUpdate, q } from "@/lib/datasource";
|
|
72
|
+
|
|
73
|
+
var updateRecord = useRecordUpdate({
|
|
74
|
+
fields: q.select({ status: "FIELD_ID1" }),
|
|
75
|
+
onSuccess: function(updatedRecord) {
|
|
76
|
+
refetch().then(function() { toast.success("Updated!"); });
|
|
77
|
+
},
|
|
78
|
+
onError: function(error) { toast.error(error.message); },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Usage -- MUST use recordId, NOT id, AND fields MUST be nested:
|
|
82
|
+
updateRecord.mutate({
|
|
83
|
+
recordId: "RECORD_ID",
|
|
84
|
+
fields: { status: "active" },
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### CRITICAL: Two parser requirements for `useRecordUpdate`
|
|
89
|
+
|
|
90
|
+
Softr's Action parser is strict about both the **method name** and the **payload shape**. Get either wrong and the Action is never derived, `enabled` stays `false`, and any UI gated on it silently does nothing — no error, no warning, console just shows `enabled: false, error: null, status: "idle"`.
|
|
91
|
+
|
|
92
|
+
**Requirement 1 — Call `.mutate()`, NOT `.mutateAsync()`.** The parser scans for the literal `.mutate(` token to detect mutation call sites. `.mutateAsync()` runs fine at runtime (it's just a Promise wrapper), but the parser ignores it and no Update Action gets created. Pass per-call success/error handlers as the second argument (react-query convention):
|
|
93
|
+
|
|
94
|
+
```jsx
|
|
95
|
+
// CORRECT — parser sees `.mutate(`, derives the Action
|
|
96
|
+
updateRecord.mutate(
|
|
97
|
+
{ recordId: id, fields: { status: optionId } },
|
|
98
|
+
{
|
|
99
|
+
onSuccess: function() { toast.success("Saved"); },
|
|
100
|
+
onError: function(err) { toast.error(err.message); },
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// WRONG — parser ignores `.mutateAsync(`, Action never created
|
|
105
|
+
updateRecord.mutateAsync({ recordId: id, fields: { status: optionId } })
|
|
106
|
+
.then(function() { toast.success("Saved"); });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Requirement 2 — Payload must be `{ recordId, fields: {...} }` — not flat.** Field values must be nested inside a `fields: {...}` object. The flat form (`mutate({ recordId, status: "active" })`) can succeed at runtime, but the parser doesn't see field references inside it, so no Action is derived:
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
// CORRECT
|
|
113
|
+
updateRecord.mutate({ recordId: id, fields: { status: optionId } }, { onSuccess, onError });
|
|
114
|
+
|
|
115
|
+
// WRONG — Action parser ignores this, hook stays disabled
|
|
116
|
+
updateRecord.mutate({ recordId: id, status: optionId }, { onSuccess, onError });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Verified by direct experiment (May 2026): Softr's Studio AI assistant emits both `.mutate()` AND the nested payload shape — and that combination is what produces a derived Update Action. Switching either back to its alternative form (`.mutateAsync()` or flat payload) disables the hook.
|
|
120
|
+
|
|
121
|
+
### useRecordDelete
|
|
122
|
+
|
|
123
|
+
```jsx
|
|
124
|
+
import { useRecordDelete } from "@/lib/datasource";
|
|
125
|
+
|
|
126
|
+
var deleteRecord = useRecordDelete({
|
|
127
|
+
onSuccess: function(result) {
|
|
128
|
+
refetch().then(function() { toast.success("Deleted!"); });
|
|
129
|
+
},
|
|
130
|
+
onError: function(error) { toast.error(error.message); },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Usage -- pass just the string, NOT an object:
|
|
134
|
+
deleteRecord.mutate("RECORD_ID");
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Three q.select() Mappings Pattern
|
|
138
|
+
|
|
139
|
+
For blocks that read, create, and update, use separate mappings:
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
// 1. READ -- includes everything (formulas, lookups, linked records)
|
|
143
|
+
var select = q.select({
|
|
144
|
+
name: "FIELD_1", email: "FIELD_2",
|
|
145
|
+
score: "FORMULA_FIELD", // read-only OK here
|
|
146
|
+
department: "LOOKUP_FIELD", // read-only OK here
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 2. CREATE -- only writable fields
|
|
150
|
+
var createFields = q.select({ name: "FIELD_1", email: "FIELD_2" });
|
|
151
|
+
|
|
152
|
+
// 3. UPDATE -- only writable fields that can be edited
|
|
153
|
+
var updateFields = q.select({ name: "FIELD_1", email: "FIELD_2" });
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## File Uploads
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
import { useUpload } from "@/lib/datasource";
|
|
160
|
+
|
|
161
|
+
var upload = useUpload();
|
|
162
|
+
|
|
163
|
+
// Single file:
|
|
164
|
+
upload.uploadAsync(file).then(function(results) {
|
|
165
|
+
var result = results[0];
|
|
166
|
+
if (result.status === "completed") {
|
|
167
|
+
// result.url = uploaded file URL, result.file.name = original filename
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Combine with record creation:
|
|
172
|
+
upload.uploadAsync(file).then(function(results) {
|
|
173
|
+
var result = results[0];
|
|
174
|
+
if (result.status === "completed") {
|
|
175
|
+
createRecord.mutate({
|
|
176
|
+
name: "Document",
|
|
177
|
+
attachment: { filename: result.file.name, url: result.url },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Linked Record Format for Mutations
|
|
184
|
+
|
|
185
|
+
```jsx
|
|
186
|
+
// CORRECT -- Array of { id } objects
|
|
187
|
+
createRecord.mutate({
|
|
188
|
+
parentAccount: [{ id: "RECORD_ID_1" }],
|
|
189
|
+
teamMembers: [{ id: "MEMBER_1" }, { id: "MEMBER_2" }],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// WRONG -- Plain string or array of strings
|
|
193
|
+
parentAccount: "RECORD_ID_1" // Won't work
|
|
194
|
+
teamMembers: ["MEMBER_1"] // Won't work
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Writing to Field Types
|
|
198
|
+
|
|
199
|
+
Different Softr field types accept different value shapes in mutation payloads. The shape returned when you READ a field is often different from the shape you must SEND when you WRITE.
|
|
200
|
+
|
|
201
|
+
### Dropdown / Single Select (Softr Database)
|
|
202
|
+
|
|
203
|
+
Write the option's UUID as a **plain string**, not an object:
|
|
204
|
+
|
|
205
|
+
```jsx
|
|
206
|
+
// CORRECT -- plain string UUID
|
|
207
|
+
createRecord.mutate({
|
|
208
|
+
status: "822b8d69-3af4-47b4-90eb-3a80c5d1b85c",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// WRONG -- object form (returned on read, but rejected on write)
|
|
212
|
+
createRecord.mutate({
|
|
213
|
+
status: { id: "822b8d69-3af4-47b4-90eb-3a80c5d1b85c", label: "Active" },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// WRONG -- display label
|
|
217
|
+
createRecord.mutate({
|
|
218
|
+
status: "Active",
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Option UUIDs are stable. Three ways to retrieve them:
|
|
223
|
+
|
|
224
|
+
- **AI scaffolding** -- Softr's AI assistant in Studio inlines them automatically into `<SelectItem value="...">` when generating a form
|
|
225
|
+
- **Network inspector** -- DevTools -> Network -> filter `tablespace-with-tables` returns the full `choices` array for any SELECT field (see [fields.md](fields.md#field-inspector-block) for the full technique). Pasting this JSON into an AI assistant chat is the most reliable way to share UUIDs without transcription errors.
|
|
226
|
+
- **Runtime scan** -- learn them at runtime from already-loaded records (useful when the block must work in environments where UUIDs are not known at code time)
|
|
227
|
+
|
|
228
|
+
Verified by direct experiment (April 2026) for `useRecordCreate`. The same pattern is expected to apply to `useRecordUpdate` but has not been independently verified.
|
|
229
|
+
|
|
230
|
+
### Linked Record
|
|
231
|
+
|
|
232
|
+
Array of `{ id }` objects. See "Linked Record Format for Mutations" above.
|
|
233
|
+
|
|
234
|
+
### Text / Email / URL / Phone
|
|
235
|
+
|
|
236
|
+
Plain string. To clear a value, both `null` and `""` work for Softr Database text fields (verified by direct experiment, May 2026, for `useRecordUpdate`). Behavior on other data sources has not been independently verified.
|
|
237
|
+
|
|
238
|
+
## Cross-Table Operations
|
|
239
|
+
|
|
240
|
+
`useRecordCreate`, `useRecordUpdate`, and `useRecordDelete` only work with the block's configured datasource. To write to a different table, use the **Softr Database REST API** via `fetch()`:
|
|
241
|
+
|
|
242
|
+
**Base URL:** `https://tables-api.softr.io/api/v1/databases/{databaseId}/tables/{tableId}/records`
|
|
243
|
+
|
|
244
|
+
**Authentication:** `Softr-Api-Key` header with a Personal Access Token.
|
|
245
|
+
|
|
246
|
+
**Create:** POST with `{ fields: { fieldId1: "value1" } }`
|
|
247
|
+
**Read:** GET with `?limit=200&fieldNames=true`
|
|
248
|
+
**Update:** PATCH `/{recordId}` with `{ fields: { fieldId1: "newValue" } }`
|
|
249
|
+
|
|
250
|
+
Notes:
|
|
251
|
+
- API key is exposed in client-side code -- acceptable for internal portals only
|
|
252
|
+
- When updating linked records or multi-selects, read existing values first, merge, then write
|
|
253
|
+
- Use `fieldNames=true` on GET for human-readable field names
|
|
254
|
+
- Rate limits: Reads 40 req/s, Writes 30 req/s
|
|
255
|
+
|
|
256
|
+
Verified by direct experiment (May 2026): POST to this endpoint with field IDs as keys writes successfully, returning HTTP 200 and the full record JSON. The endpoint uses the same field-ID format and the same value shapes as `useRecordCreate` -- plain string UUID for dropdown writes; the response returns the dropdown value as a `{id, label}` object (matching the read shape).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Xano
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Xano is a no-code backend-as-a-service (BaaS) built on PostgreSQL. Available on Professional plans and above.
|
|
5
|
+
|
|
6
|
+
## Connection Setup
|
|
7
|
+
1. In your Xano workspace, enable the **Database Connector** add-on.
|
|
8
|
+
2. Note the public IP address and credentials (Host, User, Password) from the Database Connector settings.
|
|
9
|
+
3. In the Softr admin, go to your data source settings and select Xano.
|
|
10
|
+
4. Enter the credentials: Host, User, and Password.
|
|
11
|
+
5. Use **Full Access** credentials (recommended) for read/write, or **Read-Only** for display-only apps.
|
|
12
|
+
6. **Whitelist Softr IP addresses** in your Xano firewall settings:
|
|
13
|
+
- `3.120.79.212`
|
|
14
|
+
- `3.123.159.186`
|
|
15
|
+
- `52.58.246.121`
|
|
16
|
+
7. Once connected, all workspaces and tables in that Xano instance are available.
|
|
17
|
+
|
|
18
|
+
## Vibe Coding Field IDs
|
|
19
|
+
Use the Field Inspector block to determine exact field IDs for this data source.
|
|
20
|
+
|
|
21
|
+
## Supported Fields
|
|
22
|
+
Field support follows PostgreSQL column types since Softr connects via direct database access. Standard column types (text, integer, boolean, timestamp, etc.) are writable. Computed/generated columns are read-only.
|
|
23
|
+
|
|
24
|
+
## Rate Limits
|
|
25
|
+
No Softr-imposed rate limits. Xano handles concurrency at the database level. Unlimited records and unlimited concurrent users.
|
|
26
|
+
|
|
27
|
+
## Gotchas
|
|
28
|
+
- **Direct database access, not API.** Softr connects to Xano's PostgreSQL database directly via the Database Connector. It does NOT go through Xano's custom API endpoints. Business logic in Xano API stacks will not execute on Softr reads/writes.
|
|
29
|
+
- **Database Connector must be enabled.** This is a separate add-on in Xano -- the connection will fail without it.
|
|
30
|
+
- **IP whitelisting is required.** If you skip whitelisting the three Softr IPs, connections will be refused.
|
|
31
|
+
- **Full Access vs Read-Only credentials** determine whether Softr can write data. Choose based on your use case.
|
|
32
|
+
|
|
33
|
+
## Best For
|
|
34
|
+
- Scalable applications with complex backend logic in Xano
|
|
35
|
+
- Apps that need unlimited records and concurrent users
|
|
36
|
+
- Projects where Xano handles business logic via API stacks and Softr provides the frontend
|
|
37
|
+
- Teams wanting a dedicated backend with PostgreSQL power and a no-code UI layer
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "softr-vibe-coding",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Claude Code skill for generating production-ready Softr Vibe Coding blocks (JSX). Installs into ~/.claude/skills/ and auto-updates on each Claude Code session.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"softr-vibe-coding": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"SKILL.md",
|
|
11
|
+
"ui-ux-guidelines.md",
|
|
12
|
+
"references/",
|
|
13
|
+
"datasources/",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"claude-skill",
|
|
21
|
+
"softr",
|
|
22
|
+
"vibe-coding",
|
|
23
|
+
"no-code",
|
|
24
|
+
"jsx",
|
|
25
|
+
"react"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://github.com/leo-softr/Softr-Vibe-Coding-Block-Claude-Skill#readme",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/leo-softr/Softr-Vibe-Coding-Block-Claude-Skill.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/leo-softr/Softr-Vibe-Coding-Block-Claude-Skill/issues"
|
|
34
|
+
},
|
|
35
|
+
"author": "leo-softr <leo@softr.io>",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Advanced Integrations
|
|
2
|
+
|
|
3
|
+
Patterns for embedding third-party libraries that ship their own global CSS inside a Softr Vibe Coding block without style bleed.
|
|
4
|
+
|
|
5
|
+
## CSS Isolation via Shadow DOM
|
|
6
|
+
|
|
7
|
+
**Problem:** Third-party libraries ship CSS with generic selectors like `.marker`, `.editor`, `.btn`. Inside a Softr page, those selectors bleed into the rest of the page or get clobbered by Softr's own styles. Scoping manually is impractical.
|
|
8
|
+
|
|
9
|
+
**Solution:** Mount the library inside a **Shadow DOM** attached to a host element. The shadow root is a fully isolated DOM subtree -- the library's CSS cannot leak out, and Softr's styles cannot leak in. React renders the host wrapper; the library lives inside the shadow.
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
import { useEffect, useRef } from "react";
|
|
13
|
+
|
|
14
|
+
export default function Block() {
|
|
15
|
+
var hostRef = useRef(null);
|
|
16
|
+
|
|
17
|
+
useEffect(function() {
|
|
18
|
+
var host = hostRef.current;
|
|
19
|
+
if (!host || host.shadowRoot) return; /* already initialized */
|
|
20
|
+
|
|
21
|
+
var shadow = host.attachShadow({ mode: "open" });
|
|
22
|
+
|
|
23
|
+
/* Inject the library's CSS inside the shadow */
|
|
24
|
+
|
|
25
|
+
/* Option A: external stylesheet from CDN */
|
|
26
|
+
var link = document.createElement("link");
|
|
27
|
+
link.rel = "stylesheet";
|
|
28
|
+
link.href = "https://cdn.example.com/somelib/style.css";
|
|
29
|
+
shadow.appendChild(link);
|
|
30
|
+
|
|
31
|
+
/* Option B: inline <style> for custom overrides */
|
|
32
|
+
var style = document.createElement("style");
|
|
33
|
+
style.textContent = ".marker { color: red; }";
|
|
34
|
+
shadow.appendChild(style);
|
|
35
|
+
|
|
36
|
+
/* Create the container where the library mounts */
|
|
37
|
+
var innerDiv = document.createElement("div");
|
|
38
|
+
innerDiv.id = "lib-root";
|
|
39
|
+
innerDiv.style.width = "100%";
|
|
40
|
+
innerDiv.style.height = "500px";
|
|
41
|
+
shadow.appendChild(innerDiv);
|
|
42
|
+
|
|
43
|
+
/* Initialize the library against innerDiv */
|
|
44
|
+
// var map = L.map(innerDiv).setView([0, 0], 2);
|
|
45
|
+
// var editor = new TinyMCE({ target: innerDiv });
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
/* To query elements inside the shadow later: */
|
|
49
|
+
// var el = hostRef.current.shadowRoot.getElementById("lib-root");
|
|
50
|
+
|
|
51
|
+
return <div ref={hostRef} style={{ width: "100%" }} />;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Key Points
|
|
56
|
+
|
|
57
|
+
- Use `attachShadow({ mode: "open" })`. The `open` mode lets you access the shadow later via `host.shadowRoot`. Always use `open`.
|
|
58
|
+
- **Guard against double-initialization.** React re-renders (and StrictMode) can fire the effect twice. Check `if (host.shadowRoot) return;` before calling `attachShadow` -- it throws if called on an element that already has a shadow root.
|
|
59
|
+
- **Query inside the shadow** via `host.shadowRoot.getElementById(...)` or `.querySelector(...)`. `document.getElementById` will NOT find elements inside a shadow root.
|
|
60
|
+
- **CSS is fully isolated in both directions.** Softr's fonts, resets, and utility classes do not reach inside. If you need Softr's font inside the shadow, redeclare it in the injected `<style>`.
|
|
61
|
+
- **Events bubble out** of the shadow root normally (click, input, etc.), so React event handling on the host still works for most libraries.
|
|
62
|
+
|
|
63
|
+
## When NOT to Use Shadow DOM
|
|
64
|
+
|
|
65
|
+
If the library is React-native (shadcn, Recharts, lucide-react) and uses inline styles or scoped classes, skip the shadow -- it adds complexity for no benefit. Reserve this pattern for libraries that inject global CSS.
|
|
66
|
+
|
|
67
|
+
## Libraries Where This Pattern Saves Hours
|
|
68
|
+
|
|
69
|
+
Leaflet, Mapbox GL, TinyMCE, Quill, CKEditor, FullCalendar, Grapesjs, Froala, and any CDN-distributed widget from a non-React world.
|