Skip to content

myst-contrib/myst-jsonform

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

myst-jsonform

A MyST plugin providing a {jsonform} directive that renders an interactive form from a JSON Schema, using JSONForms.

Install

Add the plugin to your myst.yml:

project:
  plugins:
    - https://cdn.jsdelivr.net/gh/myst-contrib/myst-jsonform@main/dist/jsonform.mjs

The directive body: two modes

The body can be written in one of two modes (and in each case, using either YAML or JSON syntax).

Mode 1 — a single Schema

The whole body is the JSON Schema. JSONForms auto-generates the layout.

```{jsonform}
type: object
properties:
  name:
    type: string
    title: Your name
  age:
    type: integer
    title: Age
    minimum: 0
```

Mode 2 — a folded form with named chunks

If the body has a top-level Schema: key, it is read as a set of named chunks. This unlocks a custom layout, initial data, and styling. Two families of keys:

JSONForms-native — these mirror the three tabs on the jsonforms.io examples:

key supported by required meaning
Schema jsonforms.io yes the JSON Schema
UISchema jsonforms.io no, auto-generated if omitted the UI Schema (layout);
Data jsonforms.io no initial form data
Style this plugin no per-form CSS — see Styling
Submit this plugin no, defaults to print what happens on submit — see Submit actions

Key names are matched case-insensitively and ignoring spaces/underscores, so UISchema, UI Schema and ui_schema are equivalent.

```{jsonform}
Schema:
  type: object
  properties:
    firstName: {type: string, title: First name}
    lastName:  {type: string, title: Last name}
UISchema:
  type: HorizontalLayout
  elements:
    - {type: Control, scope: "#/properties/firstName"}
    - {type: Control, scope: "#/properties/lastName"}
Data:
  firstName: Ada
```

What's supported

From a bare schema, JSONForms renders:

  • strings (with format for email, uri, date, date-time, time)
  • integers / numbers (with minimum / maximum)
  • booleans (checkboxes)
  • enum (dropdowns), and arrays of enum (multi-select checkboxes)
  • arrays of objects (add/remove tables)
  • required fields and live validation (the Submit button stays disabled until the form is valid)

Nested objects

A bare schema does not expand nested object properties — JSONForms' auto-generated layout only renders the top-level fields. To render nested objects, use Mode 2 and provide a UISchema whose controls point at the nested scopes (typically inside a Group):

```{jsonform}
Schema:
  type: object
  properties:
    name: {type: string, title: Name}
    address:
      type: object
      properties:
        street: {type: string, title: Street}
        city:   {type: string, title: City}
UISchema:
  type: VerticalLayout
  elements:
    - {type: Control, scope: "#/properties/name"}
    - type: Group
      label: Address
      elements:
        - {type: Control, scope: "#/properties/address/properties/street"}
        - {type: Control, scope: "#/properties/address/properties/city"}
```

Styling

The form renders inside an open shadow root, so it is style-isolated from the page. There are three ways to customise it:

  1. Theme variables (site-wide). The base stylesheet exposes CSS custom properties — --jsonform-accent, --jsonform-accent-hover, --jsonform-border, --jsonform-radius, --jsonform-muted, --jsonform-error, --jsonform-gap, --jsonform-max-width, … Because inherited properties cross the shadow boundary, you can override them with ordinary site CSS:

    :root { --jsonform-accent: #2e7d32; --jsonform-radius: 10px; }
  2. Inline CSS (per form). A Style: chunk with verbatim rules, injected into that form only:

    ```{jsonform}
    Schema:
      type: object
      properties: {name: {type: string, title: Name}}
    Style: |
      .control > label { color: rebeccapurple; }
      .control input { border: 2px solid rebeccapurple; }
    ```
  3. External stylesheet (per form). A Style: value that is a path or URL ending in .css is treated as a reference; mystmd stages the file and links it inside the form's shadow root:

    Style: _static/style_forms.css

Submit actions

The Submit chunk says what happens when the (validated) form is submitted. It is either a single action or a list of actions, each with a type. On submit they run in order; the form reports success only if every action succeeds, otherwise it shows the first failure.

If Submit is omitted, the default is a single print action.

type: print

Shows the collected data as JSON below the form (the default behaviour). Always succeeds.

Submit:
  type: print

type: webapi

Sends the data to an HTTP endpoint with fetch.

key default meaning
url (required) endpoint to send to
method POST HTTP method
json false content-type: falsetext/plain (no CORS preflight), trueapplication/json
headers (none) extra request headers, merged over the default Content-Type

For example:

Submit:
  type: webapi
  url: https://my-data-collector.example.org/survey
  • Success = the response status is 2xx. Redirects (3xx) are followed.
  • CORS: by default the body is sent as text/plain (still a JSON string), which is a "simple" request and avoids the browser's CORS preflight — so the endpoint only needs to return Access-Control-Allow-Origin. Set json: true to use the official application/json content-type, which triggers a preflight (the server must then also handle OPTIONS). All CORS configuration is on the endpoint's server, never the page's.

Formspree
You can easily setup a free endpoint with Formspree or similar services.

Submit:
  type: webapi
  url: https://formspree.io/f/xxxxxxxx
  json: true
  headers:
    Accept: application/json

Formspree needs Accept: application/json, otherwise they 302-redirect to an HTML "thanks" page with no CORS headers, which fails.

type: event

Emits DOM CustomEvents so other JavaScript on the page can react to the form.

key default meaning
submit_event (none) event name dispatched when the form is submitted
change_event (none) event name dispatched on every change (keystroke, etc.)
name (none) identity, echoed into event.detail.name
Submit:
  type: event
  name: survey12
  submit_event: done
  change_event: ongoing

The event is dispatched with bubbles: true, composed: true, so it crosses the shadow-DOM boundary and reaches document/window — page code listens globally, with no handle to the element:

window.addEventListener('done', (e) => {
  console.log(e.detail.name, e.detail.data, e.detail.valid);
});
window.addEventListener('ongoing', (e) => {
  console.log('changed:', e.detail.data);
});

event.detail carries { name, phase, data, valid, errors }, where phase is 'change' or 'submit' and valid is errors.length === 0. event.target is the form's root element, so a scoped listener with a handle works too.

Event names are global strings: if you create two forms, you can use the same event names and callback handlers, and you can disambiguate via detail.name.
Note that due to JSONForms internals, the change_event if defined also fires once on load.

A list of actions

Note that a single form can choose to trigger multiple actions on submit, e.g.

Submit:
  - type: print
  - type: webapi
    url: https://data-collector.example/survey/12/response
  - type: event
    submit_event: done

More examples

A page exercising the directive (types, layouts, styling) is published at:

and its source is:

How it works

The directive emits an anywidget node. The browser-side widget (dist/widget.mjs) is a self-contained bundle of React + JSONForms, so there is exactly one copy of React and of @jsonforms/core — no CDN resolution at view time. The plugin itself (dist/jsonform.mjs) is bundled too (with js-yaml inlined) so it can be loaded directly from a URL.

Development

Requires bun.

bun install
bun run build      # builds dist/widget.mjs and dist/jsonform.mjs
bun run watch      # rebuilds the widget on change (for local iteration)

For local iteration, point your myst.yml at the source plugin instead of the URL; it automatically uses the local dist/widget.mjs when present:

project:
  plugins:
    - /absolute/path/to/myst-jsonform/src/jsonform.mjs

Releasing

The dist/ bundles are committed to the repository (that is what users load via jsDelivr). After changing anything under src/:

bun run build
git add dist
git commit -m "rebuild"
git push

jsDelivr serves @main with a cache of up to ~12 h. For reproducible, instantly pinned releases, tag a version and have users reference it instead of @main (and bump the WIDGET_URL tag in src/jsonform.mjs to match):

git tag v0.1.0 && git push --tags
    - https://cdn.jsdelivr.net/gh/flotpython/myst-jsonform@v0.1.0/dist/jsonform.mjs

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors