Dependencies

htmx

htmx is a client side javascript library for creating interactive web- pages. It is based on sending asynchronous (AJAX) requests to a server to rerender parts of the current html document on certain triggers, and swapping out just that part of the DOM. htmx is configured through attributes on html elements. For example, consider the following html snippet:

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML">
    Click Me!
</button>

with htmx, clicking this button will send a post request to /clicked. It will then replace the element with id parent-div with whatever (partial) html content is returned. It will swap out the whole #parent-div element (indicated by hx-swap="outerHTML". If hx-swap was omitted it would swap out just the inner contents of the #parent-div element. As you can see, htmx doe not perform any html rendering itself, all it does swap out content. There are however, many ways how htmx can swap out content. While going over the details of how htmx works is beyond the scope of this documentation, some use cases and examples are given below.

Include form fields

By default htmx will include the name and value of any form input field as to the request, if the htmx element that triggers the request is inside a <form>. This includes hidden form fields and <select> elements. You can also use the hx-include attribute with a valid css selector to include those input values. How htmx sends the values depends on the type of the request. If the request is a GET request it will send the values as query parameter, otherwise the value will be sent as formdata. It is possible to send tweak which parameters to send by setting the hx-params attribute. It is also possible to send predefined data by setting the hx-vals attribute to a valid json map.

Override default behaviour using response headers

The server can override the htmx client’s default behaviour what to do with a reponse. This can be done using Response headers. You can for example set the HX-Refresh or HX-Redirect to force the client to do a full page refresh or redirect to a different page.

Swap out other content (out-of-band swapping)

Sometimes you need to swap out more than one piece of html. In this case, the response must append the additional html content to the response and set the the hx-swap-oob attribute to one of the elements on the root element of this additional content, as well as the id of the element to swap out.

Usage in Django

Argus uses the django-htmx app to support htmx on the server side. This consists mainly of dealing with htmx request and response headers. Whenever htmx makes a request, htmx sets the HX-Request request header. django-htmx reads this header and sets the htmx attribute on the Request object. In the request views, Argus looks for this attribute to determine whether this was a full request that should return a full response or an htmx request that should return a partial response. Another way to use django-htmx is to use the preconfigured reponse classes that automatically set htmx response headers whenever appropriate, such as the HX-Redirect or HX-Refresh header.

Hyperscript

Most of the user interaction can be achieved using htmx and the pattern of sending AJAX requests and swapping out partial html content. However, sometimes it makes more sense to add a bit of client-side-only interaction. For this we use Hyperscript <https://hyperscript.org/docs/>. Hyperscript is developed by the same developers as htmx and is made to work alongside it. It is a DSL that is designed to be easy to read. It reads almost as pseudo code. For example, consider the following snippet:

<button _="on click toggle .red on me">
Click Me
</button>

This button is given some hyperscript on the _ attribute. When clicked, hypescript will add the red css class on the button. If the button already has the red class, it will remove that class. Adding or removing css classes is the most prevalent use case for hyperscript. The added/removed classes generally associate with some css to hide or display certain elements, or to give them specific styling. Other use cases for Hypscript in Geant Argus are removing certain elements and to triggering or halting events.

Tailwind CSS

Tailwind CSS is a “utility first” css framework, meaning that like other css frameworks, you add classes to html elements and those classes have css definitions associated with it that come from the framework. Most css frameworks acts as a “comoponent” framework so you add a button class to <button> elements and these get nicely styled as a button, or a card element to a <div> to style that element as a card. As mentioned, Tailwind is a utiilty framework and its classes are much more low-level. For example, the class p-8 adds padding: '2rem'; and bg-white adds background-color: white. You may say that this looks a lot like inline css, and you’re right. However, Tailwind is much more powerful than just that. With simple modifiers such as sm: or lg: you can build behaviour responsive to screen size. It is also possible to extend tailwind. For example, you can define a primary color based on a css variable: 'primary': 'rgba(var(--primary))' and then create themes that set this variable. In your code you would use bg-primary or text-primary and those elements are coloured based on the current value of --primary. Tailwind sits in the middle between handcrafting your own css and having a component based framework do all the work.

Having said that, there is value in having a component based framework. This is why we also include Daisy UI. Daisy UI is a component based CSS framework built on top of tailwind, so that you can add a btn class and obtain a nicely styled button, as well as many other components. It also creates default themeable colours such as primary, neutral and accent.

Now, having all these utility and component classes available, would result in a huge css file, even when minified. A static large CSS file is also very difficult to make extensible. It is therefore required to build your own CSS file using a command line tool that Tailwind provides: tailwindcss. You point this tool to your tailwind config and a base css file that contains your custom css definitions like so (this is an example and not the command that we use for Geant Argus, see Compiling Tailwind config and base CSS):

tailwindcss -c tailwind.config.js -i base.css -o final.css

In this example the tailwind.config.js contains your customizations as well as the location of all your source (html template) files. This way tailwindcss can parse your source files and only include the classes that you actually use. The base.css file acts as an entry point for tailwindcss and you can include any custom css you want. The generated css file will be stored as final.css. You can also build a minified version of the css file by supplying the -m flag

Installation

tailwindcss can be automatically downloaded for your platform by running:

make get-tailwind

This is also done as part of the make initialize-repo command. Alternatively, you can follow the installation instructions in Argus documentation: Install and build Tailwind CSS and daisyUI. It is important to download the correct version (as specified in the tailwindcss/VERSION file).

Compiling Tailwind config and base CSS

As described above, TailwindCSS can generate the css file from a base css file and a tailwind.config.js. However, in the case of (Geant) Argus, there is no static base css file or tailwind.config.js. This is due to the fact that the sources of the two Argus projects need to be combined. The solution to this can be found in the tailwind_config Django management command supplied by Argus. This command generates the base css file and the tailwind config. For generating the base css file, the command looks in the AppConfig of every app listed in the INSTALLED_APPS setting for a tailwind_css_files() method and creates includes in the base.css for every file listed by that method. For Geant Argus, the css snippets are located in the geant_argus/geant_argus/tailwindcss/ directory. Snippets are ordered by name, so by giving them a numerical value, it is possible to give certain snippets a higher priority than others. See also custom-css-snippets.

The tailwind.config.js file is generated from a template. The Django template engine is used for this. The template location is tailwind/tailwind.config.js and the geant_argus app has this template overridden. Aside from the static content in this template, there is an important interpolated variable {{ projectpaths }}. This variable is injected with the template directory of every app in the INSTALLED_APPS setting. This way all templates are evaluated by tailwindcss to look for tailwind classes, be they from argus.htmx, geant_argus or any other app that uses tailwind.

Geant Argus has tailwind_config configured to output its results in the tailwindcss/ directory as tailwind.config.json and geant.base.css for the tailwind config and the base css respectively. The content of the files is dependent on the virtual environment in which they were generated, so these files cannot be checked in source control. The same holds true for the generated base css file. For Geant Argus, the only file that is checked in, is the minified css file located in src/geant_argus/geant_argus/static/geant.min.css, which is generated by running:

make css

Custom CSS Snippets

Custom CSS snippets are located in geant_argus/geant_argus/tailwindcss. These contains the css definitions required for certain pieces of functionality. Some of these pieces are resusable, while others are only used for a specific part of Geant Argus.

While creating css snippets, it is possible to add tailwind directives. For example, you can use @apply to have tailwind apply the contents of a tailwind class to a css snippet:

.my-custom-class {
  @apply border-base-content/50 bg-base-100;
}

Upgrading dependencies

See Argus Documentation: Upgrading Dependencies