DOM
Last updated
Last updated
While a browser receives an HTML source file, it starts to parse it piece by piece to construct an object representation of the document called the DOM (Document Object Model) tree. Simultaneously, the DOM is being converted into a render tree, which represents what eventually is being painted. The document can start being rendered and painted in the browser before it is fully loaded. Unless it is blocked by CSS or JavaScript.
When the parser comes across CSS code, the rendering process is being blocked until the CSS is fully parsed. Similar as before, the browser now constructs a CSSOM (CSS Object Model) tree which associates the styles to each node. After parsing the CSS, a combination of the DOM and CSSOM are being used for continuing creating the render tree.
Even though the rendering process can be blocked by CSS, the DOM is still being parsed. Unless it comes across JavaScript. When the parser reaches a <script>
tag, the parsing stops, and the script is being executed. This is the reason that a JavaScript file needs to be placed after the appearance of a referenced element in the script.
Previously, it has been best practice to always include the <script>
tags at the end of the document to make sure that all the elements are available and to not block the rendering and therefore painting process of the browser.
Nowadays, there are other options to avoid that JavaScript is being parser blocking. For example:
This image summarizes the many different approaches to load JavaScript code. More information can be found in the HTML5 specification
Basically, the DOM is an interface for HTML (and XML) documents which represents the page. It is dynamic, and the browsers provide an API to read and change the content, structure, and style of the document via JavaScript. This allows for changing parts of the website without the need of a refresh and therefore a repaint of the whole page.
JavaScript has access to a global object. In a browser, window
is the global object and represents the window/tab of the browser in which the script is running. One of its property is window.document
which serves as an entry point to the parsed DOM tree.
Because
window
is the global object, there is no need to reference its properties (e.g.document
) viawindow
. The property name can be used directly as the script will figure out the global object at runtime.
The document
interface can now be used to manipulate the DOM.
As an example let's create an HTML file:
The browser will parse this file to the following DOM:
This DOM can now be manipulated via the document
interface in JavaScript:
There are three different ways to access the content of a DOM element.
element.textContent
: represents the text content of a node as it is in the DOM. Therefore, it doesn't include the HTML tags but keeps the content of non-visible elements. For example, the content of <script>
or <style>
tags.
element.innerText
: similar to textContent
but uses CSS knowledge and only returns visible content. This has the disadvantages that reading a value with innerText
triggers a reflow to ensure up-to-date computed styles. This can be computationally expensive and should be avoided when possible.
element.innerHTML
: represents the HTML source of the element. It should only be used when the intention is to work with HTML markup. Misusing it for text is not optimal for performance and it is vulnerable to XSS attacks.
The element.innerHTML
method allows to build up a nested structure using the HTML markup language relatively easy.
This gets slightly more challenging for appending or modifying nodes to an already existing DOM as all its child elements are being re-parsed and recreated completely. This means that saved references to nodes are no longer pointing to the supposed elements.
Re-parsing the whole structure of the element is also bad for the performance.
There is a solution for this called element.insertAdjacentHTML
which does not re-parse all its child elements.
This combination between innerHTML
and references is not very readable. Plus, when dealing with registering event listeners as well, it can get complicated. If a reference to a created element should exist at a later time, it is advisable to use the document.createElement
method.
Puerro provides an abstraction to make it more convenient to create elements.
After the HTML has been parsed, rendered and painted, the browser is usually waiting for user interactions. For this to work the browser uses an event-driven programming model to notify the JavaScript code about what's happening on the page.
There are a lot of different events. For example, when the DOM is finished with loading, clicking elements, typing on the keyboard, scrolling and many more.
In order react to an event, Event Handlers
are used. Event handlers are functions which are being called from the browser when an event occurs.
When an event is fired, the first parameter an handler receives is an Event
object which contains useful information and methods. The most used are:
target
: A reference to the target to which the event was originally dispatched.
type
: The name of the event.
stopPropagation()
: Stops the propagation of events further along in the DOM.
preventDefault()
: Cancels the event.
To register an event there are three possibilities.
The most legacy but direct way is to register event handlers directly in the HTML markup.
But especially for larger projects this is considered a bad practice as it is hard to read and maintain because it doesn't separate the view from the actions. It also requires that the functions are exposed globally, which is pollution to the global namespace.
A better way is to register the event handlers in the JavaScript code. It is similar to the inline event handlers, but it respects the separation of concerns and the scope is more controllable.
A drawback with this approach is that it is only possible to assign one listener to each event.
addEventListener()
The most modern approach is to use the element.addEventListener()
function. It allows to register as many event handlers as needed.
With this approach it is also possible to remove listeners with the element.removeEventListener()
function. Another advantage is the ability to choose between event bubbling and capturing.
When nodes are nested, a user interaction can trigger multiple events. Two different models exist to handle this:
Bubbling (default): The event propagates from the clicked item up to all its parents, starting from the nearest one.
Capturing: The outer event handlers are fired before the more specific handler.
With the following example, the events bubble. Meaning they are propagated upwards.
To make sure that the DIV
event listeners triggers first, the methods useCapture
parameter needs to be true. All the event handlers with useCapture
enabled run first (top down), afterwards the bubbling handlers (bottom up).
To complete stop the propagation, the handler can call the stopPropagation()
method on the event object.
Building forms is a widely used pattern for web applications. HTML provides a <form>
tag, which allows to group interactive controls together for submitting data to a server. When a form is being submitted, an HTTP Request with the specified method is sent to the specified resource. With this traditional approach, the page always will be refreshed and new rendered based on the response.
This approach is acceptable if we want to display a completely different view after submitting the form. However, for modern web application this is usually not desirable. Instead, it is better to use an Ajax request in the background without affecting the page and to manipulate the DOM based on the response.
Nevertheless, using the <form>
tag has many advantages and should still be used for grouping interactive controls:
It improves the logical structure of the HTML.
It increases the accessibility for screen readers.
It provides a better user experience on mobile phones.
It has the ability to access its elements conveniently.
It has the ability to easily reset its elements.
In order to use the <form>
tag without it being submitted, an event handler has to be registered for the form's submit event. In this handler the method event.preventDefault()
can be executed to prevent the form from submitting.
A button can have 3 different types:
submit
,reset
andbutton
. The default type issubmit
which will attempt to submit form data when clicked. When the intention is to use a button without a default behaviour, explicitly specifytype="button"
.A
<form>
can also be submitted by pressing enter or via JavaScript. Therefore, using a<button type="button">
with a click event handler won't be enough. Plus receiving the target form in the event is a huge benefit.
When event handler functions receive events, they can in turn manipulate the DOM.
For a handler function to manipulate the DOM, references to the elements which have to be manipulated are needed. Those references can either be already available in the surrounding scope or can be created in the function itself.
With the help of eta reduction, the parameter can be removed when there is only one argument or when using curried functions.
x => foo(x)
can be shortened tofoo
This gets problematic when the intention is to test this unit, since the DOM might not be available. Furthermore, this approach can quickly become difficult to maintain.
A better approach for a simplified testability is to receive the nodes which are being manipulated as a parameter.
The element which fires the event does not have to be passed as an argument because it is available through event.target
.
When new elements are being created, it is a good practice to return them for a more convenient testing.
To use the handler functions, they simply have to be registered with the needed references as arguments.
To use the handler functions for testing, the needed elements need to be created.
The above example can be found in the Puerro Examples.
This style of programming with direct DOM manipulations within the event handler functions is easy and intuitive. It can be used for various tasks:
Dynamic informational websites.
Reactive content without side effects.
Experimenting/Prototyping.
Simple Web application without many changing parts.
Server-Side rendered web applications.
Zero dependencies.
Little code.
Easy to understand.
Fast.
This approach is getting harder to maintain when either frontend state is being introduced or there are many changing elements. This is especially true when the application starts growing.
When an event triggers a lot of changes, a reference to each dependent element needs to be managed. If multiple elements need to be updated constantly and in short time frames, it can start to become expensive to constantly query and update the DOM. Furthermore, when updates depend on data stored in the DOM, the decapsulation between view and model is not given.
For large application there probably will be redundant code and all DOM related accesses are scattered through the code.
Difficult to scale and maintain.
Hard to structure/organize.
Redundant code.
Scattered DOM manipulations.
Very specific.
State lives in the view.