@wallaform/custom-field-sdk
v0.2.0
Published
SDK for building custom fields for Walla forms
Downloads
121
Maintainers
Readme
@wallaform/custom-field-sdk
Build custom form fields for Walla — a modern form builder platform.
Custom fields run inside sandboxed iframes and communicate with the host form via Cap'n Web RPC over MessagePort — a type-safe, bidirectional RPC channel with zero configuration.
Walla 폼에 임베드되는 커스텀 필드를 만들기 위한 SDK입니다. Sandboxed iframe 내에서 실행되며, Cap'n Web RPC over MessagePort를 통해 호스트 폼과 양방향 타입 안전 통신을 합니다.
Installation / 설치
npm install @wallaform/custom-field-sdkThis is an ESM-only package. Use
<script type="module">or a bundler (webpack, vite, etc.).ESM 전용 패키지입니다.
<script type="module">또는 번들러(webpack, vite 등)에서 사용하세요.
Quick Start / 빠른 시작
import { WallaField } from '@wallaform/custom-field-sdk';
const field = new WallaField({ debug: true });
// 1. Receive initialization data from the host form
// 호스트 폼에서 초기화 데이터를 수신합니다
field.onInit(({ properties, value, theme, locale, fieldInfo, formContext, mode }) => {
// mode: 'live' (real form filling) or 'preview' (dashboard editor preview)
// mode: 'live' (실제 폼 응답) 또는 'preview' (대시보드 에디터 프리뷰)
// Apply theme colors to your UI / 테마 색상을 UI에 적용
document.body.style.fontFamily = theme.fontFamily;
document.body.style.background = theme.background;
// Restore previously saved value / 이전에 저장된 값 복원
if (value) applyValue(value);
});
// 2. Handle validation requests (sync or async)
// 유효성 검사 요청 처리 (동기/비동기 모두 지원)
field.onValidate((submitType) => {
const myValue = getMyValue();
if (!myValue) {
return { valid: false, errors: [{ message: 'This field is required' }] };
}
return { valid: true };
});
// 3. Report value changes — the host auto-saves every change
// 값 변경을 보고합니다 — 호스트가 매 변경마다 자동 저장합니다
myInput.addEventListener('input', () => {
field.setValue(myInput.value);
});
// 4. Auto-resize the iframe to fit your content
// 컨텐츠에 맞게 iframe 높이를 자동 조절합니다
field.setHeight(document.body.scrollHeight);How It Works / 작동 원리
Host (Walla Form) Custom Field (iframe)
│ │
│── transfers MessagePort ──────────────►│ One-time postMessage
│ │ MessagePort 전달 (1회)
│ Cap'n Web RPC channel │
│◄══════════════════════════════════════►│ Bidirectional RPC
│ │ 양방향 RPC 통신
│── fieldApi.init(payload) ─────────────►│
│◄── hostApi.setValue(value) ────────────│
│── fieldApi.validate('submit') ────────►│ Returns Promise
│◄── { valid: true } ───────────────────│ 네이티브 awaitThe SDK handles all connection setup automatically. You never interact with MessagePort or RPC sessions directly.
SDK가 모든 연결 설정을 자동으로 처리합니다. MessagePort나 RPC 세션을 직접 다룰 필요가 없습니다.
API Reference
Constructor / 생성자
const field = new WallaField(options?: WallaFieldOptions);| Option | Type | Default | Description |
|--------|------|---------|-------------|
| debug | boolean | false | Log all RPC messages to console / RPC 메시지를 콘솔에 출력 |
The SDK automatically:
- Extracts
fieldIdfrom the URL query parameter (?fieldId=xxx) - Waits for the host to transfer a MessagePort
- Establishes a Cap'n Web RPC session
SDK가 자동으로:
- URL 쿼리 파라미터에서
fieldId를 추출합니다 (?fieldId=xxx) - 호스트가 MessagePort를 전달할 때까지 대기합니다
- Cap'n Web RPC 세션을 수립합니다
Callbacks (Host → Your Field) / 콜백 (호스트 → 필드)
Register callbacks to respond to host events. All callbacks are optional and single-registration (last-write-wins).
호스트 이벤트에 응답하는 콜백을 등록합니다. 모든 콜백은 선택사항이며, 마지막 등록이 우선합니다.
// Initialization — receive config, theme, value, and form context
// 초기화 — 설정, 테마, 값, 폼 컨텍스트 수신
field.onInit((payload: FieldInitPayload) => { ... });
// Validation — return { valid, errors? }. Async supported.
// 유효성 검사 — { valid, errors? } 반환. 비동기 지원.
field.onValidate((submitType: 'next' | 'submit') => { ... });
// External value injection (pre-fill, restore after page navigation)
// 외부 값 주입 (사전 입력, 페이지 이동 후 복원)
field.onValueSet((value: unknown) => { ... });
// Field is being removed from the form — clean up resources
// 폼에서 필드가 제거됩니다 — 리소스를 정리하세요
field.onDestroy(() => { ... });
// Host requests focus on this field (e.g., page navigation)
// 호스트가 이 필드에 포커스를 요청합니다 (예: 페이지 이동)
field.onFocus(() => { ... });
// Other fields' values changed (filtered by observeFields policy)
// 다른 필드의 값이 변경되었습니다 (observeFields 정책에 따라 필터링)
field.onFormValuesChanged((values: Record<string, unknown>) => { ... });Actions (Your Field → Host) / 액션 (필드 → 호스트)
Call these methods to communicate with the host form.
호스트 폼과 통신하려면 이 메서드들을 호출하세요.
// Report a value change — host auto-saves on every call
// 값 변경 보고 — 호스트가 매 호출마다 자동 저장
field.setValue(value: unknown);
// Request iframe height change / iframe 높이 변경 요청
field.setHeight(height: number);
// Upload binary data through the host (e.g., signatures, audio recordings)
// 호스트를 통해 바이너리 데이터를 업로드합니다 (예: 서명, 오디오 녹음)
const { url } = await field.uploadBlob(arrayBuffer, 'image/png', 'signature.png');
// Report an error to the host for display
// 호스트에 에러를 보고하여 표시
field.reportError({ message: string, recoverable: boolean });State Getters / 상태 조회
field.getProperties<T>() // Current field properties / 현재 필드 속성
field.getTheme() // Current theme / 현재 테마
field.getLocale() // Current locale ('ko', 'en', etc.) / 현재 로케일
field.getFieldInfo() // { fieldId, formId, label }
field.getFormContext() // { fields: FieldSummary[], values: Record<string, unknown> }
field.isConnected() // Whether RPC channel is established / RPC 채널 수립 여부
field.getVersion() // SDK version / SDK 버전Validation / 유효성 검사
Both synchronous and asynchronous validators are supported:
동기 및 비동기 검증 모두 지원합니다:
// Synchronous / 동기
field.onValidate((submitType) => {
return { valid: true };
});
// Asynchronous (e.g., server-side check) / 비동기 (예: 서버 검증)
field.onValidate(async (submitType) => {
const res = await fetch('/api/validate', { method: 'POST', body: ... });
const { ok } = await res.json();
return { valid: ok, errors: ok ? undefined : [{ message: 'Server validation failed' }] };
});submitType:'next'(page navigation) or'submit'(final submission)- If no callback is registered,
{ valid: true }is returned automatically - If the callback throws, the error is caught and treated as
{ valid: false }
Observing Other Fields / 다른 필드 관찰
Custom fields can react to other fields' values. Access is controlled by the observeFields policy set on the custom field version:
커스텀 필드는 다른 필드의 값에 반응할 수 있습니다. 접근은 커스텀 필드 버전에 설정된 observeFields 정책으로 제어됩니다:
| Mode | Behavior | Use Case |
|------|----------|----------|
| none | No form context provided | Standalone fields (color picker, signature) |
| all | All fields and values (sensitive excluded) | Summary, calculation fields |
| configured | Admin selects specific fields | Fields that reference specific inputs |
field.onInit(({ formContext }) => {
// formContext.fields — other fields' metadata (type, label, etc.)
// formContext.values — other fields' current values
console.log('Available fields:', formContext.fields.length);
});
field.onFormValuesChanged((values) => {
// Called whenever an observed field's value changes
// 관찰 중인 필드의 값이 변경될 때마다 호출됩니다
const total = Object.values(values).reduce((sum, v) => sum + Number(v), 0);
field.setValue(total);
});Sensitive field types (SECRETS, PHONE_NUMBER, EMAIL) are always excluded, even in
allmode.민감 필드 타입 (비밀번호, 전화번호, 이메일)은
all모드에서도 항상 제외됩니다.
Theming / 테마
The host form's theme is provided as a WallaFieldTheme object. Use it to match your field's appearance with the form's design.
호스트 폼의 테마가 WallaFieldTheme 객체로 전달됩니다. 필드의 외관을 폼 디자인에 맞추는 데 사용하세요.
field.onInit(({ theme }) => {
const root = document.documentElement;
root.style.setProperty('--bg', theme.background);
root.style.setProperty('--fg', theme.foreground);
root.style.setProperty('--primary', theme.primary);
root.style.setProperty('--font', theme.fontFamily);
root.style.setProperty('--radius', theme.borderRadius);
});interface WallaFieldTheme {
background: string; // Form background / 폼 배경색
foreground: string; // Primary text color / 기본 텍스트 색상
primary: string; // Accent/brand color / 강조/브랜드 색상
primaryForeground: string; // Text on primary color / 강조색 위 텍스트
field: string; // Input field background / 입력 필드 배경
fieldForeground: string; // Input placeholder color / 입력 플레이스홀더 색상
border: string; // Border color / 테두리 색상
accent: string; // Secondary accent / 보조 강조색
fontFamily: string; // CSS font-family / CSS 폰트 패밀리
borderRadius: string; // CSS border-radius / CSS 테두리 둥글기
direction: 'ltr' | 'rtl'; // Text direction / 텍스트 방향
}Security / 보안
Custom fields run in a sandboxed iframe with sandbox="allow-scripts" (no allow-same-origin). This means your field:
커스텀 필드는 sandbox="allow-scripts" (allow-same-origin 없음) sandboxed iframe 내에서 실행됩니다:
- Cannot access the host page's DOM, cookies, or localStorage
- Cannot navigate the host page
- Can run JavaScript and make network requests (fetch)
- Can communicate with the host only through the RPC channel
Communication uses a MessagePort — a dedicated 1:1 channel that only the host and your field can access. No origin validation, nonce, or namespace filtering is needed.
통신은 MessagePort — 호스트와 필드만 접근할 수 있는 전용 1:1 채널을 사용합니다.
Host-side limits / 호스트 측 제한
| Limit | Value | Description |
|-------|-------|-------------|
| setValue size | 64 KB | Maximum JSON-serialized value size / 최대 값 크기 |
| setValue debounce | 50 ms | Rapid calls are debounced / 빠른 호출은 디바운스 |
| setHeight range | 0–5000 px | Height is clamped / 높이 범위 제한 |
| setHeight throttle | 100 ms | Rapid calls are throttled / 빠른 호출은 스로틀 |
| reportError message | 500 chars | Message is truncated / 메시지 잘림 |
| validate timeout | 10 sec | Host times out if no response / 응답 없으면 타임아웃 |
Response sheet display limits / 응답 시트 표시 제한
The response sheet and CSV/Excel export auto-generate columns from your outputSchema. These limits apply to all tabular display contexts. Raw data in Open API and webhooks is always complete.
응답 시트와 CSV/Excel 내보내기는 outputSchema에서 자동으로 컬럼을 생성합니다. 이 제한은 표 형태 표시에 적용됩니다. Open API와 웹훅의 원본 데이터에는 제한이 없습니다.
| Limit | Value | Description | |-------|-------|-------------| | Max nesting depth | 3 | Properties deeper than 3 levels are not shown / depth 3 초과 속성은 미표시 | | Leaf column count | No limit | Consistent with other multi-column field types / 다른 다중 컬럼 필드와 동일하게 제한 없음 | | Array of objects | skipped | Only arrays of primitives (string[], number[]) are shown / primitive 배열만 표시 |
Tip: Design your outputSchema with flat or shallow structures for best sheet display. Use nested objects sparingly.
팁: 시트에서 잘 보이려면 outputSchema를 가능한 flat하게 설계하세요. 중첩은 최소화하는 것이 좋습니다.
Lifecycle / 라이프사이클
1. iframe loads → SDK waits for MessagePort from host
iframe 로드 → SDK가 호스트의 MessagePort를 대기
2. Host transfers MessagePort → RPC session established
호스트가 MessagePort 전달 → RPC 세션 수립
3. Host calls init() → your onInit callback fires
호스트가 init() 호출 → onInit 콜백 실행
4. User interacts → you call setValue() → host auto-saves
사용자 인터랙션 → setValue() 호출 → 호스트 자동 저장
5. Form submit → host calls validate() → your onValidate returns result
폼 제출 → 호스트가 validate() 호출 → onValidate가 결과 반환
6. Field removed → host calls destroy() → your onDestroy fires → port closed
필드 제거 → 호스트가 destroy() 호출 → onDestroy 실행 → 포트 닫힘Conditional rendering: When a field is hidden by branch logic, the iframe is fully unmounted (not CSS-hidden). When shown again, a new iframe loads and init() is called with the previously saved value. Internal UI state (cursor, scroll) is lost — only the semantic value survives.
조건부 렌더링: 분기 로직으로 필드가 숨겨지면 iframe이 완전히 unmount됩니다 (CSS 숨김이 아님). 다시 표시되면 새 iframe이 로드되고 이전에 저장된 값으로 init()이 호출됩니다.
Build / 빌드
pnpm build # ESM + type declarations + copy SDK to examples
pnpm test # 47 tests (Vitest + jsdom)| Output | Format | Size |
|--------|--------|------|
| dist/index.js | ESM (capnweb inlined) | ~35 KB |
| dist/index.d.ts | TypeScript declarations | ~6.5 KB |
Exported Types / 내보내는 타입
export {
WallaField,
type WallaFieldTheme,
type WallaFieldInfo,
type WallaFieldOptions,
type WallaFieldValidationError,
type WallaFieldValidationResult,
type ValidateCallback,
type FieldInitPayload,
type FieldRpcApi,
type HostRpcApi,
type FieldSummary,
type WallaPortInitMessage,
type ObserveFieldsMode,
} from '@wallaform/custom-field-sdk';Examples / 예제
See examples/ for working custom field implementations:
- Color Picker — Hex color input with preset swatches and theme integration
- Stat Slider — Statistical slider with Q1/Q3/Median markers
- Signature Pad — Canvas-based handwritten signature with undo, pen sizes, and HiDPI support
To run examples locally / 예제를 로컬에서 실행하려면:
pnpm build # Build SDK first
npx serve -l 4567 examples/color-picker # http://localhost:4567/index.html
npx serve -l 3456 examples/stat-slider # http://localhost:3456/index.html
npx serve -l 5678 examples/signature-pad # http://localhost:5678/index.htmlThen set the renderUrl in the database to point to the example URL.
Known Limitations & Roadmap / 알려진 제한사항 및 로드맵
- No local test harness: Currently, you need the Walla host form to test. A standalone mock host for local development is planned.
- No
localStorageaccess: Sandboxed iframes withoutallow-same-origincannot use localStorage/sessionStorage/IndexedDB. All persistent state must go throughsetValue. - Canvas/WebGL: Works inside the sandbox, but note that iframe resize doesn't fire a native event — handle
onInitand window resize yourself.
- 로컬 테스트 하네스 없음: 현재 Walla 호스트 폼이 있어야 테스트 가능합니다. 로컬 개발용 목 호스트를 계획 중입니다.
localStorage접근 불가:allow-same-origin없는 sandboxed iframe에서는 localStorage/sessionStorage/IndexedDB를 사용할 수 없습니다. 모든 영속 상태는setValue를 통해야 합니다.- Canvas/WebGL: sandbox 내에서 동작하지만, iframe 리사이즈는 네이티브 이벤트를 발생시키지 않으므로 직접 처리해야 합니다.
