Why Domma never uses eval

Knockout-style bindings usually mean compiling strings into code at runtime. Domma Reactive parses them by hand instead, so it runs under a strict Content Security Policy.

Declarative bindings are a lovely way to build an interface. You write data-bind-text="user.name" in your HTML, and the page stays in step with the data from then on. The catch, in most libraries that work this way, is how that little string becomes something that runs.

The usual shortcut

The quickest way to turn user.name or count > 1 ? 'items' : 'item' into a value is to hand it to JavaScript's own compiler - eval, or its close cousin new Function(...). Knockout, the library that made this style popular, compiles binding strings with the Function constructor. It works, and it is short.

It also means the page is running strings as code. That is exactly what a Content Security Policy exists to stop. A policy of script-src 'self' tells the browser: only run scripts that came from my own server, and never compile strings into code. It is one of the most effective defences a site has against cross-site scripting - if an attacker manages to get text onto your page, a strict policy stops that text from running.

A library that needs eval forces you to add 'unsafe-eval' to the policy, and the name is honest about what that does.

Parsing by hand

Domma Reactive - the reactive core inside Domma, published on its own - does not use eval or the Function constructor anywhere. Binding expressions are read by a small hand-written parser: it tokenises the string, builds a tree of the parts it understands, and evaluates that tree against your data.

What it understands is deliberately a subset of JavaScript expressions: property paths, the usual operators, comparisons, the ternary, function calls on functions you provide, and - since 1.2 - object and array literals such as {active: isOn.value} or [a, b]. What it refuses, it refuses clearly: a binding that will not parse logs one warning naming the expression, the element and the position of the problem, and is skipped. Everything else on the page keeps working.

Refusing is part of the design. Keys like __proto__ and constructor are rejected in object literals, and there is no way for a binding to reach outside the data you gave it. A binding is a description of a value, not a place to run code.

What you get

  • The whole library works under script-src 'self' with no unsafe-eval.
  • No build step: a <script> tag on a page your server already rendered is enough.
  • About 21 KB gzipped, no dependencies, MIT-licensed, and 996 tests.
  • Errors you can act on, instead of a page that silently stops updating.

Safe by default, not just safe to parse

Version 1.2 took the same idea one step further. A common slip is binding the observable itself instead of its value - data-if="show" rather than data-if="show.value". An observable is an object, and objects are truthy, so the content used to show. Now the binding warns once - "show" is an observable, not its value - use "show.value" - and treats it as empty: hidden content stays hidden. When a mistake happens, the failure is the safe one.

Inside Domma CMS

Domma CMS uses Domma Reactive throughout its admin, and the public pages you build with collections, forms and components run on the same library. When you ship a Domma CMS site with a strict Content Security Policy, the bindings are not the thing that makes you loosen it.

If you want to try it on its own, the Domma Reactive site has a running demo beside every feature, a comparison with Knockout, Alpine.js and petite-vue, and a tutorial that builds a contacts page in about 120 lines. The source is on GitHub.