<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gustavo A. Roberto</title>
    <description>The latest articles on DEV Community by Gustavo A. Roberto (@gustavoaroberto).</description>
    <link>https://dev.to/gustavoaroberto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1064660%2F8f5e8e5a-586f-4c04-abcb-975306d5988a.png</url>
      <title>DEV Community: Gustavo A. Roberto</title>
      <link>https://dev.to/gustavoaroberto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gustavoaroberto"/>
    <language>en</language>
    <item>
      <title>From Server-Rendered Forms to a Framework-Agnostic JavaScript Library</title>
      <dc:creator>Gustavo A. Roberto</dc:creator>
      <pubDate>Thu, 22 Oct 2026 22:19:52 +0000</pubDate>
      <link>https://dev.to/gustavoaroberto/from-server-rendered-forms-to-a-framework-agnostic-javascript-library-1g4j</link>
      <guid>https://dev.to/gustavoaroberto/from-server-rendered-forms-to-a-framework-agnostic-javascript-library-1g4j</guid>
      <description>&lt;p&gt;A few years ago, while working on a Java and Thymeleaf application, I faced an interesting frontend challenge.&lt;/p&gt;

&lt;p&gt;One of its workflows involved collecting notary-related information through a form that spanned multiple pages, with different validation requirements along the way.&lt;/p&gt;

&lt;p&gt;The challenge wasn't creating inputs or rendering HTML. It was coordinating the entire experience while preserving user progress between separate documents.&lt;/p&gt;

&lt;p&gt;That problem eventually inspired &lt;strong&gt;Demetria&lt;/strong&gt;, a JavaScript library I recently revisited and rebuilt as an open-source project.&lt;/p&gt;

&lt;p&gt;In this article, I want to share the problem that inspired it, the architecture behind the current implementation, and some of the trade-offs involved in building a framework-agnostic library.&lt;/p&gt;

&lt;h2&gt;
  
  
  01. The problem with multi-page forms
&lt;/h2&gt;

&lt;p&gt;When working with modern frontend frameworks, it's easy to take client-side state management for granted.&lt;/p&gt;

&lt;p&gt;But in a traditional server-rendered application, navigating between pages can mean loading an entirely new HTML document.&lt;/p&gt;

&lt;p&gt;Any state stored exclusively in JavaScript memory disappears.&lt;/p&gt;

&lt;p&gt;For a complex form, this creates several challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preserving previously entered values.&lt;/li&gt;
&lt;li&gt;Tracking progress through multiple steps.&lt;/li&gt;
&lt;li&gt;Enforcing validation before moving forward.&lt;/li&gt;
&lt;li&gt;Keeping form behavior consistent between pages.&lt;/li&gt;
&lt;li&gt;Avoiding duplication of navigation and state-management logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the original application, I used &lt;code&gt;localStorage&lt;/code&gt; to maintain form state between pages.&lt;/p&gt;

&lt;p&gt;That solved the immediate problem, but it also raised a broader architectural question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could we describe an entire multi-step form as data and let a reusable library handle its rendering, state, validation, and navigation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the idea behind Demetria.&lt;/p&gt;

&lt;p&gt;The name itself came from Demeter, the Greek goddess associated with agriculture and seasonal cycles. I liked the analogy between the progression of seasons and the different stages of a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  02. Rebuilding an old idea
&lt;/h2&gt;

&lt;p&gt;Years later, I returned to Demetria with more experience in frontend architecture and library design.&lt;/p&gt;

&lt;p&gt;I didn't want to build another comprehensive form management framework.&lt;/p&gt;

&lt;p&gt;Libraries such as React Hook Form already solve many problems extremely well, but my goal was different.&lt;/p&gt;

&lt;p&gt;I wanted something that could work in a modern application or a traditional server-rendered system without requiring either one to adopt a particular frontend framework.&lt;/p&gt;

&lt;p&gt;The initial requirements were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use plain JavaScript or TypeScript.&lt;/li&gt;
&lt;li&gt;Accept JSON-compatible form definitions.&lt;/li&gt;
&lt;li&gt;Generate native HTML elements.&lt;/li&gt;
&lt;li&gt;Support multiple steps and built-in validation.&lt;/li&gt;
&lt;li&gt;Allow optional persistence across HTML documents.&lt;/li&gt;
&lt;li&gt;Leave visual styling to the consuming application.&lt;/li&gt;
&lt;li&gt;Keep runtime dependencies at zero.&lt;/li&gt;
&lt;li&gt;Treat accessibility as a core design concern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These requirements shaped the boundaries of the library.&lt;/p&gt;

&lt;p&gt;Rather than building a large ecosystem, I wanted a small tool with a clearly defined responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  03. Architecture: separating responsibilities
&lt;/h2&gt;

&lt;p&gt;One of the most important decisions was keeping form orchestration separate from presentation.&lt;/p&gt;

&lt;p&gt;The current architecture can be understood through three main responsibilities: the form engine, the DOM renderer, and optional persistence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture diagram
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
    A["Host application"]
    B["JSON-compatible schema"]
    C["createForm(options)"]
    D["Form engine"]
    E["Native DOM renderer"]
    F["HTML form"]
    G["User interaction"]
    H["Optional Web Storage"]
    I["Host CSS and integration"]

    A --&amp;gt; C
    B --&amp;gt; C
    C --&amp;gt; D
    D --&amp;gt; E
    E --&amp;gt; F
    F --&amp;gt; G
    G --&amp;gt; D
    D &amp;lt;--&amp;gt; H
    A --&amp;gt; I
    I --&amp;gt; F&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Figure 1. A conceptual overview of Demetria's architecture and integration boundaries.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The form engine
&lt;/h3&gt;

&lt;p&gt;The engine coordinates form state, validation, and navigation.&lt;/p&gt;

&lt;p&gt;It understands the schema, current step, values, and transition rules without depending on the application's visual presentation.&lt;/p&gt;

&lt;p&gt;This separation allows the same form behavior to be used in different integration environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The DOM renderer
&lt;/h3&gt;

&lt;p&gt;The renderer transforms the schema into native HTML elements, including &lt;code&gt;form&lt;/code&gt;, &lt;code&gt;fieldset&lt;/code&gt;, &lt;code&gt;legend&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;textarea&lt;/code&gt;, and &lt;code&gt;button&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It also connects user interactions to the form engine.&lt;/p&gt;

&lt;p&gt;Using native browser elements reduces the need for custom behavior and makes it easier to preserve familiar keyboard interactions and semantics.&lt;/p&gt;

&lt;p&gt;Demetria deliberately ships without mandatory CSS.&lt;/p&gt;

&lt;p&gt;Applications can use the generated styling hooks to apply their own visual identity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional persistence
&lt;/h3&gt;

&lt;p&gt;The third responsibility is state persistence.&lt;/p&gt;

&lt;p&gt;By default, Demetria keeps its state in memory.&lt;/p&gt;

&lt;p&gt;When an application needs to preserve data between page loads, it can explicitly enable &lt;code&gt;sessionStorage&lt;/code&gt; or &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is particularly important for the problem that originally inspired the project.&lt;/p&gt;

&lt;p&gt;The library doesn't need to become a router or manage the server-side application lifecycle. It only needs to restore compatible form state when the application initializes it again.&lt;/p&gt;

&lt;p&gt;That boundary keeps the implementation focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  04. A declarative API
&lt;/h2&gt;

&lt;p&gt;The public API revolves around &lt;code&gt;createForm()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of manually creating fields and coordinating individual inputs, the consuming application provides a JSON-compatible schema describing the form.&lt;/p&gt;

&lt;p&gt;Here is a working example based on Demetria's v0.1.1 API.&lt;/p&gt;

&lt;p&gt;First, create a target element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"demetria-form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then define the form in JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;demetria&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;personal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Personal information&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fullName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Full name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newsletter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Receive updates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contactMethod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Preferred contact method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Phone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;phone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#demetria-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;onComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Completed form:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Demetria generates the form, handles transitions between steps, validates fields, and passes the completed values to the application.&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;email&lt;/code&gt; doesn't include &lt;code&gt;required: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Demetria, &lt;strong&gt;fields are optional by default&lt;/strong&gt;. An optional email can be left empty, but a non-empty value must satisfy the applicable email validation.&lt;/p&gt;

&lt;p&gt;This distinction is important because optional does not mean unvalidated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a JSON-compatible schema?
&lt;/h3&gt;

&lt;p&gt;Choosing a declarative schema creates a clear contract between configuration and behavior.&lt;/p&gt;

&lt;p&gt;It allows a form definition to be serialized, shared, and understood without executing arbitrary JavaScript from the schema.&lt;/p&gt;

&lt;p&gt;But that choice has trade-offs.&lt;/p&gt;

&lt;p&gt;A declarative format can be less expressive than an API built around callbacks and custom components.&lt;/p&gt;

&lt;p&gt;For the initial release, I preferred a smaller, predictable contract over supporting every possible form requirement.&lt;/p&gt;

&lt;p&gt;The objective was to make the common behavior reliable before introducing greater extensibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfg6r4ygys9ek8vn566i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfg6r4ygys9ek8vn566i.png" alt="Single Page Form Example" width="799" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  05. One library, two navigation models
&lt;/h2&gt;

&lt;p&gt;A particularly important architectural requirement was supporting both single-page and multi-page flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single-page forms
&lt;/h3&gt;

&lt;p&gt;In a single-page flow, all steps exist within the same document.&lt;/p&gt;

&lt;p&gt;Demetria updates the active step without requiring a full browser navigation.&lt;/p&gt;

&lt;p&gt;The engine preserves values as users move forward and backward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-page forms
&lt;/h3&gt;

&lt;p&gt;In a multi-page flow, the browser loads separate HTML documents.&lt;/p&gt;

&lt;p&gt;The host application controls navigation, while Demetria restores compatible form state through explicitly configured persistence.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#demetria-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;persistence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sessionStorage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registration-flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;stepId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;stepId&lt;/code&gt; option selects the step that the current document should render.&lt;/p&gt;

&lt;p&gt;Different pages can initialize Demetria with the same schema and persistence key while explicitly selecting their corresponding step.&lt;/p&gt;

&lt;p&gt;This approach avoids introducing a client-side router.&lt;/p&gt;

&lt;p&gt;It also makes the library useful in traditional applications where navigation is controlled by the server or the surrounding application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy19k9jns1vcxzflzgd31.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy19k9jns1vcxzflzgd31.jpeg" alt="Multi page form example" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The persistence trade-off
&lt;/h3&gt;

&lt;p&gt;Browser storage is convenient, but it comes with limitations.&lt;/p&gt;

&lt;p&gt;Both &lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;sessionStorage&lt;/code&gt; are accessible to JavaScript executing in the same origin.&lt;/p&gt;

&lt;p&gt;They should not automatically be treated as safe places for sensitive personal information.&lt;/p&gt;

&lt;p&gt;For that reason, Demetria doesn't persist form state by default.&lt;/p&gt;

&lt;p&gt;Applications must explicitly enable it and remain responsible for evaluating the security and privacy requirements of their data.&lt;/p&gt;

&lt;p&gt;In applications collecting sensitive information, server-side persistence with appropriate security controls may be more suitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  06. Accessibility is more than semantic HTML
&lt;/h2&gt;

&lt;p&gt;Accessibility was one of the areas I wanted to approach differently when rebuilding Demetria.&lt;/p&gt;

&lt;p&gt;A form-generation library has an important responsibility: the markup and behavior it produces become part of the user experience in every consuming application.&lt;/p&gt;

&lt;p&gt;That means accessibility problems can propagate across integrations.&lt;/p&gt;

&lt;p&gt;The renderer therefore emphasizes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native semantic controls.&lt;/li&gt;
&lt;li&gt;Programmatically associated labels.&lt;/li&gt;
&lt;li&gt;Visible required-field indicators.&lt;/li&gt;
&lt;li&gt;Validation errors associated with their fields.&lt;/li&gt;
&lt;li&gt;Keyboard-operable navigation.&lt;/li&gt;
&lt;li&gt;Focus management after validation failures.&lt;/li&gt;
&lt;li&gt;Predictable focus when changing steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But semantic HTML alone isn't enough.&lt;/p&gt;

&lt;p&gt;Focus and state transitions are particularly important in multi-step experiences.&lt;/p&gt;

&lt;h3&gt;
  
  
  A real issue discovered during testing
&lt;/h3&gt;

&lt;p&gt;During the accessibility audit for v0.1.1, we found that completing a form could cause keyboard focus to be lost.&lt;/p&gt;

&lt;p&gt;The renderer unnecessarily rebuilt the form after completion.&lt;/p&gt;

&lt;p&gt;That removed the focused button from the document, leaving focus on the page body.&lt;/p&gt;

&lt;p&gt;Instead of adding a workaround that manually restored focus, we removed the unnecessary renderer rebuild.&lt;/p&gt;

&lt;p&gt;This preserved the focused element and corrected the underlying lifecycle behavior.&lt;/p&gt;

&lt;p&gt;It was a useful reminder that accessibility issues can reveal deeper implementation problems.&lt;/p&gt;

&lt;p&gt;The same audit identified improvements related to accessible naming, responsive reflow, contrast, and ARIA announcements.&lt;/p&gt;

&lt;p&gt;Automated browser tests and axe-core checks helped verify the fixes, but I don't consider those results proof of complete WCAG 2.2 AA conformance.&lt;/p&gt;

&lt;p&gt;Manual assistive-technology testing remains important.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpx0n83ksz2uhyvydtgew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpx0n83ksz2uhyvydtgew.png" alt="Form Error Example" width="687" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  07. Zero runtime dependencies as a design constraint
&lt;/h2&gt;

&lt;p&gt;Another deliberate choice was keeping Demetria free of runtime dependencies.&lt;/p&gt;

&lt;p&gt;The browser already provides most of the capabilities required for this problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native DOM APIs.&lt;/li&gt;
&lt;li&gt;HTML form controls.&lt;/li&gt;
&lt;li&gt;Events.&lt;/li&gt;
&lt;li&gt;Web Storage.&lt;/li&gt;
&lt;li&gt;JavaScript data structures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a large runtime dependency tree would introduce additional compatibility considerations and maintenance costs.&lt;/p&gt;

&lt;p&gt;That doesn't make dependencies inherently undesirable.&lt;/p&gt;

&lt;p&gt;It simply means they should solve a problem that justifies their cost.&lt;/p&gt;

&lt;p&gt;Demetria still uses development tools for TypeScript compilation, testing, linting, and accessibility verification.&lt;/p&gt;

&lt;p&gt;The goal is to keep the library lightweight for the applications that consume it.&lt;/p&gt;

&lt;h2&gt;
  
  
  08. Testing the actual distribution
&lt;/h2&gt;

&lt;p&gt;One lesson I wanted to apply during the rebuild was that successfully compiling source code isn't enough to prove that a library can be consumed.&lt;/p&gt;

&lt;p&gt;The initial historical npm release had packaging problems.&lt;/p&gt;

&lt;p&gt;For the new implementation, I wanted to verify the actual package artifact, not only the TypeScript sources.&lt;/p&gt;

&lt;p&gt;That meant checking the compiled JavaScript, type declarations, package exports, and tarball contents.&lt;/p&gt;

&lt;p&gt;We also added unit tests and browser-based checks for the generated forms and documentation examples.&lt;/p&gt;

&lt;p&gt;The first functional release, v0.1.0, established the library's core behavior.&lt;/p&gt;

&lt;p&gt;The next patch, v0.1.1, focused on accessibility and stability improvements without introducing breaking API changes.&lt;/p&gt;

&lt;p&gt;I prefer that sequence to rushing into new features before the existing functionality is reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  09. What I learned
&lt;/h2&gt;

&lt;p&gt;Rebuilding Demetria reinforced a few engineering principles that have become important in my work.&lt;/p&gt;

&lt;h3&gt;
  
  
  A good abstraction knows what it doesn't own
&lt;/h3&gt;

&lt;p&gt;Demetria handles form orchestration, but it doesn't need to become a frontend framework, router, validation ecosystem, or form-building platform.&lt;/p&gt;

&lt;p&gt;Keeping those responsibilities separate makes the library easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Framework independence should solve a real problem
&lt;/h3&gt;

&lt;p&gt;Being framework-agnostic isn't automatically valuable.&lt;/p&gt;

&lt;p&gt;In this case, it supports a concrete integration requirement: generating form experiences in applications that may not use a modern frontend framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessibility is part of behavior
&lt;/h3&gt;

&lt;p&gt;Labels and semantic elements matter, but accessibility also depends on navigation, focus, validation, announcements, and lifecycle behavior.&lt;/p&gt;

&lt;p&gt;It needs to influence architecture and testing, not just markup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declarative APIs create valuable boundaries
&lt;/h3&gt;

&lt;p&gt;A JSON-compatible schema makes the form structure explicit.&lt;/p&gt;

&lt;p&gt;That improves portability and predictability while establishing clear limitations around extensibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small, functional releases build confidence
&lt;/h3&gt;

&lt;p&gt;A library doesn't need dozens of features to be useful.&lt;/p&gt;

&lt;p&gt;A focused implementation, working examples, accurate documentation, and a reliable distribution process are a much better starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. What's next?
&lt;/h2&gt;

&lt;p&gt;Demetria is still an early-stage open-source project.&lt;/p&gt;

&lt;p&gt;Future improvements may include conditional fields, custom validation rules, and more flexible renderer customization.&lt;/p&gt;

&lt;p&gt;But I don't want to expand the API simply to increase the number of features.&lt;/p&gt;

&lt;p&gt;The next changes should come from actual integration needs and feedback from developers using the library.&lt;/p&gt;

&lt;p&gt;For now, the main goal is to make the existing functionality understandable, accessible, and reliable.&lt;/p&gt;

&lt;p&gt;Demetria started with a complex form in a server-rendered Java application.&lt;/p&gt;

&lt;p&gt;Years later, the same idea became an opportunity to revisit library architecture, declarative APIs, browser persistence, and accessibility.&lt;/p&gt;

&lt;p&gt;And that's probably my favorite part of the project.&lt;/p&gt;

&lt;p&gt;Sometimes, revisiting an old engineering problem with a different perspective is enough to turn a specific solution into something reusable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Explore Demetria
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/gustavoaroberto/demetria" rel="noopener noreferrer"&gt;https://github.com/gustavoaroberto/demetria&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation and live examples:&lt;/strong&gt; &lt;a href="https://gustavoaroberto.github.io/demetria/" rel="noopener noreferrer"&gt;https://gustavoaroberto.github.io/demetria/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/demetria" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/demetria&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Gustavo A Roberto · Frontend Engineering, Architecture &amp;amp; Technical Leadership&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
