forgeframe 0.0.1
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 +868 -0
- package/dist/communication/bridge.d.ts +167 -0
- package/dist/communication/index.d.ts +12 -0
- package/dist/communication/messenger.d.ts +142 -0
- package/dist/communication/protocol.d.ts +75 -0
- package/dist/constants.d.ts +161 -0
- package/dist/core/component.d.ts +137 -0
- package/dist/core/consumer.d.ts +263 -0
- package/dist/core/host.d.ts +249 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/drivers/index.d.ts +18 -0
- package/dist/drivers/react.d.ts +224 -0
- package/dist/events/emitter.d.ts +150 -0
- package/dist/forgeframe.js +3179 -0
- package/dist/forgeframe.umd.cjs +5 -0
- package/dist/index.d.ts +230 -0
- package/dist/props/definitions.d.ts +63 -0
- package/dist/props/index.d.ts +13 -0
- package/dist/props/normalize.d.ts +59 -0
- package/dist/props/prop.d.ts +636 -0
- package/dist/props/schema.d.ts +196 -0
- package/dist/props/serialize.d.ts +60 -0
- package/dist/render/iframe.d.ts +213 -0
- package/dist/render/index.d.ts +38 -0
- package/dist/render/popup.d.ts +215 -0
- package/dist/render/templates.d.ts +202 -0
- package/dist/types.d.ts +1008 -0
- package/dist/utils/cleanup.d.ts +122 -0
- package/dist/utils/dimension.d.ts +50 -0
- package/dist/utils/index.d.ts +37 -0
- package/dist/utils/promise.d.ts +155 -0
- package/dist/utils/uid.d.ts +60 -0
- package/dist/window/helpers.d.ts +316 -0
- package/dist/window/index.d.ts +13 -0
- package/dist/window/name-payload.d.ts +188 -0
- package/dist/window/proxy.d.ts +168 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
# ForgeFrame
|
|
2
|
+
|
|
3
|
+
A TypeScript-first framework for embedding cross-domain iframes and popups with seamless communication. Pass data and callbacks across domains — perfect for payment forms, auth widgets, third-party integrations, and micro-frontends. Zero dependencies, ~21KB gzipped (ESM) / ~12KB gzipped (UMD).
|
|
4
|
+
|
|
5
|
+
### Terminology
|
|
6
|
+
|
|
7
|
+
ForgeFrame involves two sides:
|
|
8
|
+
|
|
9
|
+
**Consumer** — The outer app that renders the iframe and passes props into it
|
|
10
|
+
|
|
11
|
+
**Host** — The inner app running inside the iframe that receives props via `window.hostProps`
|
|
12
|
+
|
|
13
|
+
#### Real-world example
|
|
14
|
+
|
|
15
|
+
Imagine a payment company (like Stripe) wants to let merchants embed a checkout form:
|
|
16
|
+
|
|
17
|
+
| | Consumer | Host |
|
|
18
|
+
|--|----------|------|
|
|
19
|
+
| **Who builds it** | Merchant (e.g., `shop.com`) | Payment company (e.g., `stripe.com`) |
|
|
20
|
+
| **What they do** | Embeds the checkout, receives `onSuccess` | Provides the checkout UI, calls `onSuccess` when paid |
|
|
21
|
+
| **Their domain** | `shop.com` | `stripe.com` |
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
25
|
+
│ Consumer (merchant's site - shop.com) │
|
|
26
|
+
│ │
|
|
27
|
+
│ Checkout({ amount: 99, onSuccess: (payment) => { │
|
|
28
|
+
│ // Payment complete! Fulfill the order │
|
|
29
|
+
│ }}).render('#checkout-container'); │
|
|
30
|
+
│ │ │
|
|
31
|
+
│ ▼ │
|
|
32
|
+
│ ┌──────────────────────────────────────────────┐ │
|
|
33
|
+
│ │ Host (payment form - stripe.com) │ │
|
|
34
|
+
│ │ │ │
|
|
35
|
+
│ │ const { amount, onSuccess, close } │ │
|
|
36
|
+
│ │ = window.hostProps; │ │
|
|
37
|
+
│ │ │ │
|
|
38
|
+
│ │ // User enters card, pays... │ │
|
|
39
|
+
│ │ onSuccess({ paymentId: 'xyz', amount }); │ │
|
|
40
|
+
│ │ close(); │ │
|
|
41
|
+
│ └──────────────────────────────────────────────┘ │
|
|
42
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### Which side are you building?
|
|
46
|
+
|
|
47
|
+
| If you want to... | You're building the... |
|
|
48
|
+
|-------------------|------------------------|
|
|
49
|
+
| Embed someone else's component into your app | **Consumer** |
|
|
50
|
+
| Build a component/widget for others to embed | **Host** |
|
|
51
|
+
| Build both sides (e.g., your own micro-frontends) | **Both** |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Table of Contents
|
|
56
|
+
|
|
57
|
+
- [Installation](#installation)
|
|
58
|
+
- [Quick Start](#quick-start)
|
|
59
|
+
- [Step-by-Step Guide](#step-by-step-guide)
|
|
60
|
+
- [1. Define a Component](#1-define-a-component)
|
|
61
|
+
- [2. Create the Host Page](#2-create-the-host-page)
|
|
62
|
+
- [3. Render the Component](#3-render-the-component)
|
|
63
|
+
- [4. Handle Events](#4-handle-events)
|
|
64
|
+
- [Props System](#props-system)
|
|
65
|
+
- [Host Window API (hostProps)](#host-window-api-hostprops)
|
|
66
|
+
- [Templates](#templates)
|
|
67
|
+
- [React Integration](#react-integration)
|
|
68
|
+
- [Advanced Features](#advanced-features)
|
|
69
|
+
- [API Reference](#api-reference)
|
|
70
|
+
- [TypeScript](#typescript)
|
|
71
|
+
- [Browser Support](#browser-support)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install forgeframe
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
> **`Consumer`**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import ForgeFrame, { prop } from 'forgeframe';
|
|
89
|
+
|
|
90
|
+
const PaymentForm = ForgeFrame.create({
|
|
91
|
+
tag: 'payment-form',
|
|
92
|
+
url: 'https://checkout.stripe.com/payment',
|
|
93
|
+
dimensions: { width: 400, height: 300 },
|
|
94
|
+
props: {
|
|
95
|
+
amount: prop.number(),
|
|
96
|
+
onSuccess: prop.function<(txn: { transactionId: string }) => void>(),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const payment = PaymentForm({
|
|
101
|
+
amount: 99.99,
|
|
102
|
+
onSuccess: (txn) => console.log('Payment complete:', txn),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await payment.render('#payment-container');
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> **`Host`**
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { type HostProps } from 'forgeframe';
|
|
112
|
+
|
|
113
|
+
interface PaymentProps {
|
|
114
|
+
amount: number;
|
|
115
|
+
onSuccess: (txn: { transactionId: string }) => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
declare global {
|
|
119
|
+
interface Window {
|
|
120
|
+
hostProps: HostProps<PaymentProps>;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { amount, onSuccess, close } = window.hostProps;
|
|
125
|
+
|
|
126
|
+
document.getElementById('total')!.textContent = `$${amount}`;
|
|
127
|
+
document.getElementById('pay-btn')!.onclick = async () => {
|
|
128
|
+
await onSuccess({ transactionId: 'TXN_123' });
|
|
129
|
+
await close();
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
That's it! ForgeFrame handles all the cross-domain communication automatically.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Step-by-Step Guide
|
|
138
|
+
|
|
139
|
+
### 1. Define a Component
|
|
140
|
+
|
|
141
|
+
> **`Consumer`**
|
|
142
|
+
|
|
143
|
+
Components are defined using `ForgeFrame.create()`. This creates a reusable component factory.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import ForgeFrame, { prop } from 'forgeframe';
|
|
147
|
+
|
|
148
|
+
interface LoginProps {
|
|
149
|
+
email?: string;
|
|
150
|
+
onLogin: (user: { id: number; name: string }) => void;
|
|
151
|
+
onCancel?: () => void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const LoginForm = ForgeFrame.create<LoginProps>({
|
|
155
|
+
tag: 'login-form',
|
|
156
|
+
url: 'https://auth.stripe.com/login',
|
|
157
|
+
dimensions: { width: 400, height: 350 },
|
|
158
|
+
props: {
|
|
159
|
+
email: prop.string().optional(),
|
|
160
|
+
onLogin: prop.function<(user: { id: number; name: string }) => void>(),
|
|
161
|
+
onCancel: prop.function().optional(),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
<details>
|
|
167
|
+
<summary>Explanation</summary>
|
|
168
|
+
|
|
169
|
+
- **`tag`** (required): Unique identifier for the component
|
|
170
|
+
- **`url`** (required): URL of the host page to embed
|
|
171
|
+
- **`dimensions`**: Width and height of the iframe
|
|
172
|
+
- **`props`**: Schema definitions for props passed to the host
|
|
173
|
+
|
|
174
|
+
</details>
|
|
175
|
+
|
|
176
|
+
### 2. Create the Host Page
|
|
177
|
+
|
|
178
|
+
> **`Host`**
|
|
179
|
+
|
|
180
|
+
The host page runs inside the iframe at the URL you specified. It receives props via `window.hostProps`.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { type HostProps } from 'forgeframe';
|
|
184
|
+
|
|
185
|
+
interface LoginProps {
|
|
186
|
+
email?: string;
|
|
187
|
+
onLogin: (user: { id: number; name: string }) => void;
|
|
188
|
+
onCancel?: () => void;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
declare global {
|
|
192
|
+
interface Window {
|
|
193
|
+
hostProps: HostProps<LoginProps>;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { email, onLogin, onCancel, close } = window.hostProps;
|
|
198
|
+
|
|
199
|
+
if (email) document.getElementById('email')!.value = email;
|
|
200
|
+
|
|
201
|
+
document.getElementById('login-form')!.onsubmit = async (e) => {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
await onLogin({
|
|
204
|
+
id: 1,
|
|
205
|
+
name: 'John Doe',
|
|
206
|
+
email: document.getElementById('email')!.value,
|
|
207
|
+
});
|
|
208
|
+
await close();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
document.getElementById('cancel')!.onclick = async () => {
|
|
212
|
+
await onCancel?.();
|
|
213
|
+
await close();
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
<details>
|
|
218
|
+
<summary>Explanation</summary>
|
|
219
|
+
|
|
220
|
+
- **`HostProps<LoginProps>`**: Combines your props with built-in methods (`close`, `resize`, etc.)
|
|
221
|
+
- **`window.hostProps`**: Automatically available in ForgeFrame hosts, contains all props passed from the consumer
|
|
222
|
+
- **`close()`**: Built-in method to close the iframe/popup
|
|
223
|
+
|
|
224
|
+
</details>
|
|
225
|
+
|
|
226
|
+
### 3. Render the Component
|
|
227
|
+
|
|
228
|
+
> **`Consumer`**
|
|
229
|
+
|
|
230
|
+
Back in your consumer app, create an instance with props and render it.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
const login = LoginForm({
|
|
234
|
+
email: 'user@example.com',
|
|
235
|
+
onLogin: (user) => console.log('User logged in:', user),
|
|
236
|
+
onCancel: () => console.log('Login cancelled'),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await login.render('#login-container');
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 4. Handle Events
|
|
243
|
+
|
|
244
|
+
> **`Consumer`**
|
|
245
|
+
|
|
246
|
+
Subscribe to lifecycle events for better control.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const instance = LoginForm({ /* props */ });
|
|
250
|
+
|
|
251
|
+
instance.event.on('rendered', () => console.log('Login form is ready'));
|
|
252
|
+
instance.event.on('close', () => console.log('Login form closed'));
|
|
253
|
+
instance.event.on('error', (err) => console.error('Error:', err));
|
|
254
|
+
instance.event.on('resize', (dimensions) => console.log('New size:', dimensions));
|
|
255
|
+
|
|
256
|
+
await instance.render('#container');
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Available Events:**
|
|
260
|
+
|
|
261
|
+
| Event | Description |
|
|
262
|
+
|-------|-------------|
|
|
263
|
+
| `render` | Rendering started |
|
|
264
|
+
| `rendered` | Fully rendered and initialized |
|
|
265
|
+
| `prerender` | Prerender (loading) started |
|
|
266
|
+
| `prerendered` | Prerender complete |
|
|
267
|
+
| `display` | Component became visible |
|
|
268
|
+
| `close` | Component is closing |
|
|
269
|
+
| `destroy` | Component destroyed |
|
|
270
|
+
| `error` | An error occurred |
|
|
271
|
+
| `props` | Props were updated |
|
|
272
|
+
| `resize` | Component was resized |
|
|
273
|
+
| `focus` | Component received focus |
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Props System
|
|
278
|
+
|
|
279
|
+
ForgeFrame uses a fluent, Zod-like schema API for defining props. All schemas implement [Standard Schema](https://standardschema.dev/), enabling seamless integration with external validation libraries.
|
|
280
|
+
|
|
281
|
+
### Defining Props
|
|
282
|
+
|
|
283
|
+
Props define what data can be passed to your component.
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import ForgeFrame, { prop } from 'forgeframe';
|
|
287
|
+
|
|
288
|
+
const MyComponent = ForgeFrame.create({
|
|
289
|
+
tag: 'my-component',
|
|
290
|
+
url: 'https://widgets.stripe.com/component',
|
|
291
|
+
props: {
|
|
292
|
+
name: prop.string(),
|
|
293
|
+
count: prop.number(),
|
|
294
|
+
enabled: prop.boolean(),
|
|
295
|
+
config: prop.object(),
|
|
296
|
+
items: prop.array(),
|
|
297
|
+
onSubmit: prop.function<(data: FormData) => void>(),
|
|
298
|
+
nickname: prop.string().optional(),
|
|
299
|
+
theme: prop.string().default('light'),
|
|
300
|
+
email: prop.string().email(),
|
|
301
|
+
age: prop.number().min(0).max(120),
|
|
302
|
+
username: prop.string().min(3).max(20),
|
|
303
|
+
slug: prop.string().pattern(/^[a-z0-9-]+$/),
|
|
304
|
+
status: prop.enum(['pending', 'active', 'completed']),
|
|
305
|
+
tags: prop.array().of(prop.string()),
|
|
306
|
+
scores: prop.array().of(prop.number().min(0).max(100)),
|
|
307
|
+
user: prop.object().shape({
|
|
308
|
+
name: prop.string(),
|
|
309
|
+
email: prop.string().email(),
|
|
310
|
+
age: prop.number().optional(),
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
<details>
|
|
317
|
+
<summary>Explanation</summary>
|
|
318
|
+
|
|
319
|
+
| Prop | Description |
|
|
320
|
+
|------|-------------|
|
|
321
|
+
| `name`, `count`, `enabled`, `config`, `items` | Basic types: string, number, boolean, object, array |
|
|
322
|
+
| `onSubmit` | Functions are automatically serialized for cross-domain calls |
|
|
323
|
+
| `nickname` | `.optional()` makes the prop accept `undefined` |
|
|
324
|
+
| `theme` | `.default('light')` provides a fallback value |
|
|
325
|
+
| `email` | `.email()` validates email format |
|
|
326
|
+
| `age` | `.min(0).max(120)` constrains the range |
|
|
327
|
+
| `username` | `.min(3).max(20)` constrains string length |
|
|
328
|
+
| `slug` | `.pattern(/.../)` validates against a regex |
|
|
329
|
+
| `status` | `prop.enum([...])` restricts to specific values |
|
|
330
|
+
| `tags` | `.of(prop.string())` validates each array item |
|
|
331
|
+
| `scores` | Array items can have their own validation chain |
|
|
332
|
+
| `user` | `.shape({...})` defines nested object structure |
|
|
333
|
+
|
|
334
|
+
</details>
|
|
335
|
+
|
|
336
|
+
### Prop Schema Methods
|
|
337
|
+
|
|
338
|
+
All schemas support these base methods:
|
|
339
|
+
|
|
340
|
+
| Method | Description |
|
|
341
|
+
|--------|-------------|
|
|
342
|
+
| `.optional()` | Makes the prop optional (accepts `undefined`) |
|
|
343
|
+
| `.nullable()` | Accepts `null` values |
|
|
344
|
+
| `.default(value)` | Sets a default value (or factory function) |
|
|
345
|
+
|
|
346
|
+
### Schema Types
|
|
347
|
+
|
|
348
|
+
| Type | Factory | Methods |
|
|
349
|
+
|------|---------|---------|
|
|
350
|
+
| String | `prop.string()` | `.min()`, `.max()`, `.length()`, `.email()`, `.url()`, `.uuid()`, `.pattern()`, `.trim()`, `.nonempty()` |
|
|
351
|
+
| Number | `prop.number()` | `.min()`, `.max()`, `.int()`, `.positive()`, `.negative()`, `.nonnegative()` |
|
|
352
|
+
| Boolean | `prop.boolean()` | - |
|
|
353
|
+
| Function | `prop.function<T>()` | - |
|
|
354
|
+
| Array | `prop.array()` | `.of(schema)`, `.min()`, `.max()`, `.nonempty()` |
|
|
355
|
+
| Object | `prop.object()` | `.shape({...})`, `.strict()` |
|
|
356
|
+
| Enum | `prop.enum([...])` | - |
|
|
357
|
+
| Literal | `prop.literal(value)` | - |
|
|
358
|
+
| Any | `prop.any()` | - |
|
|
359
|
+
|
|
360
|
+
### Using Standard Schema Libraries
|
|
361
|
+
|
|
362
|
+
ForgeFrame accepts any [Standard Schema](https://standardschema.dev/) compliant library (Zod, Valibot, ArkType, etc.):
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import ForgeFrame from 'forgeframe';
|
|
366
|
+
import { z } from 'zod';
|
|
367
|
+
import * as v from 'valibot';
|
|
368
|
+
|
|
369
|
+
const MyComponent = ForgeFrame.create({
|
|
370
|
+
tag: 'my-component',
|
|
371
|
+
url: 'https://widgets.stripe.com/component',
|
|
372
|
+
props: {
|
|
373
|
+
email: z.string().email(),
|
|
374
|
+
user: z.object({ name: z.string(), role: z.enum(['admin', 'user']) }),
|
|
375
|
+
count: v.pipe(v.number(), v.minValue(0)),
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Updating Props
|
|
381
|
+
|
|
382
|
+
Props can be updated after rendering.
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
const instance = MyComponent({ name: 'Initial' });
|
|
386
|
+
await instance.render('#container');
|
|
387
|
+
await instance.updateProps({ name: 'Updated' });
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The host receives updates via `onProps`:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
window.hostProps.onProps((newProps) => {
|
|
394
|
+
console.log('Props updated:', newProps);
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Host Window API (hostProps)
|
|
401
|
+
|
|
402
|
+
In host windows, `window.hostProps` provides access to props and control methods.
|
|
403
|
+
|
|
404
|
+
### TypeScript Setup
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { type HostProps } from 'forgeframe';
|
|
408
|
+
|
|
409
|
+
interface MyProps {
|
|
410
|
+
email: string;
|
|
411
|
+
onLogin: (user: { id: number }) => void;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
declare global {
|
|
415
|
+
interface Window {
|
|
416
|
+
hostProps?: HostProps<MyProps>;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const { email, onLogin, close, resize } = window.hostProps!;
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Available Methods
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
const props = window.hostProps;
|
|
427
|
+
|
|
428
|
+
props.email;
|
|
429
|
+
props.onLogin(user);
|
|
430
|
+
props.uid;
|
|
431
|
+
props.tag;
|
|
432
|
+
|
|
433
|
+
await props.close();
|
|
434
|
+
await props.focus();
|
|
435
|
+
await props.resize({ width: 500, height: 400 });
|
|
436
|
+
await props.show();
|
|
437
|
+
await props.hide();
|
|
438
|
+
|
|
439
|
+
props.onProps((newProps) => { /* handle updates */ });
|
|
440
|
+
props.onError(new Error('Something failed'));
|
|
441
|
+
await props.export({ validate: () => true });
|
|
442
|
+
|
|
443
|
+
props.getConsumer();
|
|
444
|
+
props.getConsumerDomain();
|
|
445
|
+
props.consumer.props;
|
|
446
|
+
props.consumer.export(data);
|
|
447
|
+
|
|
448
|
+
const peers = await props.getPeerInstances();
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
<details>
|
|
452
|
+
<summary>Method Reference</summary>
|
|
453
|
+
|
|
454
|
+
| Method | Description |
|
|
455
|
+
|--------|-------------|
|
|
456
|
+
| `email`, `onLogin` | Your custom props and callbacks |
|
|
457
|
+
| `uid`, `tag` | Built-in identifiers |
|
|
458
|
+
| `close()` | Close the component |
|
|
459
|
+
| `focus()` | Focus (popup only) |
|
|
460
|
+
| `resize()` | Resize the component |
|
|
461
|
+
| `show()`, `hide()` | Toggle visibility |
|
|
462
|
+
| `onProps()` | Listen for prop updates |
|
|
463
|
+
| `onError()` | Report errors to consumer |
|
|
464
|
+
| `export()` | Export methods/data to consumer |
|
|
465
|
+
| `getConsumer()` | Get consumer window reference |
|
|
466
|
+
| `getConsumerDomain()` | Get consumer origin |
|
|
467
|
+
| `consumer.props` | Access consumer's props |
|
|
468
|
+
| `getPeerInstances()` | Get peer component instances from the same consumer |
|
|
469
|
+
|
|
470
|
+
</details>
|
|
471
|
+
|
|
472
|
+
### Exporting Data to Consumer
|
|
473
|
+
|
|
474
|
+
Host components can export methods/data for the consumer to use.
|
|
475
|
+
|
|
476
|
+
> **`Host`**
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
window.hostProps.export({
|
|
480
|
+
validate: () => document.getElementById('form').checkValidity(),
|
|
481
|
+
getFormData: () => ({ email: document.getElementById('email').value }),
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
> **`Consumer`**
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
const instance = MyComponent({ /* props */ });
|
|
489
|
+
await instance.render('#container');
|
|
490
|
+
|
|
491
|
+
const isValid = await instance.exports.validate();
|
|
492
|
+
const data = await instance.exports.getFormData();
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## Templates
|
|
498
|
+
|
|
499
|
+
### Container Template
|
|
500
|
+
|
|
501
|
+
Customize how the component container is rendered. Perfect for modals.
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
const ModalComponent = ForgeFrame.create({
|
|
505
|
+
tag: 'modal',
|
|
506
|
+
url: 'https://widgets.stripe.com/modal',
|
|
507
|
+
dimensions: { width: 500, height: 400 },
|
|
508
|
+
|
|
509
|
+
containerTemplate: ({ doc, frame, prerenderFrame, close }) => {
|
|
510
|
+
const overlay = doc.createElement('div');
|
|
511
|
+
Object.assign(overlay.style, {
|
|
512
|
+
position: 'fixed',
|
|
513
|
+
inset: '0',
|
|
514
|
+
background: 'rgba(0,0,0,0.5)',
|
|
515
|
+
display: 'flex',
|
|
516
|
+
alignItems: 'center',
|
|
517
|
+
justifyContent: 'center',
|
|
518
|
+
zIndex: '1000',
|
|
519
|
+
});
|
|
520
|
+
overlay.onclick = (e) => { if (e.target === overlay) close(); };
|
|
521
|
+
|
|
522
|
+
const modal = doc.createElement('div');
|
|
523
|
+
Object.assign(modal.style, { background: 'white', borderRadius: '8px', overflow: 'hidden' });
|
|
524
|
+
|
|
525
|
+
const closeBtn = doc.createElement('button');
|
|
526
|
+
closeBtn.textContent = '×';
|
|
527
|
+
closeBtn.onclick = () => close();
|
|
528
|
+
modal.appendChild(closeBtn);
|
|
529
|
+
|
|
530
|
+
const body = doc.createElement('div');
|
|
531
|
+
if (prerenderFrame) body.appendChild(prerenderFrame);
|
|
532
|
+
if (frame) body.appendChild(frame);
|
|
533
|
+
modal.appendChild(body);
|
|
534
|
+
|
|
535
|
+
overlay.appendChild(modal);
|
|
536
|
+
return overlay;
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Prerender Template
|
|
542
|
+
|
|
543
|
+
Customize the loading state shown while the host loads.
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
const MyComponent = ForgeFrame.create({
|
|
547
|
+
tag: 'my-component',
|
|
548
|
+
url: 'https://widgets.stripe.com/component',
|
|
549
|
+
|
|
550
|
+
prerenderTemplate: ({ doc, dimensions }) => {
|
|
551
|
+
const loader = doc.createElement('div');
|
|
552
|
+
loader.innerHTML = `
|
|
553
|
+
<div style="
|
|
554
|
+
display: flex;
|
|
555
|
+
align-items: center;
|
|
556
|
+
justify-content: center;
|
|
557
|
+
width: ${dimensions.width}px;
|
|
558
|
+
height: ${dimensions.height}px;
|
|
559
|
+
background: #f5f5f5;
|
|
560
|
+
">
|
|
561
|
+
<span>Loading...</span>
|
|
562
|
+
</div>
|
|
563
|
+
`;
|
|
564
|
+
return loader.firstElementChild as HTMLElement;
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## React Integration
|
|
572
|
+
|
|
573
|
+
### Basic Usage
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
576
|
+
import React from 'react';
|
|
577
|
+
import ForgeFrame, { prop, createReactComponent } from 'forgeframe';
|
|
578
|
+
|
|
579
|
+
const LoginComponent = ForgeFrame.create({
|
|
580
|
+
tag: 'login',
|
|
581
|
+
url: 'https://auth.stripe.com/login',
|
|
582
|
+
dimensions: { width: 400, height: 350 },
|
|
583
|
+
props: {
|
|
584
|
+
email: prop.string().optional(),
|
|
585
|
+
onLogin: prop.function<(user: { id: number; name: string }) => void>(),
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const Login = createReactComponent(LoginComponent, { React });
|
|
590
|
+
|
|
591
|
+
function App() {
|
|
592
|
+
const [user, setUser] = useState(null);
|
|
593
|
+
|
|
594
|
+
return (
|
|
595
|
+
<div>
|
|
596
|
+
<h1>My App</h1>
|
|
597
|
+
<Login
|
|
598
|
+
email="user@example.com"
|
|
599
|
+
onLogin={(loggedInUser) => setUser(loggedInUser)}
|
|
600
|
+
onRendered={() => console.log('Ready')}
|
|
601
|
+
onError={(err) => console.error(err)}
|
|
602
|
+
onClose={() => console.log('Closed')}
|
|
603
|
+
className="login-frame"
|
|
604
|
+
style={{ border: '1px solid #ccc' }}
|
|
605
|
+
/>
|
|
606
|
+
</div>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### React Props
|
|
612
|
+
|
|
613
|
+
The React component accepts all your component props plus:
|
|
614
|
+
|
|
615
|
+
| Prop | Type | Description |
|
|
616
|
+
|------|------|-------------|
|
|
617
|
+
| `onRendered` | `() => void` | Called when component is ready |
|
|
618
|
+
| `onError` | `(err: Error) => void` | Called on error |
|
|
619
|
+
| `onClose` | `() => void` | Called when closed |
|
|
620
|
+
| `context` | `'iframe' \| 'popup'` | Render mode |
|
|
621
|
+
| `className` | `string` | Container CSS class |
|
|
622
|
+
| `style` | `CSSProperties` | Container inline styles |
|
|
623
|
+
|
|
624
|
+
### Factory Pattern
|
|
625
|
+
|
|
626
|
+
For multiple components, use `withReactComponent`:
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
import { withReactComponent } from 'forgeframe';
|
|
630
|
+
|
|
631
|
+
const createComponent = withReactComponent(React);
|
|
632
|
+
|
|
633
|
+
const LoginReact = createComponent(LoginComponent);
|
|
634
|
+
const PaymentReact = createComponent(PaymentComponent);
|
|
635
|
+
const ProfileReact = createComponent(ProfileComponent);
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Advanced Features
|
|
641
|
+
|
|
642
|
+
### Popup Windows
|
|
643
|
+
|
|
644
|
+
Render as a popup instead of iframe.
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
await instance.render('#container', 'popup');
|
|
648
|
+
|
|
649
|
+
const PopupComponent = ForgeFrame.create({
|
|
650
|
+
tag: 'popup-component',
|
|
651
|
+
url: 'https://widgets.stripe.com/popup',
|
|
652
|
+
defaultContext: 'popup',
|
|
653
|
+
});
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Auto-Resize
|
|
657
|
+
|
|
658
|
+
Automatically resize based on host content.
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
const AutoResizeComponent = ForgeFrame.create({
|
|
662
|
+
tag: 'auto-resize',
|
|
663
|
+
url: 'https://widgets.stripe.com/component',
|
|
664
|
+
autoResize: { height: true, width: false, element: '.content' },
|
|
665
|
+
});
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Domain Security
|
|
669
|
+
|
|
670
|
+
Restrict which domains can embed or communicate.
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
const SecureComponent = ForgeFrame.create({
|
|
674
|
+
tag: 'secure',
|
|
675
|
+
url: 'https://secure.stripe.com/widget',
|
|
676
|
+
domain: 'https://secure.stripe.com',
|
|
677
|
+
allowedConsumerDomains: [
|
|
678
|
+
'https://myapp.com',
|
|
679
|
+
'https://*.myapp.com',
|
|
680
|
+
/^https:\/\/.*\.trusted\.com$/,
|
|
681
|
+
],
|
|
682
|
+
});
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Eligibility Checks
|
|
686
|
+
|
|
687
|
+
Conditionally allow rendering.
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
const FeatureComponent = ForgeFrame.create({
|
|
691
|
+
tag: 'feature',
|
|
692
|
+
url: 'https://widgets.stripe.com/feature',
|
|
693
|
+
eligible: ({ props }) => {
|
|
694
|
+
if (!props.userId) return { eligible: false, reason: 'User must be logged in' };
|
|
695
|
+
return { eligible: true };
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
if (instance.isEligible()) {
|
|
700
|
+
await instance.render('#container');
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Nested Components
|
|
705
|
+
|
|
706
|
+
Define nested components that can be rendered from within the host.
|
|
707
|
+
|
|
708
|
+
> **`Consumer`**
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
const ContainerComponent = ForgeFrame.create({
|
|
712
|
+
tag: 'container',
|
|
713
|
+
url: 'https://widgets.stripe.com/container',
|
|
714
|
+
children: () => ({
|
|
715
|
+
CardField: CardFieldComponent,
|
|
716
|
+
CVVField: CVVFieldComponent,
|
|
717
|
+
}),
|
|
718
|
+
});
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
> **`Host`**
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
const { children } = window.hostProps;
|
|
725
|
+
children.CardField({ onValid: () => {} }).render('#card-container');
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## API Reference
|
|
731
|
+
|
|
732
|
+
### ForgeFrame Object
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
import ForgeFrame, { prop } from 'forgeframe';
|
|
736
|
+
|
|
737
|
+
ForgeFrame.create(options) // Create a component
|
|
738
|
+
ForgeFrame.destroy(instance) // Destroy an instance
|
|
739
|
+
ForgeFrame.destroyByTag(tag) // Destroy all instances of a tag
|
|
740
|
+
ForgeFrame.destroyAll() // Destroy all instances
|
|
741
|
+
ForgeFrame.isHost() // Check if in host context
|
|
742
|
+
ForgeFrame.isEmbedded() // Alias for isHost() - more intuitive naming
|
|
743
|
+
ForgeFrame.getHostProps() // Get hostProps in host context
|
|
744
|
+
ForgeFrame.isStandardSchema(val) // Check if value is a Standard Schema
|
|
745
|
+
|
|
746
|
+
ForgeFrame.prop // Prop schema builders (also exported as `prop`)
|
|
747
|
+
ForgeFrame.CONTEXT // Context constants (IFRAME, POPUP)
|
|
748
|
+
ForgeFrame.EVENT // Event name constants
|
|
749
|
+
ForgeFrame.VERSION // Library version
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Component Options
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
interface ComponentOptions<P> {
|
|
756
|
+
tag: string;
|
|
757
|
+
url: string | ((props: P) => string);
|
|
758
|
+
dimensions?: { width?: number | string; height?: number | string };
|
|
759
|
+
autoResize?: { width?: boolean; height?: boolean; element?: string };
|
|
760
|
+
props?: PropsDefinition<P>;
|
|
761
|
+
defaultContext?: 'iframe' | 'popup';
|
|
762
|
+
containerTemplate?: (ctx: TemplateContext) => HTMLElement;
|
|
763
|
+
prerenderTemplate?: (ctx: TemplateContext) => HTMLElement;
|
|
764
|
+
domain?: string;
|
|
765
|
+
allowedConsumerDomains?: Array<string | RegExp>;
|
|
766
|
+
eligible?: (opts: { props: P }) => { eligible: boolean; reason?: string };
|
|
767
|
+
validate?: (opts: { props: P }) => void;
|
|
768
|
+
attributes?: IframeAttributes;
|
|
769
|
+
style?: CSSProperties;
|
|
770
|
+
timeout?: number;
|
|
771
|
+
children?: () => Record<string, ForgeFrameComponent>;
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Instance Methods
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
const instance = MyComponent(props);
|
|
779
|
+
|
|
780
|
+
await instance.render(container?, context?) // Render
|
|
781
|
+
await instance.renderTo(window, container?) // Render to another window
|
|
782
|
+
await instance.close() // Close and destroy
|
|
783
|
+
await instance.focus() // Focus
|
|
784
|
+
await instance.resize({ width, height }) // Resize
|
|
785
|
+
await instance.show() // Show
|
|
786
|
+
await instance.hide() // Hide
|
|
787
|
+
await instance.updateProps(newProps) // Update props
|
|
788
|
+
instance.clone() // Clone with same props
|
|
789
|
+
instance.isEligible() // Check eligibility
|
|
790
|
+
|
|
791
|
+
instance.uid // Unique ID
|
|
792
|
+
instance.event // Event emitter
|
|
793
|
+
instance.state // Mutable state
|
|
794
|
+
instance.exports // Host exports
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
## TypeScript
|
|
800
|
+
|
|
801
|
+
ForgeFrame is written in TypeScript and exports all types.
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
import ForgeFrame, {
|
|
805
|
+
prop,
|
|
806
|
+
PropSchema,
|
|
807
|
+
StringSchema,
|
|
808
|
+
NumberSchema,
|
|
809
|
+
BooleanSchema,
|
|
810
|
+
FunctionSchema,
|
|
811
|
+
ArraySchema,
|
|
812
|
+
ObjectSchema,
|
|
813
|
+
createReactComponent,
|
|
814
|
+
withReactComponent,
|
|
815
|
+
type ComponentOptions,
|
|
816
|
+
type ForgeFrameComponent,
|
|
817
|
+
type ForgeFrameComponentInstance,
|
|
818
|
+
type HostProps,
|
|
819
|
+
type StandardSchemaV1,
|
|
820
|
+
type TemplateContext,
|
|
821
|
+
type Dimensions,
|
|
822
|
+
type EventHandler,
|
|
823
|
+
type GetPeerInstancesOptions,
|
|
824
|
+
} from 'forgeframe';
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### Typing Host hostProps
|
|
828
|
+
|
|
829
|
+
```typescript
|
|
830
|
+
import { type HostProps } from 'forgeframe';
|
|
831
|
+
|
|
832
|
+
interface MyProps {
|
|
833
|
+
name: string;
|
|
834
|
+
onSubmit: (data: FormData) => void;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
declare global {
|
|
838
|
+
interface Window {
|
|
839
|
+
hostProps?: HostProps<MyProps>;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
window.hostProps!.name;
|
|
844
|
+
window.hostProps!.onSubmit;
|
|
845
|
+
window.hostProps!.close;
|
|
846
|
+
window.hostProps!.resize;
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## Browser Support
|
|
852
|
+
|
|
853
|
+
ForgeFrame requires modern browser features (ES2022+).
|
|
854
|
+
|
|
855
|
+
| Browser | Minimum Version |
|
|
856
|
+
|---------|-----------------|
|
|
857
|
+
| Chrome | 80+ |
|
|
858
|
+
| Firefox | 75+ |
|
|
859
|
+
| Safari | 14+ |
|
|
860
|
+
| Edge | 80+ |
|
|
861
|
+
|
|
862
|
+
**Note:** Internet Explorer is not supported. For IE support, use [Zoid](https://github.com/krakenjs/zoid).
|
|
863
|
+
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
## License
|
|
867
|
+
|
|
868
|
+
MIT
|