Writing a renderer
The model draws nothing: no markup, no messages, no view state. A renderer supplies all of it, and Fable.Ripple.Form.Plain is one.
This page explains how to write your own, for a CSS framework or a component library. It is recommend to use Fable.Ripple.Form.Plain sources are the reference, one file per piece.
What the model gives you
A form is a tree of items and a handful of signals. There are two kinds of item:
FieldItem(id, render): one field.Dynamic render: everything else, fromandThen,showIf, a layout or a list.
Both carry a render that takes a RenderContext and returns an element, so a renderer's job starts with a function that calls them:
let renderItems (context: RenderContext) (items: Item list) : DomItem list =
items
|> List.map (fun item ->
match item with
| FieldItem(_, render) -> render context
| Dynamic render -> render context
)
The context is what the combinators say about a subtree: disabled, read-only, which errors are visible, the validation mode, a replacement label, an external error, a running async check.
Nested items get it back through context.RenderItems. You build one only at the root, with Base.renderContext.
Add the field kinds
Each kind is attributes, a render function and a constructor calling
Base.field, in a file of its own. Custom field walks through one; theFields/folder of the Plain package is the same three pieces, repeated.The render function receives a
FieldRenderConfig: theVarto bind, the attributes, tracked reads of the field's state, andOnBlur.Plain's
Viewmodule has the parts every HTML renderer needs:controlAttributesfor the id and the ARIA attributes.shownErrorfor the message to display.labelTextfor the label.
Offer the layouts
The model has no section and no group.
Base.wrapreplaces a form's items with one element you draw around them, and a layout is one line of markup over it:let section (title: string) (form: Form<'A>) : Form<'A> = form |> Base.wrap (fun context items -> Html.fieldset [ Html.legend title yield! context.RenderItems context items ] )Draw the list
FormList.formin the Plain package is the combinator: it keeps one sub-form per item, disposes it when the item leaves, tracks the indexes and folds the results.What it needs from you is a function
RenderContext -> FormList.RenderConfig -> DomItem, which receives:- the elements to place, each with its
Itemsand aDelete; - an
Addthat appends a new item; - whether the list is disabled.
FormList.renderis the default one; Lists shows another.- the elements to place, each with its
Write the view
A view renders
form.Itemsinside a<form>, with a status line and the action. Two calls do the work that is not markup:View.renderContextbuilds the root context from aViewConfig.View.onSubmitis the submit handler: it waits for pending async checks, then callsOnSubmiton success, or shows every error and focuses the first invalid control.
let asHtml (config: View.ViewConfig<'Output>) (form: Form<'Output>) : DomItem = let mutable formElement: HTMLElement option = None let context = View.renderContext renderItems config Html.form [ attr.ref (fun element -> formElement <- Some element) View.onSubmit config form (fun () -> formElement) yield! renderItems context form.Items // the status line and the action go here ]
Reusing the Plain package
A renderer for another framework can depend on Fable.Ripple.Form.Plain and reuse its View module, its attribute types and FormList.form, replacing only the markup. That is the cheapest path when the field API should stay as it is.