Creating dynamic user interfaces with htmx ========================================== Starting version 2.11, CKAN is shipped with `htmx `_. *"htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext."* -- `htmx.org `_ While not all CKAN templates have been updated to use ``htmx``, you can use it in your own extensions to build modern user interfaces. htmx will be the core component in the implementation of the new CKAN UI, so you should expect more of it in future versions. -------- Overview -------- ``htmx`` is a library that allows you to use HTML attributes to make AJAX requests and update the DOM. It is a great alternative to Javascript frameworks like React or Vue, as it allows you to build dynamic user interfaces with regular flask views and Jinja2 templates, allowing templates to be overridden by themes and other extensions. The library is very simple to use. You just need to add the ``hx-*`` attributes to your HTML elements to make them dynamic. For example, to make a link that makes a POST request to the ``/dataset/follow/`` endpoint and replaces the HTML element with id ``package-info`` with all the HTML returned by the endpoint, you can write: .. code-block:: jinja Follow The example can be read as: "When the user clicks on this link, make a POST request to the ``/dataset/follow/`` endpoint and replace the HTML element with id ``package-info`` with all the HTML returned by the endpoint". Notice how we are using the ``hx-post`` and ``hx-target`` attributes to define the behaviour of the link. For a full list of the HTML attributes and their usage, check the `htmx documentation `_. ----------------------------------- Implementing new features with htmx ----------------------------------- ``htmx`` give us the flexibility to implement new dynamic features in CKAN by implementing new endpoints that returns the partial HTML that we want to insert into the page. The **Follow** / **Unfollow** logic is a great example of this and we will explain the thought process behind it in this section. In UI terms, the **Follow** / **Unfollow** logic is just a div containing a button that allows the user to follow/unfollow a dataset plus a counter that shows the number of followers. The div is displayed in the dataset page. This is a small interactive action and we do not want a typical full refresh of the page. It doesn't make any sense to reload the whole page just to update the number of followers and the button. This is a perfect use case for ``htmx``. What we need to achieve this behaviour is: 1. A HTML structure that encapsulates the follow/unfollow UI in a single HTML element (so it can be replaced). 2. A way to trigger a call to the endpoint when the user clicks on the button and replace the element with the new content. 3. A new endpoint that covers the backed logic and returns just enough HTML to replace the HTML element. 1. HTML structure The HTML structure is very simple: an element that contains the button and the counter. To respect the current CKAN UX we update the ``package/snippets/info.html`` snippet. We need to make sure that the ``section`` HTML element we want to replace has an id so we add it: ``id="package-info"``. .. code-block:: jinja {% block package_info %} {% if pkg %}
{% endif %} {% endblock %} 2. Triggering a call to the endpoint We need to trigger a call to the endpoint when the user clicks on the button. We can do this by adding the ``hx-post`` attribute to the button. The ``hx-post`` attribute defines the URL that will be called when the user clicks on the button. In our case, we want to call the ``/dataset/follow/`` endpoint, so we can use the ``h.url_for`` helper to generate the URL. .. code-block:: jinja Follow In addition to the ``hx-post`` attribute, we also need to define the ``hx-target`` attribute. The ``hx-target`` attribute defines the HTML element that will be replaced with the HTML returned by the endpoint. In our case, we want to replace the ``package-info`` element, so we can use the ``#package-info`` selector. 3. The endpoint The last step is to implement the endpoint that will be called when the user clicks on the button. In our case, we want to call the ``/dataset/follow/`` endpoint. This endpoint is already implemented in CKAN. We need to make sure that, under this new context, it should return only the partial HTML that we want to insert into the page instead of rendering the whole dataset page again. We achieve that by making it sure that we return the snippet that contains the HTML that we want to display, in our case ``package/snippets/info.html``. View: .. literalinclude:: ../../ckan/views/dataset.py :pyobject: follow Note that this endpoint is reusing the ``package/snippets/info.html`` that is also being called in ``package/read_base.html`` when calling ``/dataset/``. This shows how modular and reusable the CKAN templates are with ``htmx``. -------------------------------------------- 2. Accessing to HTMX request headers in CKAN -------------------------------------------- CKAN adds a new property to the CKANRequest class called ``htmx`` that you can use to access the htmx request headers. For example:: from ckan.common import request if request.htmx: # do something Calling ``request.htmx`` will return a HtmxDetails object that contains attributes for each one of the ``htmx`` attributes. For example, if you want to access the ``hx-target`` attribute, you can write:: from ckan.common import request if request.htmx: target = request.htmx.target .. literalinclude:: ../../ckan/common.py :pyobject: HtmxDetails ---------------- 3. htmx examples ---------------- Check the `htmx examples `_ for an overview of patterns that you can use to implement rich UX features.