TODO app is a good basic application that demonstrates a lot of the key concepts in web application development. This particular example focuses on the use of forms APIs to represent the entire application sate entirely in the DOM.
No frameworks were invented in the making of this app.
The example file, index.html
can be opened in your browser normally without
requiring any development server. You can also use the HTML preview feature in
your IDE or editor if one is available.
Test are executed in the test.html
file. This file is opened in the
browser the same way you would open the index.html
file. Tests results are
shown in the console. The actual code of the tests is found in the tests.client.js
file. The tests are executed with the help of a custom-made test runner
found in vanila-tester.js
.
You will find the live demo at foxbunny.github.io/vanilla-todo. The tests can also be run live by opening this page (results are shown in the developer console).
Here are some of the concepts that this applications demonstrates.
The VP architecture is a variant of the MVP architecture that uses DOM state as application state. This is not a universal architecture and does not work (well) in all cases. This application was specifically chosen to demonstrate the architecture.
The code is organized into layers. These layers are as follows:
- Global constants
- Utility
- Data (storage)
- View
- Presenter
- Event listeners
The first two layers, "Global constants" and "Utility" are universal and can be used by any other layer.
The "Data" and "View" layers independently provide building blocks for the "Presenter" layer. This means that the view layer is not allowed to access the data layer, and vice versa. The relationship between the concepts in these two layers are established in the "Presenter" layer. The presenters can call each other in addition to calling data and view code. Some presenters may be simple aliases to view functions. Rather than invoking the view functions directly in event listeners, we create aliases so that we can have a better overview of all available presenters (and, by extension, all possible user actions).
The "Event listeners" layer is boilerplate that sets the application in motion. Therefore, in the context of this app, the architecture is also event-driven. The event listener code performs some basic transformations and filtering over the events before invoking the presenters. They are not allowed to access the lower layers.
State is stored in and represented by the DOM state. We do not store state any JavaScript variables but as properties/attributes on DOM nodes. This way, we do not need to perform additional work to synchronize the UI and internal application sate, as those are on and the same. The DOM becomes the single source of truth for the application.
The JavaScript code associated with the page is named by suffixing
.{environment}.js
to the base name of the file. For example, client-side code
for index.html
is named index.client.js
. Server-side code (there's
none in this project) would be named index.server.js
.
The CSS code associated with the page is named by suffixing
.{media target}.css
to the base name of the file. For example, screen
CSS for index.html
is named index.screen.css
suffix, while print CSS would
be named index.print.css
.
The naming of the objects within JavaScript code helps identify the layer to which the object belongs.
Global constants are named using all-upper-case snake-case names. For instance,
AUTO_SAVE_DELAY
. Global constants are used to label magic values (values
that you come up with by trial and error and are not obvious even in the
context in which they are used).
Utility methods have simple names that have no specific suffixes or prefixes. These are general-purpose functions that are not application-specific. If you can copy them to another project without modification to the function or the target project, they are a utility function.
Data methods have a data
prefix. For instance, dataStoreTasks()
. These
methods use the underlying storage facilities such as the browser storage
APIs or REST services.
View category has two naming conventions. Objects that represent DOM nodes
start with the dollar character, $
. A single $
character is a special
case used for anonymous DOM node references (e.g., we don't care what their
role is, or their role can be easily inferred from the context). The name
following the $
character should match the id
or class
attribute of
the element in the HTML. Collections of elements are named using two $
characters (e.g., let $$items = document.querySelectorAll('.item')
). The
second convention is that functions that operate on the view are all
prefixed with view
.
Presenter methods have a present
prefix. These are methods that invoke
functions from the lower layers
Every <form>
and <fieldset>
element in the DOM will have an elements
property that implements the
HTMLFormControlsCollection
interface.
This is used throughout the app to quickly access the named inputs, as opposed
to using Element.queryString()
and Element.queryStringAll()
methods.
Each fieldset represents a task and can be dragged to change the task order. We use the HTML drag and drop API for this.
To render the fieldset contents we use the innerHTML
property. This is
widely considered an unsafe practice, but it is actually only unsafe in
cases where user input is directly inserted into the markup. We use an
escapeHTML
function to clean the input before inserting into the HTML.
Mobile devices do not support the HTML drag & drop API. We can provide a
fallback based on touch gestures without feature detection and similar gimmicks.
By calling Event.preventDefault()
in the touchstart
event listener, we
prevent some of the events that are triggered later, including the dragstart
.
Therefore we are able to disable the standard drag & drop handling as soon as
user starts using touch events for the purpose. As long as both methods provide
an equivalent (and preferably identical) functionality, the end user is none
the wiser.
This application uses automated UI testing (end-to-end testing) only. It does not use unit tests at all. This is because instrumenting the application code to be testable (i.e. writing it in a way that tests can access the code) is relatively expensive. Some unit testing fans say "test only the public interfaces". Well, for this application, the UI is the public interface.
The automated UI tests use a very high-level description of the test to avoid hard-coding anything related to the application implementation to avoid having to expose anything internal just for the testing purposes.
This code is licensed under the terms of the MIT license. See LICENSE.txt for the boring legal verbiage.