@narrative.io/jsonforms-provider-protocols
v2.12.0
Published
Dynamic data provider capabilities for JSONForms with Vue 3 integration
Readme
JSONForms Provider Protocols
A Vue 3 library that adds dynamic data provider capabilities to JSONForms, enabling form fields to fetch and display data from various sources like REST APIs, databases, and custom protocols.
✨ Features
- 🔌 Protocol-based Architecture: Extensible system for different data sources
- 🚀 Vue 3 Integration: Seamless integration with JSONForms Vue renderers
- 💾 Built-in Caching: TTL-based caching with configurable expiration
- 🔄 Dynamic Data Loading: Support for mount, focus, and query-based loading
- 🎯 Template Support: Dynamic URL and parameter templating
- 📦 TypeScript Support: Full TypeScript definitions included
- 🔐 Authentication: Flexible authentication mechanism support
📦 Installation
# Using bun (recommended)
bun add @narrative.io/jsonforms-provider-protocols
# Using npm
npm install @narrative.io/jsonforms-provider-protocols
# Peer dependencies
bun add @jsonforms/vue @jsonforms/core vue🚀 Quick Start
1. Register the Plugin
import { createApp } from 'vue'
import ProviderProtocols, { RestApiProtocol } from '@narrative.io/jsonforms-provider-protocols'
const app = createApp(App)
app.use(ProviderProtocols, {
protocols: [RestApiProtocol()]
})2. Configure Your Form
{
"type": "Control",
"scope": "#/properties/country",
"options": {
"provider": {
"ref": "countries",
"protocol": "rest_api",
"config": {
"url": "https://api.example.com/countries",
"items": "$.data[*]",
"map": {
"label": "$.name",
"value": "$.code"
}
}
}
}
}3. Use in Vue
<script setup lang="ts">
import { JsonForms } from '@jsonforms/vue'
import { providerRenderers } from '@narrative.io/jsonforms-provider-protocols/vue'
import { markRaw, ref } from 'vue'
const data = ref({ country: null })
const handleChange = (event) => data.value = event.data
</script>
<template>
<JsonForms
:data="data"
:schema="schema"
:uischema="uischema"
:renderers="markRaw(providerRenderers)"
@change="handleChange"
/>
</template>📖 Documentation
Getting Started
Core Concepts
- Protocols - How to fetch and transform data
- Authentication - Configure and use authentication
- Vue Components - Available components and composables
- API Reference - Complete API documentation
Examples
- Simple Dropdown
- Custom Renderers
- All Examples
- For end-to-end usage of data layer, projection, validation gating, and the CSP-safe validator, see the demo app in
demo/
Help & Troubleshooting
🔧 Key Concepts
Protocols
Define how data is fetched and transformed:
const CustomProtocol: Protocol = {
protocol: 'my-api',
async resolve(config, context) {
const response = await fetch(config.url)
const data = await response.json()
return {
items: data.map(item => ({
label: item.name,
value: item.id
})),
ttl: 300
}
}
}Data Transforms
Transform API response data before mapping to form items using a pipeline of transforms:
{
"provider": {
"protocol": "rest_api",
"config": {
"url": "https://api.example.com/data",
"items": "$.data[*]",
"transforms": [
{
"name": "flatten",
"key": "children",
"labelFormat": "{parent.name} → {name}"
},
{
"name": "filter",
"key": "active",
"values": [true]
}
],
"map": {
"label": "$.name",
"value": "$.id"
}
}
}
}Built-in Transforms
Flatten Transform Recursively flattens nested tree structures into a single-level array:
{
"name": "flatten",
"key": "children",
"labelFormat": "{parent.name} → {name}"
}key: The property containing nested children arrayslabelFormat(optional): Template for formatting labels using parent and child properties- Adds
_depth,_parent, and_formattedLabelmetadata to items
Filter Transform Filters items based on conditions. Supports a simple single-key syntax and a multi-condition syntax with operators.
Simple syntax (single key/values):
{
"name": "filter",
"key": "category",
"values": ["A", "B"]
}Multi-condition syntax (AND logic):
{
"name": "filter",
"conditions": [
{ "key": "status", "values": ["active"] },
{ "key": "connections", "operator": "empty" }
]
}Available operators:
| Operator | Description |
|----------|-------------|
| eq | Value matches one of values (default) |
| neq | Value does NOT match any of values |
| empty | Value is null, undefined, empty array, or empty string |
| notEmpty | Inverse of empty |
| gt | Value > values[0] |
| gte | Value >= values[0] |
| lt | Value < values[0] |
| lte | Value <= values[0] |
| contains | String includes substring, or array includes value |
Combining Transforms Transforms are applied sequentially in pipeline order:
{
"transforms": [
{ "name": "flatten", "key": "children" },
{ "name": "filter", "key": "type", "values": ["product"] }
]
}Template Variables
Create dynamic URLs using form data:
{
"url": "https://api.example.com/countries/{{data.country}}/states",
"query": { "search": "{{ui.query}}" }
}Load Strategies
Control when data is fetched:
mount- Load when component mounts (default)onFocus- Load when field receives focusquery- Load when user types (autocomplete)
Derive Functionality
Auto-populate fields from form data or the dataLayer:
{
"type": "Control",
"scope": "#/properties/derived_field",
"options": {
"derive": "country",
"mode": "follow",
"readonly": true
}
}DataLayer Support
Inject external data into forms using the createDataLayer API and reference it with the dataLayer() derive syntax:
<script setup>
import { createDataLayer } from '@narrative.io/jsonforms-provider-protocols'
const dataLayer = createDataLayer()
dataLayer.push({ dataset_name: "My Dataset" })
// Merge additional data at any time
dataLayer.push({ dataset_id: 42 })
</script>{
"type": "Control",
"scope": "#/properties/audience",
"options": {
"derive": "dataLayer(dataset_name)",
"mode": "follow",
"readonly": true
}
}The ConnectorDataLayer type defines available properties:
interface ConnectorDataLayer {
dataset_name?: string
dataset_description?: string
dataset_id?: number
profile_id?: string
profile_name?: string
}You can also read the dataLayer state directly in components:
<script setup>
import { useDataLayer } from '@narrative.io/jsonforms-provider-protocols'
const dataLayerState = useDataLayer()
// dataLayerState.value.dataset_name
</script>Error Handling
Control error display behavior with the showError property:
{
"provider": {
"protocol": "rest_api",
"config": {
"url": "https://api.example.com/data",
"showError": false,
"items": "$.data[*]",
"map": { "label": "$.name", "value": "$.id" }
}
}
}When showError is false, failed requests return empty results instead of throwing errors. Defaults to true.
Projection
Render simple controls (text, number) against deeply nested or array-wrapped data structures using the projection UISchema option. The control sees a simple value, while the underlying form data maintains the full structure.
The projection path is relative to the control's scope. So when scope is #/properties/data_rates and the projection is "0.video_rate_usd", it resolves to data_rates[0].video_rate_usd in the form data:
{
"type": "Control",
"scope": "#/properties/data_rates",
"options": {
"placeholder": "Enter video rate...",
"projection": "0.video_rate_usd"
}
}With form data { data_rates: [{ video_rate_usd: 2.5, display_rate_usd: 1.5 }] }, the control renders 2.5 as a simple number input. When the user changes the value, only video_rate_usd is updated — all sibling properties are preserved.
Path syntax: Dot-separated segments (similar to Lodash _.get) where numeric segments are array indices and string segments are object keys.
| Projection Path | Data Shape | Control Sees |
|-----------------|-----------|-------------|
| 0 | [123] | 123 |
| 0.video_rate_usd | [{ video_rate_usd: 2.5 }] | 2.5 |
| include | { include: ["a", "b"] } | ["a", "b"] |
The schema is also resolved through the projection path, so validation works correctly on the projected value.
Use useProjection directly in custom components:
<script setup>
import { useProjection } from '@narrative.io/jsonforms-provider-protocols'
const { projectedData, projectedSchema, handleProjectedChange, hasProjection } = useProjection(control, handleChange)
</script>Composables
Use providers directly in your components:
<script setup>
import { useProvider } from '@narrative.io/jsonforms-provider-protocols/vue'
const { items, loading, error, reload } = useProvider(binding, context)
</script>The library also exposes useProjection, useDirtyValidation, useDataLayer, and useDeriveInitialValue for custom renderers — see Vue Components and the API Reference.
Object Multi-Select
For arrays whose items are paired objects (e.g. [{ dataset_id, dataset_name }]), ProviderObjectMultiSelect registers automatically and translates between the form-data shape and PrimeVue's <MultiSelect> model:
{
"type": "Control",
"scope": "#/properties/datasets",
"options": {
"objectKeys": { "value": "dataset_id", "label": "dataset_name" },
"autoSelectSingle": true,
"provider": { "ref": "datasets", "protocol": "rest_api", "config": { "...": "..." } }
}
}objectKeys is inferred from the array's items.required when one of the two required properties has format: "uuid". Specify explicitly to override.
Schema-Aware Initialization
initFormDataFromSchema walks $ref/$defs, seeds const and default values (including through optional oneOf containers), and omits unset optionals:
import { initFormDataFromSchema, seedProjectionTargets } from '@narrative.io/jsonforms-provider-protocols'
const data = seedProjectionTargets(initFormDataFromSchema(schema), uischema)seedProjectionTargets is needed when an options.projection control targets an array index — without it, items.required validators see no item to apply against and the asterisk on the projected control would lie. It's idempotent and preserves any existing values at the target paths.
Validation Gating
Renderers (both built-in and custom, via useDirtyValidation) gate error display behind user interaction. Errors and p-invalid styling appear only after blur (text/number/select) or first change (checkbox/multiselect). The full AJV error array still flows through <JsonForms @change> unchanged, so consumers decide separately when to enable a submit button.
Derive Initial Value (Async)
When the initial value of a field comes from an API call rather than from local form data, use options.deriveInitialValue:
{
"type": "Control",
"scope": "#/properties/advertiser",
"options": {
"deriveInitialValue": {
"protocol": "rest_api",
"config": {
"url": "https://api.example.com/profiles/{{data.profile_id}}",
"items": "$",
"map": { "value": "$.mdm_id" }
}
}
}
}The fetched value is applied on mount once template variables resolve, re-fetched when dependencies change, and not re-applied when the user has already changed the field with the same template context. See API: useDeriveInitialValue.
CSP-Safe Validation
For environments that forbid new Function (Cloudflare Pages, strict CSP), import the AJV-shaped facade backed by @cfworker/json-schema from the ./no-eval-ajv subpath:
import { createNoEvalAjv } from '@narrative.io/jsonforms-provider-protocols/no-eval-ajv'
const ajv = createNoEvalAjv({ draft: '2020-12' })<JsonForms
:data="data"
:schema="schema"
:uischema="uischema"
:ajv="ajv"
:renderers="markRaw(providerRenderers)"
@change="handleChange"
/>Only compile() is functional; addSchema/getSchema/addFormat/addKeyword are no-ops kept for forward compatibility with plugins that probe the AJV interface. AJV error shapes (instancePath, keyword, params, message) are preserved so JsonForms can render errors identically.
Migrating from Earlier Betas
| Old API | New API |
|---------|---------|
| provide('externalData', ref) | createDataLayer().provide() |
| derive: "externalData(field)" | derive: "dataLayer(field)" |
| registerTransform(...) | Filter transform conditions array (see Filter Transform) |
🛠 Development
# Install dependencies
bun install
# Run tests
bun test
# Build library
bun run build
# Type checking
bunx vue-tsc --noEmit
# Watch mode for development
bun run devLocal Development with Consumer Apps
To test changes locally in a consumer application, use bun link to create a symlink:
In this library:
# Build and create a global link
bun run link:globalIn your consumer application:
# Link to the local version
bun link @narrative.io/jsonforms-provider-protocolsChanges to the library will be reflected in the consumer app after rebuilding:
# In this library - rebuild after making changes
bun run build
# Or use watch mode for automatic rebuilds
bun run devVerify the build succeeded:
# Check that all dist files exist
bun run check:builtThis runs scripts/ensureBuild.js to verify that all expected output files exist in the dist/ directory before linking.
To unlink and use the published package again:
# In your consumer application - remove the symlink and reinstall from npm
rm -rf node_modules/@narrative.io/jsonforms-provider-protocols
bun installNote:
bun unlink {packageName}is not yet implemented, so manual removal is required.
Creating a .tgz Package
To create a distributable package for testing:
bun run pack:distThis creates a .tgz file that can be installed in consumer apps via:
bun add ./path/to/narrative.io-jsonforms-provider-protocols-x.x.x.tgz🤝 Contributing
Contributions are welcome! Please read our Contributing Guide and check out the issue tracker.
📄 License
MIT - see LICENSE file for details.
