How to Build an AI Generated Calculator Without Custom JavaScript

Written by anywhichway | Published 2026/01/24
Tech Story Tags: ai | ai-generated-ui | lightview | a2ui | jprx | json-schema | xpath | hackernoon-top-story

TLDRDeclaritive User Interfaces For Humans and AI: Letting AI write raw JavaScript introduces security and maintenance issues. Instead, we should move toward a "Data as UI" model using declarative, sandboxed formats based on industry standards like JSON Pointers, XPath, and JSON Schema.via the TL;DR App

How a JSON or JSON-like language enables the next generation of safe human and AI-generated UIs

Introduction

In The Future of AI-Generated UI, I pointed out that the raw JavaScript generated by AI-generated code is a security problem, and the flexibility of JavaScript without a framework can result in hard-to-manage code. I argued that we need declarative, sandboxed formats like A2UI or a Computational DOM (cDOM) with JSON Pointer Regular Expressions (JPRX if), if we want to trust an LLM to build our interfaces.

It also helps if the approach is based on industry standards for which there is lots of documentation and examples that have probably been consumed by LLMs during training. cCOM and JPRX do this; they incorporate concepts and syntax from JSON Pointers, JSON Schema, and XPath.

In my previous article, to show how a cDOM and JPRX work, I used a reactive counter, but let's be real: reactive counters and to-do lists are easy. Any framework looks elegant when the logic fits on a napkin. To prove a JSON-based approach actually holds up, you need a problem with messy state, edge cases, and distinct modes of operation. You need a calculator.

Calculators are inherently tricky:

  • Input Modes: Are we typing a fresh number or appending to an existing one?
  • Chaining: What happens when you hit `+` then `-` then `*` without hitting equals?-
  • DRY Logic: How do we minimize code differences between 10 handlers for buttons 0-9?

So, I asked Claude Opus to build a fully functional, iOS-style calculator using zero custom JavaScript functions - just declarative cDOM and JPRX expressions. The fact that AI could produce a declarative calculator with little prompting purely from documentation demonstrates another point I made in my earlier article: cDOM and JPRX aren't just new syntax. They can be a protocol for human-machine collaboration.


The Code

To reduce characters and quotation noise while allowing inline explanation, I am using cDOMC, a compressed version of a cDOM. A regular cDOM does not support comments and requires quotes around attributes and JPRX expressions. When represented with quotes and without comments, cDOM can be treated as regular JSON.

{
  div: {
    class: "calculator",
    // A calculator feels stateless, but it's actually a strict state machine. 
    // You're never just "typing a number"; you're either entering the first operand, 
    // waiting for an operator, or entering the next operand.
    onmount: =state({
      display: "0", // What you see on the screen
      expr: "", // History string, (e.g. "8 + 5 =")
      prev: "", // value stored before an operation
      op: "", // the active operator
      waiting: false // true when expecting a new number vs operator
    },{
      name: "c", // the root name of our state, so we can express things like: /c/display
      schema: "polymorphic", // allow type changes, e.g. "0" or 0
      scope: $this // scope the path to the current element
    }),
    children: [
      // Display area
      {
        div: {
          class: "display",
          children: [
            { div: { class: "expression",children[=/c/expr] }},
            { div: { class: "result",children[=/c/display] }}
          ]
        }
      },
      // Button grid
      {
        div: {
          class: "buttons",
          children: [
            // Row 1: AC, ±, %, ÷
            {
              button: { 
                class: "btn btn-clear", 
                onclick: =/c = { display: "0", expr: "", prev: "", op: "", waiting: false }, 
                children: ["AC"] 
              } 
            },
            { 
              button: { 
                class: "btn btn-function", 
                onclick: =/c = { display: negate(/c/display), waiting: true, expr: "" }, 
                children: ["±"] 
              } 
            },
            {
              button: { 
                class: "btn btn-function", 
                onclick: =/c = { display: toPercent(/c/display), waiting: true, expr: "" }, 
                children: ["%"] 
              } 
            },
            // Divison is our first operator. This is where it gets tricky. 
            // When you click `+`, you can't just link `prev` to `display`. 
            // If you did, `prev` would update every time you selected a new digit for the**second**number, 
            // breaking the math. We need a snapshot of the value at that exact moment.
            // Excel solves this with INDIRECT, effectively dereferencing a cell. JPRX borrows the same concept:
            { 
              button: { 
                class: "btn btn-operator", 
                onclick: =/c = { 
                  prev: indirect(/c/display), // Capture the value right now
                  expr: concat(/c/display, " ÷"), 
                  op: "/", waiting: true 
                  }, 
                children: ["÷"] 
              } 
            },          
            // Row 2: 7, 8, 9, ×
            // I have 10 number buttons. Do I write 10 handlers? Do I write a loop? In React or Vue, 
            // you'd probably map over an array. With JPRX, the DOM is the data key and although map is available, 
            // I represent the calculator using literals in this example. In a future article I will cover map. 
            // By giving each button an `id` (e.g., `id: "7"`), we write a uniform logic expression that adapts 
            // to whichever element triggered it. We just reference $this.id in JPRX and use an xpath to get the text
            // content for the child node, #../@id. In cDOM (not JPRX) '#' delimits the start of an xpath expression
            { 
              button: { 
                id: "7", 
                class: "btn btn-number", 
                onclick: =/c = { 
                  display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false
                }, 
                children: [#../@id] // use xpath (starting char #) to get the text for the button from parent id
              } 
            },
            // Here's what is happening:
            // Waiting for input? (e.g., just hit `+`) → Replace the display with the button's ID.
            // Displaying "0"? → Replace it (avoids "07").
            // Otherwise: → Append the button's ID.
            // This is replicated identically for every number button. No loops, no external helper functions.
            {
              button: { 
                id: "8", 
                class: "btn btn-number", 
                onclick: =/c = { 
                  display: if(/c/waiting, $this.id, if(/c/display==0), $this.id, concat(/c/display, $this.id))), waiting: false 
                }, 
                children: [#../@id] 
              } 
            },
            { 
              button: { 
                id: "9", 
                class: "btn btn-number", 
                onclick: =/c = { 
                  display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false 
                }, 
                children: [#../@id] 
              } 
            },
            { 
              button: { 
                class: "btn btn-operator", 
                onclick: =/c = { 
                  prev: indirect(/c/display), expr: concat(/c/display, " ×"), op: "*", waiting: true 
                }, 
                children: ["×"] 
              } 
            },
                        
            // Row 3: 4, 5, 6, −
            { 
              button: { 
                id: "4", 
                class: "btn btn-number", 
                onclick: =/c = { 
                  display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false 
                }, 
                children: [#../@id] 
              } 
            },
            { 
              button: { 
                id: "5", 
                class: "btn btn-number", 
                onclick: =/c = { 
                  display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false 
                }, 
                children: [#../@id]
              }  
            },
            { 
              button: { 
                id: "6", 
                class: "btn btn-number", 
                onclick: =/c = { 
                  display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false 
                }, 
                children: [#../@id] 
              } 
            },
            { 
              button: { 
                class: "btn btn-operator", 
                onclick: =/c = { 
                  prev: indirect(/c/display), expr: concat(/c/display, " −"), op: "-", waiting: true 
                  }, 
                children: ["−"] 
              } 
            },
                        
            // Row 4: 1, 2, 3, +, use set and eq just to demonstrate equivalence with = and ==
            // the buttons below use 'set' in place of the infix operator '=', just to show a different way of doing things
            { 
              button: { 
                id: "1", 
                class: "btn btn-number", 
                onclick: =set(/c, { 
                              display: if(/c/waiting, $this.id, 
                                if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), 
                              waiting: false 
                         }), 
                children: [#../@id] 
              } 
            },
            { 
              button: { 
                id: "2", 
                class: "btn btn-number", 
                onclick: =set(/c, { 
                            display: if(/c/waiting, $this.id, 
                              if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), 
                            waiting: false 
                          }), 
                children: [#../@id] 
              } 
            },
            {
              button: { 
                id: "3", 
                class: "btn btn-number", 
                onclick: =set(/c, { 
                          display: if(/c/waiting, $this.id, 
                            if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))),
                          waiting: false 
                        }), 
                children: [#../@id] 
              } 
            },
            { 
              button: { 
                class: "btn btn-operator", 
                onclick: =set(/c, { 
                            prev: indirect(/c/display), 
                            expr: concat(/c/display, " +"), 
                            op: "+", waiting: true }), 
                children: ["+"] 
              } 
            },
                        
            // Row 5: 0, ., =
            { 
              button: { 
                id: "0",
                class: "btn btn-number btn-wide", 
                onclick: =set(/c, { 
                            display: if(/c/waiting, $this.id,   
                              if(eq(/c/display, "0"), "0", concat(/c/display, $this.id))), 
                            waiting: false }), 
                children: [#../@id] 
              } 
            },
            {
              button: 
              { 
                class: "btn btn-number", 
                onclick: =set(/c, { 
                            display: if(/c/waiting, "0.", 
                              if(contains(/c/display, "."), /c/display, concat(/c/display, "."))), 
                            waiting: false }), 
                children: ["."] 
              } 
            },
            // Finally, the math. We need to say:
            // 1. Take the snapshot we stored
            // 2. Apply the current operator
            // 3. combine it with what's on screen now
            // This is the job of calc(). If prev == 8 and op == * and display = 5, then calc would be evaluated as calc("8 * 5")
            // To keep the syntax a little cleaner we also use $(<path>) as a shorthand for indirect.
            { 
              button: 
              { 
                class: "btn btn-equals", 
                onclick: =set(/c, { 
                            display: if(eq(/c/op, ""), /c/display, calc(concat("$('/c/prev') ", /c/op, " $('/c/display')"))), 
                            expr: concat(/c/expr, " ", /c/display, " ="), 
                            prev: "", op: "", 
                            waiting: true }), 
                children: ["="] 
              } 
            }
          ]
         }
     },
     // Branding
     {
       div: {
         class: "branding",
         children: [
           { 
              span: { 
                children: [
                  "Built with ", 
                  { 
                    a: { 
                      href: "https://github.com/anywhichway/lightview", target: "_blank", 
                      children: ["Lightview"] 
                    } 
                  }, 
                  " cDOM • No custom JS!" 
                ] 
            } 
          }
        ]
       }
     }
   ]
  }
}

Loading cDOM via Lightview Hypermedia

Lightview supports hypermedia capability similar to HTMX by allowing the use of the src attribute on almost any element.

Simply reference a cDOM file using src:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">    
    <meta name="description"
      content="A beautiful calculator built with Lightview cDOM and JPRX reactive expressions - no custom JavaScript!">
    <title>Calculator | Lightview cDOM</title>    
    <link rel="stylesheet" href="calculator.css">    
    <!-- Load Lightview scripts -->    
    <script src="/lightview.js"></script>  <!-- DOM as JSON and reactivity support -->    
    <script src="/lightview-x.js"></script> <!-- hypermedia support -->    
    <script src="/lightview-cdom.js"></script> <-- cDOM/JPRX support -->
    </head>
  <body>    
    <!-- The calculator cDOM is loaded via Lightview's hypermedia src attribute -->    
    <div id="app" src="./calculator.cdomc"></div>
  </body>
</html>

The src attribute works like an HTML <img> or <script> tag - Lightview automatically fetches the .cdomc file, parses it, and renders the reactive content into the target element. This approach:

Why Build This Way?

You might look at concat("$('/c/prev') ...") and ask: Why in the world wouldn't you just write parseFloat(prev) + parseFloat(curr)?

If you are a human writing code for yourself? You probably would. Lightview supports standard JS handlers for exactly that reason.

But if you are building infrastructure for AI Agents, the calculus changes. Sticking to a declarative, JSON-based path offers things raw code can't:

  • Sandboxing: It executes in a controlled environment. The logic can't access `window`, make global fetch requests, or execute arbitrary secondary code. This makes it safe to "hot swap" UI logic generated by an LLM in real-time.
  • Portability: This entire UI—logic and all—is just data. It can be sent from a server, stored in a database, or streamed from an AI model.
  • Mental Model: It forces a clear separation between state transformations and view structure, which is exactly how LLMs reason best.


This calculator proves that "declarative" doesn't have to mean "dumb." With the right primitives - state, conditionals, and path-based referencing—you can build rich, complex interactions without ever leaving the data structure.

The Bigger Picture

This series isn't just about a new library. It's about finding the right abstraction layer for the AI age.

In The Future of AI-Generated UI, we looked at the risks of letting LLMs write raw scripts and introduced the "Data as UI" philosophy.


In this article, we showed that "Data as UI" doesn't mean "dumb UI." We handled state, context, data snapshots, math, and DOM navigation with ‘xpath’ without executing a single line of custom JavaScript.


cDOM defines structure. JPRX defines behavior. It’s reactivity without the compilation and UI without the security risks.

Try It Yourself

The complete calculator is available at:


Written by anywhichway | Working in the clouds around Seattle ... sailing when it's clear.
Published by HackerNoon on 2026/01/24