$ \def\Vec#1{\mathbf{#1}} \def\vt#1{\Vec{v}_{#1}(t)} \def\v#1{\Vec{v}_{#1}} \def\vx#1{\Vec{x}_{#1}} \def\av{\bar{\Vec{v}}} \def\vdel{\Vec{\Delta}} $

Harald Kirsch

about this blog
2023-12-18

Developing Simple Web Applications

Recently I got interested in what it takes to implement simple web applications without the waggon-loads of framework software typically used these days. The following describes an approach which works surprisingly well.

How?

Use Typescript

Coming from a Java background, I feel much better when the compiler and, even more, the IDE can help to avoid stupid mistakes and moreoever tell me a lot about intent and functionality of variables and functions by showing their types.

Alas for what follows, using Typescript is not crucial. Plain Javascript can be used in the same way.

Implement Components

The following list summarizes the concept. Discussion and rationale is provided afterwards.
  1. Create components as Java-/Typescript classes.
  2. Let such a component manage a piece of DOM, typically with its root element and a few descendants.
  3. The component may be a container where some of the descendants are provided by other components.
  4. Typically a component has a getElement(): HTML...Element method to provide the root element of the DOM part it manages.
  5. A component should have a clean API for other code to manipulate it. Direct event bindings should be scarcely used. For example a component providing visual list manipulation: to select a list element, the component could bind the list elements to an onclick event, but by providing a selectElement() method it allows other ways of selection than by clicking.
  6. Make use of components which have no DOM elements to manipulate of their own, but are rather controllers (in the model-view-controller abstraction) which take care of event handling and then call APIs of DOM-care-takers in response to UI events.
  7. Use document.createElement() to create the DOM elements a component wants to directly manipulate. Do not use template engines.
  8. Let a component add the minimum of CSS rules that are needed to let the managed piece of DOM work as intented. It does not need to look nice from the component-added CSS alone.
  9. These component-added CSS rules shall be inserted at the front of the the <head> element. This allows style sheet links with rules that override the rules added by the components.
  10. The component Javascript class should add a sufficiently "unique" CSS class to the root of the DOM piece it manages. This could be, for example, the name of the Javascript class.
  11. Whether the components are implemented as Web Components or rather favor composition over inheritance is a matter of taste. But good taste suggests to avoid Web Components as they are rather complicated while not adding a lot, if any, benefit.
  12. Don't let a component tweak HTML elements it does not own. If there are things to adapt, to change, to tweak, to configure, the owner component should have a respective API.

Where is the framework?

Right there. The whole idea is to get away without using tons of framework code. Just follow the simple guidelines outlined above.

It would obviously be nice if there were already components written in the style described above with well defined behaviour, independent of any framework, very scarce dependencies on other components, all interoperable because all they provide are a Javascript class each with a simple API to operate them.

Why?

At work we developed (web-)app software with Typescript, Angular, Cordova, Ionic and whatnot. It shall make things easier and development quicker and more reliable.

It works for the strong typing which Typescript brings in.

Angular and Ionic, on the other hand, were often the enemy we had to fight to get what we and/or the customer wanted. Both shall reduce complexity, but the price is (a) more complexity plus (b) magic.

If the magic just works, great. But too many times the magic didn't work as needed and we had to unravel the inner workings of magic to figure out how to nudge it to do what was required.

So I started to wonder whether the cost matched the value. Obviously this depends on so many factors that the outcome is different for everyone. Plus: no sane head of development would say these days:

"no frameworks, we use plain Typescript and the odd small library as long as it does not dictate our code structure, provides no magic and has a sane API to call."

Because there are always problems. With "industry standard" frameworks, the head of development can report: "this is normal, its complex software, there is a reported bug and BigCompany wants to fix it in release 1.2.3."

With custom made components, there is nobody to blame outside the company if shit happens. But for my private projects, I can do what I want, so I started to follow the guidelines above.

Works for me.

More Detailed "How?"

Historically a web page was just that, a page of HTML. Then came some active content, like Javascript, then came frameworks which allowed to pepper HTML templates heavily with code. Lets now finish this shift: don't write HTML at all, write pure Java-/Typescript.

These components need not be Web Components which extend from HTMLElement or its descendants. It works well if a visual component is represented by a Javascript class which creates a small part of the DOM tree and provides the root of the tree for other components to retrieve and add to their own part of the tree. As an example lets assume we have a ModalMenu component and a component providing the content to be shown, lets say FileMenu. The code to use these might look like:

const modal = new ModalMenu(...);
const fileMenu = new FileMenu(...);
modal.show(fileMenu.getElement());

The idea is that ModalMenu.show() accepts an HTMLElement to inject into some wrapper, maybe a HTMLDialogElement and then shows in on screen.

To create the HTML elements, the component classes may use whatever works best. But consider to just use document.createElement() calls, which has at least the following benefits over templating (see more about this below):

Less typing hassles in Typescript
By using
const button = documentCreateElement('button');
the button immediately has the right type, HTMLButtonElement. When using a template HTML structure a document.querySelector('#gooButton') returns Element|null which is quite annoying in Typescript. But even when using Javascript, the problem may arise that the element ID in the template is changed to #fooButton which requires the insight to know there is a query selector used to retrieve it and that code must be changed too. And eventually, it won't.
Fewer DOM elements
We're all lazy. If each element of the DOM needs a separate createElement we somewhat try to avoid mushrooming of <div> wrappers :-)
Cleaner code
For the same lazyness reason we're pushed to extract repeated element creation into short, understandable functions while a template rather suggests to use copy&paste&screwup coding for similar repeated elements.

CSS how?

Let the components add the CSS which is absolutely necessary for their function to front of the <head> element. Think of a minimum size for an element to be tappable with fat fingers.

Non-functional styling for the good look should go into a normal style sheet. As long as it is loaded after the CSS injected into the head, the style sheet can still override the injected rules.

Each component should add a "unique" CSS class name to the root element(s) it manages. Here everyone cries "foul" and that conflicts are to be expected. Interestingly, though, fully qualified class names in the Java world manage for decades to stay different from each other with maybe very few exceptions. So if you start to develop a library along the advice suggested here, you may start to mimic the scheme and use class names like de_mydomain_MenuButton.

Two types of components

Similar to not having to much styling to generate a specific look, a component should also be scarce in which user interaction it provides via event bindings. If the user shall interact with some elements managed by the component, first add an API to the component under the assumption that other components want to initiate the interaction. Whether the component provides default event bindings or not can be decided afterwards.

As a result you will note that you naturally get two kinds of components: those with DOM elements and those which deal with event handling. It may well be OK to not separate these for small components. A button to initiate a foobleblarg is just that: a button with an action to take. No need to separate the action out.

But consider component MarliworgList maintaining and displaying a list of marliworgs. Deleting an element from the list should be first and formost an API of the class MarliworgList. As should be the selection of list elements. Which user interaction initiates the selection and deletion of elements is a completely different story. Whether some button, whether a mouse click, whether an incremental search box: there may be many ways to select and delete elements and all of these

Avoid Templates

Do not use templates.

Just use document.createElement() calls wrapped in suitably designed (code wise) Type-/Javascript classes. Keep the DOM elements you need to manipulate in class fields, stick the others right into the DOM snippet the Javascript class is managing and forget about them. Make use of the full power of Type-/Javascript for clean code when creating DOM elements instead of weird template syntax.

Show me the code

See arrangimage as an application where I used the approach described above. Ignore that I use a class Template, it is a relic of an initial mistake which needs to be removed.