A Todo App in plain HTML
A complete Todo app without writing JavaScript.
What is “Fore” and how can we use it to build an Todo app?
This article gives an introduction to Fore — a framework for building user interfaces in plain HTML5.
Here you find the live demo and sources.
What is Fore?
‘Fore’ (as in ‘foreground’) is a framework that uses HTML tags (Web Components) to build user interfaces. What sets it apart is the fact that you don’t need to write any JavaScript to build even very complex applications. /
Key Features:
- Application State Engine with 2-way data-binding
- support for multiple data structures at runtime
- support for XML, HTML and JSON as data formats currently
- implemented as Vanilla Web Components with minimal dependencies
- extensible with JavaScript, XPath and XQuery/XUpdate functions
- no build tools required
Get started
To get started just create a plain HTML file and add those two lines:
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@jinntec/fore@latest/resources/fore.css">
<script type="module"
src="https://cdn.jsdelivr.net/npm/@jinntec/fore@latest/dist/fore.js">
This will be enough to load Fore from a CDN into your page.
It’s all just tags
Fore is fully descriptive. That means that all functionality is provided by custom HTML tags. However you can hook in JavaScript if desired but it will rarely be needed.
So let’s get started by adding Fore to the body of your page:
<body>
<fx-fore>
</fx-fore>
</body>
The fx-fore
Element creates a scope similar to a usual form
tag in HTML. You might have several of those in your page if things get more complicated. For a simple Todo app a single one is sufficient.
All Fore element are Web Components and therefore follow the convention of using a hyphen in their name. The prefix is always ‘fx-’.
The structure of a Fore element
Fore keeps all state related things in a model. Let’s add it.
<body>
<fx-fore>
<fx-model>
<fx-model>
</fx-fore>
</body>
The model is a ‘blind’ element. It is not rendered at all.
For simple use cases the model can be auto-generated and implied by the structure of bindings. Examples are in our demos.
Models keep data in instances which hold our data-structure(s):
<body>
<fx-fore>
<fx-model>
<fx-instance>
<data>
</data>
</fx-instance>
<fx-model>
</fx-fore>
</body>
We also add a ‘data’ element as root for our structure. The name is arbitrary and can be chosen freely. There just needs to be a single root element. JSON however could be inlined directly but we’re not using it here.
We define our Todo items as a simple HTML tag for our todo app. It will use it’s text node for describing the task and add ‘complete’ and ‘due’ attributes as additional state.
<task complete="false" due=""></task>
We could have used inline or external XML or JSON also.
Our source now looks like this:
<body>
<fx-fore>
<fx-model>
<fx-instance>
<data>
<task complete="false" due=""></task>
</data>
</fx-instance>
<fx-model>
</fx-fore>
</body>
Adding the UI
Now that we’ve setup the bare-bones of our model we can proceed with adding the visual part of the app alongside the model. We start by adding a heading and a repeat control that will stamp out our todo items.
<body>
<fx-fore>
<fx-model>
<fx-instance>
<data>
<task complete="false" due=""></task>
</data>
</fx-instance>
<fx-model>
<h1>Todos</h1>
<fx-repeat ref="task">
<template>
</template>
<fx-repeat>
</fx-fore>
</body>
The fx-repeat
element provides a template that is stamped out for every occurrence of ‘task’ in the instance. If we got 3 tasks we would get the contents of the template rendered 3 times — you get it.
Note the ref
attribute on the repeat — this is the binding expression referring to the task.
To actually display some controls we add the following:
fx-repeat id="r-task" ref="task">
<template>
<div>
<fx-control ref="@complete" value-prop="checked" update-event="input">
<input class="widget" type="checkbox">
</fx-control>
<fx-control id="task" class="complete {@complete}" ref="."></fx-control>
<fx-control ref="@due">
<input type="date">
</fx-control>
</div>
</template>
</fx-repeat>
Ok, here’s a lot new stuff to explain.
For every repeat entry we output a div containing three fx-control
elements to bind to our data. fx-control
is generic and needs some more explanation.
One control to bind them all
fx-control
is special —it does not provide any visual representation itself but instead wraps whatever ‘widget’ you prefer to use for your application. fx-control
just provides the binding between a concrete widget and the data item it is binding to in the data.
The motivations behind this are:
- keep the codebase small
- allow maximum freedom in choosing your favourite widgets. Problems and limitations of large component libraries are a topic for itself. Fore allows mixing and matching instead of adding another collection of controls that never covers everything.
- avoid exploding dependencies but let the user decide which extras to use when native widgets are not enough.
The price to pay is the extra element you need to write. However in practice you usually want a wrapper element for layout (alignment) purposes anyway and you can just use the fx-control
to host a label, the widget and optional alerts.
Let’s have a look at the attributes. We already came across the ref
attribute on our first control and we bound it to the ‘@complete’ attribute of our task.
The ref is ‘scoped’ meaning it resolves upwards the tree to determine the bound data item. To do that it looks upwards for further ‘ref’ attributes and if present appends itself to them.
In our example this will result in the path ‘task/@complete’ (a slash is used as devider). By scoping you can address hierarchical structures without repeating the full path all the time.
Some readers may recognise XPath here and that’s right. Fore uses XPath 3.1 as the binding language. The syntax is easy to learn, standard-based and very powerful as it can address either markup (HTML and XML) and JSON structures.
The repeat element has an index (defaulting to 1) and resolves the ‘ref’ to the correct index item — when the second task has focus it resolves to ‘task[2]/@complete’.
Still here? With binding being understood you got most of it covered already. So let’s go on…
value-prop
and update-event
are two optional attributes of the control. With value-prop
you define which property of your ‘widget’ you want to use as the value of the control. If not given it will default to ‘value’.
The update-event
attribute specifies the event that triggers the updating of the bound node in the model. It defaults to ‘blur’. Whenever that event fires the widget will trigger the updating of the bound data item in the model replacing it with the value of the widget.
Ok, but what about the ‘visual’ part? A concrete control (whether it’s a native control, some Web- or JavaScript-Component ) is referred to as ‘widget’ in Fore.
Even another
fx-fore
element can be used as widget but that’s makes a complete article in itself.
It defaults to a native input
element. When the fx-control
is empty it will create it for you.
<fx-control ref="task"><fx-control>
creates
<fx-control ref="task">
<input type="text">
</fx-control>
To use a custom control that is not a native input
you have to add a class ‘widget’ to mark it as the widget. This example does not relate to our todo app and is just to illustrate the point.
<fx-control ref="message">
<label>Message
<!-- binding a textarea as widget -->
<textarea class="widget"></textarea>
<label>
</fx-control>
... or ...
<fx-control ref="message">
<!-- binding a Polymer paper-input as widget -->
<paper-input label="Message" class="widget"></textarea>
</fx-control>
... or ...
<fx-control ref="message">
<!-- binding an arbitrary web-component as widget -->
<my-input class="widget">
<label>Message</label>
</my-input>
</fx-control>
... or ...
Adding todo items with the insert action
Instead of coding the same operations over and over in JavaScript Fore provides a set of ready-made actions that can perform tasks like:
- inserting, deleting, replacing and updating data nodes
- displaying messages with different types
- sending or loading data
- dispatching events
- show/hide dialogs
- and much more…
In our example we now like to add an ‘insert’ action to add a todo to our list. I just add it to the heading for prominent display:
<h1>Todo
<fx-trigger class="btn add">
<button>+</button><!-- triggering element -->
<!-- action(s) -->
<fx-insert ref="task" origin="instance('helper')/task" at="1" position="before"></fx-insert>
</fx-trigger>
</h1>
fx-trigger
is similar to control in that it is just a wrapper around the real triggering element — a button in our case. Here we don’t need a marker class — the first child element will be taken as the triggering element.
The insert action will create a new ‘task’ in our data and put it before the first item (latest on-top).
What’s that origin="instance('helper')/task
? To insert we need a template data item as our list shall be empty initially. The origin
attribute points to the template that gets inserted. We put that into a new separate instance. It may provide some sensible defaults for new entries.
Time to see the whole picture:
<body>
<fx-fore>
<fx-model>
<fx-instance>
<data>
</data>
</fx-instance>
<fx-instance id="helper">
<data>
<task complete="false" due=""></task>
</data>
</fx-instance>
<fx-model>
<h1>Todo
<fx-trigger class="btn add">
<button>+</button>
<fx-insert ref="task" origin="instance('helper')/task" at="1" position="before"></fx-insert>
</fx-trigger>
</h1>
<fx-repeat ref="task">
<template>
<div>
<fx-control ref="@complete" value-prop="checked" update-event="input">
<input class="widget" type="checkbox">
</fx-control>
<fx-control id="task" class="complete {@complete}" ref="."></fx-control>
<fx-control ref="@due">
<input type="date">
</fx-control>
</div>
</template>
<fx-repeat>
</fx-fore>
</body>
We have 2 instances now. Starting with the second you have to give them an id
. The instance()
function may take an id (otherwise always returns the first) and allows to refer to a specific instance.
We created an instance called 'helper’ here for the purpose of holding our origin
template and keeping it seperate from the todo list. The fx-insert
will fetch the template, clone and insert it into the default instance (the first one) at given ref
. The at
specifies the insertion index and position
can be ‘before’ or ‘after’.
Please note that you can ignore the ‘data’ part in an instance path. This doesn’t need to be given to reduce typing.
Delete an item
After all this long preliminary story it’s now getting easier i promise ;)
To add a delete button to every todo item we simply put it into the repeat template:
<fx-repeat ref="task">
<template>
<div>
<fx-control ref="@complete" value-prop="checked" update-event="input">
<input class="widget" type="checkbox">
</fx-control>
<fx-control id="task" class="complete {@complete}" ref="."></fx-control>
<fx-control ref="@due">
<input type="date">
</fx-control>
<fx-trigger class="btn delete">
<button>x</button>
<fx-action>
<fx-delete ref="."></fx-delete>
<fx-send submission="save"></fx-send>
</fx-action>
</fx-trigger>
</div>
</template>
<fx-repeat>
The delete action uses a ref=”.”
attribute. The dot refers to the current node in XPath which resolves to the current task. As with the insert action we have the fx-delete
nested within a trigger element.
Saving todos in localStorage
This section introduces submissions. Think of submissions as an fetch on steroids. These are part of the model and allow loading and saving of data, treatment of the response and error- and success-handling.
Let’s add a submission to store our todos to localStorage:
<fx-submission id="save"
url="localStore:todos"
method="post"
replace="none">
<fx-message event="submit-done">Changes have been stored</fx-message>
</fx-submission>
First of all a submission needs an id for referral. The url
points to the location we want to send our data. Fore has built-in support for localStorage which we’ll use in this example. It will be activated with the ‘localStore’ protocol. Otherwise the standard http protocol with the known standard methods is used.
We use a method ‘post’ to store the data under key ‘todos’. The replace
attribute defines what to do with the response of the operation. The ‘localStore’ handler won’t return anything for a POST so we just say replace="none"
meaning we are not using the response (read on).
The submit-done
event is triggered when the submission was successful and is used to show a message. Likewise we could have attached a message for an error by using the submit-error
event.
As i want the todos to be stored on any value change of a control i add an event-handler that does exactly that:
<fx-action event="value-changed">
<fx-send submission="save"></fx-send>
</fx-action>
Each of the fx-control
elements fires a value-changed
event when they get changed. By catching it further up in the tree we can handle all of them in a central place.
Actions add themselves as listeners on their parent element. The followingvalue-changed
listener attaches to the <div>
element.
<fx-repeat id="r-task" ref="task">
<template>
<div>
<!-- the action attaches to its parent always (the <div>) -->
<fx-action event="value-changed">
<fx-send submission="save"></fx-send>
</fx-action>
<fx-control ref="@complete" value-prop="checked" update-event="input">
<input class="widget" type="checkbox">
</fx-control>
...other value-changed emitting controls...
The fx-action
is just a wrapper for a block of other actions and wouldn’t be needed here strictly. Instead this would have been enough and have the same effect:
<fx-send submission="save" event="value-changed"></fx-send>
The send
action triggers a submission. The submission
attribute is an idref to the corresponding fx-submission
element.
Loading the data
Saving alone wouldn’t be of much use. Let’s add the counterpart:
<fx-submission id="load"
url="localStore:todos"
method="get"
replace="instance">
<fx-message event="submit-done">Todos loaded from localStorage</fx-message>
</fx-submission>
It gets a unique id
again, uses a GET method instead and specifies replace="instance"
to load the ‘response’ data into our data instance. The contents of the response will replace the instance data.
fx-submission
has a lot of further options to control the details of request/response processing which can be studied in our vast collection of examples.
As with the ‘save’ we need an event to load the data. Luckily Fore gives us some lifecycle events to hook into. Whenever the page loads it will trigger initialisation of the model which concludes with dispatching a model-construct-done
event.
By adding this to the model we can now trigger the loading on page load.
<fx-model>
<fx-send submission="load" event="model-construct-done"></fx-send>
...
Erasing the data
For completeness we should allow users to erase the data from localStorage again.
<fx-submission id="clear"
url="localStore:todos"
method="delete">
<fx-message event="submit-done">Todos erased from localStorage</fx-message>
<fx-reload event="submit-done"></fx-reload>
</fx-submission>
You guess it — another id, same url and a method="delete"
will do it. On submit-done
we inform the user and reload the page. All data will be gone.
Adding state to data
To add state (calculations and constraints) to data items the fx-bind
element is used. The state is automatically kept up-to-date in case conditions change by user interaction.
It supports the following facets:
- calculate — a XPath expression to be calculated. XPath offers a vast collection of built-in functions to use. Here’s a good reference.
- constraint — a XPath expression evaluating to Boolean that determines the validity of a data node. An optional
alert
attribute may specify an error message when condition becomes invalid. - readonly — a XPath expression evaluating to Boolean to switch nodes between ‘readonly’ and ‘readwrite’ state
- required — a XPath expression evaluating to Boolean to switch nodes between ‘optional’ and ‘required’ state
- relevant — a XPath expression evaluating to Boolean to switch nodes between ‘relevant’ and ‘nonrelevant’ state. Nonrelevant nodes are not send during submission (by default) and controls bound to them are not displaying. With relevance it is easy to build UIs that react to data changes optionally showing/hiding parts of the UI.
It is beyond the scope of this article to fully explain the possiblities of the state engine. For futher information please see our demos and documentation.
This example uses the following bindings which get added to the model:
<fx-bind ref="task" relevant="../show-completed='true' or ./@complete='false'">
<fx-bind ref="./text()" required="true()"></fx-bind>
</fx-bind>
The first bind refers to ‘task’ again and applies a relevance condition that will show/hide tasks depending on the value of the node ‘show-completed’ (which needs to be added to our instance) or the current value of the ‘complete’ attribute being ‘false’:
<fx-instance>
<data>
<task complete="false" due="18.04.2023">write article</task>
<show-completed>false</show-completed>
</data>
</fx-instance>
The second (nested) bind attaches to the text node of the task and makes it required by using the XPathtrue()
function. This little example already shows that conditions can refer to other nodes to evaluate. The built-in dependency engine automatically detects those dependencies and executes them in the right order. Further it will only re-compute expressions that actually need re-calculation by using a calculation-(sub)graph.
Refining the UI with some informal messages
Finally we add some informal messages that count the ‘open’ and ‘completed’ todos and show an encouraging message when the user has completed all tasks.
We add those as ‘template expressions’ to the UI part below our heading. Template expressions are enclosed within curly brackets and evaluate an expression. An example will make that clear:
<div class="info">
You have {count(instance()/task[@complete='true'])} completed tasks
</div>
<div class="info open">
{if(count(instance()/task[@complete='false'])!=0) then "You have " || count(instance()/task[@complete='false']) || " open tasks" else ""}
</div>
<div class="info big">
{if(count(instance()/task[@complete='false'])=0) then "You're all done!" else ""}
</div>
Let’s dissect the first example and i’ll leave the others for you to find out:
{count(instance()/task[@complete='true'])}
count()
and instance()
are XPath functions. While count()
is part of the XPath language itself and — you guess it — counts some nodes, instance()
is defined as a custom function in Fore. It will return the default instance data which is always the first in document order.
The above expression will therefore count all task nodes found in the default instance that are completed. For those not being familiar with XPath the part between the square brackets is called a ‘predicate’ and applies a filter to the nodes that are referred to.
Wrapping up
This concludes our little todo app showing the basic functionalities. However it only scratched the surface of the possibilities Fore provides.
It is also beyond the scope of this article to explain XPath in detail but it is very easy and intuitive to use and a powerful language to search and filter data nodes in either XML or JSON structures.
All this and much more can be done without even using a single line of JavaScript while staying close to the browser platform and giving maximum flexibility in choosing whatever components you prefer for the appeal of your apps. You can even combine it with other frameworks if you wish.
To see this little app in action please head over to our homepage which provides an extensive (and growing) set of examples.
I’m aware that this article leaves some gaps and open questions. However i hope i’ve raised some interest and happy to answer whatever comes up.
Thanks for reading.
Links
- Fore — Homepage, Documentation and Demos
- Source of inspiration — XForms 2.0