Widget · counted in the browser
A calculator on any page, one line of HTML.
The calc widget builds a calculator from a JSON file: the fields, their limits and units, the formulas and the formatting. It counts again on every keystroke, keeps the numbers in the hash of the address and says where every number comes from.
One line to connect it
The line goes into the page. The widget reads the file of the calculator, builds the form, draws the table and the chart, and writes the numbers into the hash of the address.
<div class="calc" data-calc="assets/calc/data/demo.json"><script src="assets/calc/calc-widget.js" defer></script></div>
On a page of the factory the same widget comes from one line in the data file of the page:
"calc": "assets/calc/data/demo.json"
- Put the line on the page. The path of the file is relative, so the page keeps working under any mount.
- Put the files of the widget in the assets of the page. The engine, the widget and the stylesheet. The build of the factory copies them by itself for every page that asks for a calculator; a page of the site already has the assets folder.
- Write the calculator as a JSON file. The fields, the limits, the units, the formulas and the formatting — nothing else.
- Run the test.
node apps/web/tools/calc/test_calc.jscompares the numbers of the demo with the numbers worked out by hand and refuses a formula that reaches outside the file.
The stylesheet of the widget is added by the script itself. A page that wants the calculator without a flash of unstyled text may also put <link rel="stylesheet" href="assets/calc/calc.css"> in the head, as this page does.
The calculator is a JSON file
No code in the page: the fields, their limits and units, the formulas and the formatting are all in one file that the widget reads.
{
"id": "demo-film-speed",
"fields": [
{ "id": "length", "label": "Length of the film",
"type": "number", "min": 1, "max": 600, "default": 120,
"units": [ { "value": "min", "factor": 1 },
{ "value": "h", "factor": 60 } ] }
],
"outputs": [
{ "id": "minutes_of_watching", "label": "Minutes of watching",
"expr": "minutes_of_film / lookup(\"speed\", pace)",
"format": { "style": "duration" } }
],
"source": { "label": "Demo values made for the demo page of the calc widget" }
}
| Field | Type | Limits |
|---|---|---|
| Length of the film | number | 1 to 600, minutes or hours |
| Speed of the player | choice | Slower, the speed of the film, faster |
Demo values made for the demo page of the calc widget — they are not a claim about the service.
A formula cannot leave the file
The formulas run in a small language of their own. There is no eval and no call of a function from outside; a name that the file does not declare is refused before the calculator is drawn.
minutes_of_film / lookup("speed", pace)
No eval
The formula is read by a small parser of the widget: numbers, text, comparisons, arithmetic and a fixed list of functions. A statement, a semicolon or a call of an outside function is a mistake, not a secret door.
Only the names of the file
A formula may use the fields, the constants, the tables and the outputs above it. Every other name is reported before the widget draws anything.
The link with the numbers
The state of the calculator travels in the hash of the address, so a link opens the same calculation. Copy the link and the reader sees your numbers, not their own.
Nothing is sent
There is no form and no account. The numbers are counted in the browser, and the only request the page makes is for the file of the calculator.
Where the numbers of the demo come from
The calculator on this page is a demo: it counts the length of a film at another speed, and every number it starts from is an example made for this page. The test of the widget compares the demo with the numbers worked out by hand on every run, so a number cannot drift away from the file unnoticed.
| What | Value | Where it comes from |
|---|---|---|
| Length of the film | 120 minutes by default, up to 600 minutes | Demo value of the widget |
| Speed of the player | three quarters of the speed, the speed of the film, one and a half times the speed | Demo value of the widget |
| An hour | 60 minutes | Arithmetic, not a claim |
| An evening of watching | 150 minutes | Demo value of the widget |
Demo values made for the demo page of the calc widget — they are not a claim about the service.
About the widget
What happens to the numbers I type?
They stay in the browser. The counting happens in JavaScript, the numbers travel in the hash of the address, and nothing is sent to the server. There is no form, no account and no file behind the calculator.
Can a formula read something from the outside?
No. The formula is parsed by the widget itself and may only use the fields, the constants, the tables and the outputs of the same file. There is no eval, and a name that the file does not declare is reported before anything is drawn.
Can the widget be used on a page of the factory?
Yes. The template of the page does not change: the data file of the page gets one key, calc, with the path of the file of the calculator. The build puts the widget into the page and copies the files of the widget into the assets of that language.
Does the page work without JavaScript?
The text and the numbers of this page are in the markup as well, so the page reads without the script. The calculator itself needs it: the form, the chart and the link are built in the browser.
What about the screen readers and the keyboard?
Every field has a label, the results are a real table with a caption, and the chart is an SVG with a title and a table with the same numbers under it. The results are announced through a polite live region, and nothing moves for a reader who asked for less motion.
Where do the numbers of the calculator come from?
They are demo values made for this page: the length of a film, the speed of the player and an hour of sixty minutes. They are not a claim about the service. The test of the widget compares them with the numbers worked out by hand, so a change in the demo has to be confirmed in the test as well.