Skip to content

A server-side rendering (SSR) library for Clojure web applications that facilitates defining, composing, organising, and unit testing user interface components.

Notifications You must be signed in to change notification settings

TenDaysOfClojure/hiccup-server-components

Repository files navigation

Clojars Project

Hiccup Server Components

A server-side rendering (SSR) library for Clojure web applications that facilitates defining, composing, organising, and unit testing user interface components, as well as generating the associated HTML. Based on the Hiccup library

The goal of this library is to facilitate rapid web application development and increase maintainability of user interface code by providing conventions and tools to model user interfaces.

Components represent modular, abstract pieces of the user interface which are composed into a larger, complex applications with a high degree of abstraction.

Can be used seamlessly with HTTP routing libraries such as Reitit, Compojure, and directly with various Clojure ring implementations for generating HTML responses. Can also be used to generate static HTML files.

Table of contents

Installation

Clojars Project

Add the following dependancy to your Clojure projects to get the latest version:

Clojure CLI/deps.edn:

net.clojars.t_d_c/hiccup-server-components {:mvn/version "0.20.0"}

Leiningen/Boot:

[net.clojars.t_d_c/hiccup-server-components "0.20.0"]

back to top

Introduction

Using Hiccup to represent HTML

The Hiccup library is used for representing HTML in Clojure. It uses vectors to represent elements, and maps to represent an element's attributes.

The below Hiccup data represents the HTML for a typical login webpage:

;; HTML document
[:html
 [:head
  [:meta {:charset "UTF-8"}]
  [:meta {:content "width=device-width, initial-scale=1.0", :name "viewport"}]

  ;; Include CSS and javascript assets
  [:link {:rel "stylesheet", :href "/css/main.css"}]
  [:script {:src "/js/app-bundle.js"}]
  [:title "Login now"]]
 [:body

  ;; HTML form which posts to /login
  [:form.input-form {:action "/login" :method "POST"}
   [:h1.form-title "Login now"]
   [:p.form-intro-text
    "Enter your email address and password to access your personal dashboard"]

   ;; Email input field with label
   [:div.form-field
    [:label.form-label {:for "email-address"} "Email address"]
    [:input.text-input
     {:id "email-address",
      :name "email-address",
      :autofocus true,
      :type "text"}]]

   ;; Password field with label
   [:div.form-field
    [:label.form-label {:for "password"} "Password"]
    [:input.text-input
     {:id "password", :name "password", :autofocus false, :type "password"}]]

   ;; Form action buttons included primary button and cancel button
   [:div.form-action-buttons
    [:button.primary-submit-button {:type "submit"} "Login now"]

    ;; Cancel button navigates back to '/home' using javascript
    [:button.cancel-button
     {:type "button", :onclick "javascript:window.location='/home'"}
     "Cancel"]]]]]

Further abstraction with Hiccup server components

Hiccup server components allows developers to represent web pages and user interface components with a high level of abstraction, leveraging the Hiccup library.

Using Hiccup server components We can represent the typical login webpage as follows:

[:ux.layouts/html-doc {:title "Login now"}

  [:ux.forms/form
   {:title "Login now"
    :intro-text "Enter your email address and password to access your personal dashboard"
    :action "/login"}

   [:ux.forms/text-input
    {:label-text "Email address"
     :element-name "email-address"
     :auto-focus? true}]

   [:ux.forms/text-input
    {:label-text "Password"
     :element-name "password"
     :input-type "password"}]

   [:ux.forms/action-buttons
    [:ux.forms/primary-submit-button "Login now"]
    [:ux.forms/cancel-button "Back" "/home"]]]]

Which would produce the following web page:

With the following HTML:

<html>
  <head>
      <meta charset="UTF-8"/>
      <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
      <link href="/css/main.css" rel="stylesheet"/>
      <script src="/js/app-bundle.js"></script>
      <title>Login now</title>
  </head>
  <body>
      <form action="/login" method="POST" class="input-form">
          <h1 class="form-title">Login now</h1>
          <p class="form-intro-text">
            Enter your email address and password to access your personal
            dashboard.
          </p>
          <div class="form-field">
              <label class="form-label" for="email-address">Email address</label>
              <input autofocus="autofocus" class="text-input"
                     id="email-address"
                     name="email-address" type="text"/>
          </div>
          <div class="form-field">
              <label class="form-label" for="password">Password</label>
              <input class="text-input" id="password" name="password"
                  type="password"/>
          </div>
          <div class="form-action-buttons">
              <button class="primary-submit-button" type="submit">Login now</button>
              <button class="cancel-button"
                      onclick="javascript:window.location=&apos;/home&apos;"
                      type="button">Cancel</button>
          </div>
      </form>
  </body>
</html>

back to top

Getting started: Building an example login page

The goal of this introductory example is to demonstrate 95% of the features provided by this library. A basic understanding of the Hiccup library is a prerequisite.

The following example shows how to build a simple login page.

Step 1: Requiring the namespace

The public API for Hiccup Server Components is provided by the ten-d-c.hiccup-server-components.core namespace.

The first step, after including the library in your project, is to require the namespace:

(ns introductory-example.core
  (:require [ten-d-c.hiccup-server-components.core :as hc]))

Step 2: Register your first component

The first component we'll register is an HTML document which involves the following steps:

  • Using the reg-component function to register a new component using a qualified keyword.

  • Include doc and examples metadata (in the form of a clojure map) when registering the component to document the component.

  • Implement the components responsibilities using a pure function.

(hc/reg-component
 ;; Keyword to uniquely identify the component:
 :ux.layouts/html-doc

 ;; Include meta data in the form of a map including `doc` and `examples` keys:
 {:doc
  "The main HTML document including a HEAD (with required CSS and Javascript
    included) and BODY section.

    This component is the basis for all top-level pages in the application.

    The first parameter (a map) represents the component's options, followed by
    a variable list of `child-elements` in the form of Hiccup data that will be
    placed in the BODY.

    Component options:

    - `title`: The title of the HTML document (will populate the title tag
       in the HEAD of the document)"

  :examples {"With single child element"
             [:ux.layouts/html-doc
              {:title "One child element"}
              [:div "Hello world"]]

             "With multiple child elements"
             [:ux.layouts/html-doc
              {:title "Multiple child element"}
              [:h1 "Hello world"]
              [:p "This is a test"]
              [:a {:href "/search"} "Try searching for more results"]]}}

 ;; Pure function implementing the responsibilities of the component:
 (fn [{:keys [title] :as options} & child-elements]
   [:html
    [:head
     [:meta {:charset "UTF-8"}]
     [:meta {:content "width=device-width, initial-scale=1.0"
             :name "viewport"}]
     ;; Include application CSS and any javascript
     [:link {:rel "stylesheet" :href "/css/main.css"}]
     [:script {:src "/js/app-bundle.js"}]
     ;; Include the title of the document
     [:title title]]
    ;; Variable child elements included in body element
    [:body child-elements]]))

The ->html function can be used to convert Hiccup data, which references the component using its qualified keyword, to HTML:

(hc/->html
 [:ux.layouts/html-doc
  {:title "Multiple child element"}
  [:h1 "Hello world"]
  [:p "This is a test"]
  [:a {:href "/search"} "Try searching for more results"]])

The following HTML is returned:

<html>
<head>
    <meta charset="UTF-8"/>
    <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
    <link href="/css/main.css" rel="stylesheet"/>
    <script src="/js/app-bundle.js"></script>
    <title>Multiple child element</title>
</head>
<body>
    <h1>Hello world</h1>
    <p>This is a test</p>
    <a href="/search">Try searching for more results</a>
</body>
</html>

Step 3: Register components related to HTML forms

The next step is to register components that will be responsible for the HTML form which involves the following steps:

  • Using the reg-component function to register a new component using a qualified keyword.

  • Including doc and example metadata (in the form of a clojure map) when registering the component to document the component.

  • Using a pure function, implement the components responsibilities that include:

    • Building a HTML form with an action url

    • Includes a standardised title and intro-text which enrich the all forms with text prompts.

    • Allows for a variable amount of form elements which will be child elements of the form.

(hc/reg-component
 ;; Keyword to uniquely identify the component:
 :ux.forms/form

 ;; Include meta data:
 {:doc
  "Builds an HTML form which will perform an HTTP POST to the given `action` URL
   and provides a standardised `title`, optional `intro-text` which will be
   displayed before form elements.

   Along with the above options supports a variable amount of form elements.

  Component options:

  - `:action`: Specifies the URL where to send the form-data when a form is
               submitted. e.g. \"/auth/login\"

  - `:title`: (Required) The title text to be displayed at the top of the form
              e.g. \"Login now\"

  - `intro-text`: (Optional) Introduction text providing a summary for what the
                  form is capturing as well as what happens on submission
                  e.g. \"Enter your details to login and view your dashboard\"."

  :example [:ux.forms/form
            {:action "/auth/login"             
             :title "Login now"
             :intro-text "Provide your details to login and view your personal
                          dashboard"}
            [:div
             [:label {:for "email-address"} "Email address"]
             [:input
              {:name "email-address" :id "email-address" :type "email"}]]]}

 ;; Pure function implementing the responsibilities of the component:              
 (fn [{:keys [action title intro-text]} & form-elements]
   [:form.input-form {:action action :method "POST"}
    (when title [:h1.form-title title])
    (when intro-text [:p.form-intro-text intro-text])
    form-elements]))

The ->html function can be used to convert Hiccup data, which references the component using its qualified keyword, to HTML:

(hc/->html
 [:ux.forms/form
  {:action "/login"
   :title "Login now"
   :intro-text "Provide your details to login and view your personal
                dashboard"}
  [:div
   [:label {:for "email-address"} "Email address"]
   [:input
    {:name "email-address" :id "email-address" :type "email"}]]])

The following HTML is returned:

<form action="/login" method="POST" class="input-form">
    <h1 class="form-title">Login now</h1>
    <p class="form-intro-text">Provide your details to login and view your personal
                              dashboard</p>
    <div>
        <label for="email-address">Email address</label>
        <input id="email-address" name="email-address" type="email"/>
    </div>
</form>

Step 4: Register components for form input

The next step is to register components that will be responsible for text input which, as with previous examples, involves using the reg-component function, including doc and examples metadata, and using a pure function, implement the components responsibilities that include:

(hc/reg-component
 ;; Keyword to uniquely identify the component:
 :ux.forms/text-input

 ;; Include meta data:
 {:doc
  "A single-line text field of the given `input-type` that includes a label.

   Component options:

   - `:element-name`: Specifies the name of an input element which will be used
                       as the name of the field when posting a FORM. Also used
                       as the elements id.

   - `label-text`: The text used for the associated label element.

   - `input-type`: The type of form element e.g \"text\", \"email\",
                   \"password\" etc.

   - `auto-focus?`: Boolean value indicating that an element should be focused
                    on page load."

  :examples {"Email field" [:ux.forms/text-input
                            {:auto-focus? true
                             :label-text "Email address"
                             :element-name "email-address"
                             :input-type "email"}]

             "Password field" [:ux.forms/text-input
                               {:label-text "Password"
                                :element-name "password"
                                :input-type "password"}]}}

 ;; Pure function implementing the responsibilities of the component:
 (fn [{:keys [element-name label-text input-type auto-focus?]
      :or {input-type "text" auto-focus? false}}]
   [:div.form-field
    [:label.form-label {:for element-name} label-text]
    [:input.text-input
     {:id element-name :name element-name
      :autofocus auto-focus? :type input-type}]]))

The ->html function can be used to convert Hiccup data, which references the component using its qualified keyword, to HTML:

[:ux.forms/text-input
  {:auto-focus? true
   :label-text "Email address"
   :element-name "email-address"
   :input-type "email"}]

The following HTML is returned:

<div class="form-field">
  <label class="form-label" for="email-address">Email address</label>
  <input autofocus="autofocus" class="text-input"
         id="email-address" name="email-address"
         type="email"/>
</div>

Step 5: Form buttons

The next step is to register components that will be responsible for form buttons.

We'll register two buttons :ux.forms/primary-submit-button and :ux.forms/cancel-button with metadata that document the button components:

(hc/reg-component
 :ux.forms/primary-submit-button

 {:doc
  "Represents the button that will take the primary action which is submitting
   the form. Button text is specified as the first and only parameter."

  :examples {"Login submit button" [:ux.forms/primary-submit-button
                                    "Login now"]

             "Save submit button" [:ux.forms/primary-submit-button
                                   "Save"]}}

 (fn [button-text]
   [:button.primary-submit-button {:type "submit"} button-text]))


(hc/reg-component
 :ux.forms/cancel-button

 {:doc "Represents a button that is used to cancel an action and not submit a
        form, for example going back to a previous page."

  :examples {"Login submit button" [:ux.forms/cancel-button
                                    "Login now"]

             "Save submit button" [:ux.forms/cancel-button
                                   "Save"]}}

 (fn [button-text url-to-redirect-to]
   [:button.cancel-button
    {:type "button"
     :onclick (str "javascript:window.location='"
                   url-to-redirect-to "'")}
    button-text]))

In addition to the submit and cancel button components we need a container element for form buttons that will centre align and space the buttons consistently:

(hc/reg-component
 :ux.forms/action-buttons
 {:doc
  "A container that will centre and consistently space buttons which represent
   actions that can be taken on the form such as submitting the form or
   going back to a previous page.

   Usually placed at the bottom of a form after input fields.

   Designed to work with form buttons such as `:ux.forms/primary-submit-button`
   and `:ux.forms/cancel-button`"

  :example [:ux.forms/action-buttons
            [:ux.forms/primary-submit-button "Login now"]
            [:ux.forms/cancel-button "Back" "/home"]]}
 (fn [& children]
   [:div.form-action-buttons
    children]))

The ->html function can be used to convert Hiccup data, which references the component using its qualified keyword, to HTML:

(hc/->html
  [:ux.forms/action-buttons
    [:ux.forms/primary-submit-button "Login now"]
    [:ux.forms/cancel-button "Back" "/home"]])

The following HTML is returned:

<div class="form-action-buttons">
    <button class="primary-submit-button" type="submit">Login now</button>

    <button class="cancel-button"
            onclick="javascript:window.location=&apos;/home&apos;"
            type="button">
            Back
    </button>
</div>

Step 6: Composing all form elements into a login screen and generating HTML

We have now registered components for the following form components:

  • :ux.layouts/html-doc - Builds the main HTML document.
  • :ux.forms/form - Builds the HTML form element.
  • :ux.forms/text-input - Builds a single-line text field.
  • :ux.forms/action-buttons- Layout container for buttons at the bottom of a form.
  • :ux.forms/cancel-button - A less pronounced button used for cancel actions.
  • :ux.forms/primary-submit-button - The primary button of the form.

From this catalogue of form components we can compose another component that represents a typical login page a user would use to authenticate:

(hc/reg-component
 :ux.pages/login

 {:doc "The main login page for user authentication"
  :example [:ux.pages/login]}

 (fn []
   [:ux.layouts/html-doc {:title "Login now"}

    [:ux.forms/form
     {:title "Login now"
      :intro-text "Enter your email address and password to access your personal dashboard"
      :action "/login"}

     [:ux.forms/text-input
      {:label-text "Email address"
       :element-name "email-address"
       :auto-focus? true}]

     [:ux.forms/text-input
      {:label-text "Password"
       :element-name "password"
       :input-type "password"}]

     [:ux.forms/action-buttons
      [:ux.forms/primary-submit-button "Login now"]
      [:ux.forms/cancel-button "Cancel" "/home"]]]]))

We can then generate the HTML by using the component->html function:

(hc/component->html :ux.pages/login)

Which produces the following HTML:

<html>
  <head>
      <meta charset="UTF-8"/>
      <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
      <link href="/css/main.css" rel="stylesheet"/>
      <script src="/js/app-bundle.js"></script>
      <title>Login now</title>
  </head>
  <body>
      <form action="/login" method="POST" class="input-form">
          <h1 class="form-title">Login now</h1>
          <p class="form-intro-text">
            Enter your email address and password to access your personal
            dashboard.
          </p>
          <div class="form-field">
              <label class="form-label" for="email-address">Email address</label>
              <input autofocus="autofocus" class="text-input"
                     id="email-address"
                     name="email-address" type="text"/>
          </div>
          <div class="form-field">
              <label class="form-label" for="password">Password</label>
              <input class="text-input" id="password" name="password"
                  type="password"/>
          </div>
          <div class="form-action-buttons">
              <button class="primary-submit-button" type="submit">Login now</button>
              <button class="cancel-button"
                      onclick="javascript:window.location=&apos;/home&apos;"
                      type="button">Cancel</button>
          </div>
      </form>
  </body>
</html>

back to top

Defining components

Defining components is achieved by using the reg-component function, which associates a qualified keyword with either a function (for dynamic content) or a vector or string (for static content) that represents a piece of the user interface.

In turn, components can then be referenced by their qualified keyword in Hiccup data, much like HTML elements, allowing for composition.

The Hiccup server components library is included in your code by requiring the ten-d-c.hiccup-server-components.core namespace:

(ns your-web-app.ux.core
  (:require [ten-d-c.hiccup-server-components.core :as hc]))

Registering component functions

When a component returns dynamic content and accepts parameters that affect its output, it can be registered using a function.

The below example registers a component :ux.components/primary-submit-button which abstracts a HTML submit button:

(hc/reg-component :ux.components/primary-submit-button
                  (fn [text attributes]
                    [:button.primary-button.submit-button
                     (assoc attributes :type "submit")
                     text]))

The above component function accepts a text parameter (representing the button text) as well as an attributes parameter (representing arbitrary attributes of the button). In addition, a primary-button & submit-button css class are added to the button using the Hiccup CSS-like shortcut.

When registering a component in this manner the function should meet the following requirements:

1.) Should be a pure function:

  • A pure function is free of side effects, does not change any other part of the system, and is not affected by any other part of the system.

  • A pure function's return values are identical for identical arguments.

2.) Should have an intuitive argument structure and provide sensible defaults. This will be covered in more detail later.

3.) Should return either a vector representing Hiccup data or a string.

To expand on the previous example and highlight a different approach to function arguments, we could define a :ux.components/cancel-button as follows:

(hc/reg-component
  :ux.components/cancel-button
  (fn [& {:keys [text on-cancel]}]
    [:button.cancel-button
     {:type "button" :onclick on-cancel}
     text]))

The above component function destructures a text parameter (representing the button text) as well as an on-cancel handler (representing the client-side onclick handler of the button). In addition, a cancel-button css class is added to the button.

Component metadata

When a component is registered using the reg-component function, metadata in the form of a map can be included as the second parameter to help document the component and its parameters as well as provide examples of component usage.

While arbitrary metadata can be provided, the convention of component metadata includes the following keys:

  • :doc - a docstring describing the component and its parameters,
  • :example - Hiccup data showing a single example usage of the component
  • :examples - A map with keys that describe the example and values that are Hiccup data, allowing for multiple examples to be provided.

The below example registers a :ux.layouts/centred-container component with metadata including a doc and a example key:

(hc/reg-component
 :ux.layouts/centred-container

 {:doc
   "A simple container to horizontally centre content. Accepts one or more child
    elements."
  :example [:ux.layouts/centred-container
            [:h1 "Hello world"]]}

 (fn [& children]
   [:div.centred-container children]))

The below example registers a :ux.layouts/centred-container component with metadata including a doc and a examples key:

(hc/reg-component
 :ux.layouts/centred-container

 {:doc
   "A simple container to horizontally centre content. Accepts a variable amount
    of child elements."

  :examples
  {"With a single child element" [:ux.layouts/centred-container
                                  [:h1 "Hello world"]]

   "With multiple child elements" [:ux.layouts/centred-container
                                   [:h1 "Hello world"]
                                   [:p "Hello this is a test"]
                                   [:p "This is a second paragraph"]]}}

 (fn [& children]
   [:div.centred-container children]))

Registering component vectors or strings

When a component returns static content and does not require any input parameters that affect its output, it can be registered using either a vector (representing Hiccup data) or a string (representing either plain text or an HTML string).

The below example registers a component :ux.components/sale-banner with a vector representing Hiccup data:

(hc/reg-component
  :ux.components/sale-banner
  [:div.sale-banner
   [:strong "50% off sale!"] " Buy any item on promotion and get 50% off. "
   [:a {:href "/promotions"} "View promotions"]])

The below examples register plain text components representing page titles. Plain text components can be useful for having text content available as a component:

(hc/reg-component :ux.page-titles/login-page "Login now")
(hc/reg-component :ux.page-titles/dashboard-page "Welcome to your dashboard")
(hc/reg-component :ux.page-titles/register-page "Register your account now")

By default, all strings are escaped, however, components can be registered as raw HTML strings that won't be escaped by including the :hiccup-server-components/raw-html? true option as the second parameter to reg-component.

Using Hiccup data structure is always preferred over raw HTML strings, but this option is included for completeness:

(hc/reg-component
  :ux.components/sale-banner-raw
  ;; Including this option will prevent HTML from being escaped
  {:hiccup-server-components/raw-html? true}
  "<div class='sale-banner'>
     <strong>50% off sale!</strong> Buy any item on promotion and get 50% off.
     <a href='/promotions'>Go to promotions</a>
   </div>")

back to top

Composing components

Once components have been registered they can be referenced in Hiccup data by their qualified keywords much like any other HTML element allowing for composition.

The below example shows a centred container div with a "side bar" (containing a list of links representing a menu) and a "main content" area displaying content based on what is selected in the side bar:

[:div.centred-container
  [:div.side-bar
    [:ul
      [:li.active [:a {:href "/dashboard"} "Dashboard"]]
      [:li [:a {:href "/your-profile"} "Your profile"]]
      [:li [:a {:href "/manage-users"} "Manage users"]
      [:li [:a {:href "/logout"} "Logout"]]]]]

  [:div.main-content
   [:h1.main-title "Welcome to your dashboard"]
   [:p "This is your dashboard, this is a test."]]]

In the above example div, ul, li, a, h1 and p HTML elements are composed into an abstract structure that represents the "side bar" and "main content" areas, which is centred by a parent container.

As a first step, we can extract both the "side bar" and the "main content" areas into vector components that initially represent static content:

(hc/reg-component
 :ux.components/side-bar
 [:div.side-bar
  [:ul
   [:li.active [:a {:href "/dashboard"} "Dashboard"]]
   [:li [:a {:href "/your-profile"} "Your profile"]]
   [:li [:a {:href "/manage-users"} "Manage users"]
   [:li [:a {:href "/logout"} "Logout"]]]]])


(hc/reg-component
 :ux.components/main-content
 [:div.main-content
  [:h1.main-title "Welcome to your dashboard"]
  [:p "This is your dashboard, this is a test."]])

We can then reference these components by their qualified keyword in Hiccup data (much like any other HTML element), effectively providing a level of abstraction:

[:div.centred-container
  [:ux.components/side-bar]
  [:ux.components/main-content]]

Taking it a step further, we can also abstract the "centred container" div element. Parent/container elements often have a variable number of child elements that are accommodated by using a variadic function (i.e. a function with a variable number of arguments):

(hc/reg-component
 :ux.layouts/centred-container
 {:doc "A simple container to horizontally center content"
  :examples
  {"With a single child element" [:ux.layouts/centred-container
                                   [:p "This is a test"]]

   "With multiple child elements" [:ux.layouts/centred-container
                                    [:h1 "Hello world"]
                                    [:p "Hello this is a test"]
                                    [:p "This is a second paragraph"]]}}
 ;; Variadic function with variable arguments representing child elements.
 (fn [& children]
   [:div.centred-container children]))

We can then further abstract the example as follows:

[:ux.layouts/centred-container
  [:ux.components/side-bar]
  [:ux.components/main-content]]

While for the sake of example, the "side bar" and "main content" components were initially registered as static content (using a vector representing Hiccup data), these components should become dynamic by using component functions.

Converting to a dynamic side bar component

To convert the "side bar" into a dynamic component we can take the following steps:

  1. Define the component's responsibilities: The component is responsible for constructing a "side bar" element that includes a list of links representing a menu. It is also responsible for determining which of the links in the menu is "active".

  2. Define component parameters: The component accepts a map as parameter that includes two keys: active-url representing the url of the menu link that should be marked as active and menu-items which is a vector of maps that include a href and label key which represent the menu items.

    An example of referencing the component with parameters:

    [:ux.components/side-bar
      {:active-url "/dashboard"
       :menu-items [{:href "/dashboard" :label "Dashboard"} ;; marked as active
                    {:href "/your-profile" :label "Your profile"}
                    {:href "/manage-users" :label "Manage users"}
                    {:href "/logout" :label "Logout"}]}]
  3. Register a component that meets the above requirements:

    (hc/reg-component
     :ux.components/side-bar
     ;; Accepts `active-url` and `menu-items`
     (fn [{:keys [active-url menu-items]}]
       [:div.side-bar
        [:ul
         ;; Builds a list of menu items
         (for [{:keys [href label]} menu-items]
           ;; Determines if the menu item is active based on the `active-url`
           ;; and assigns a "active" CSS class
           (let [css-class (when (= href active-url)
                             "active")]
             [:li {:class css-class}
              [:a {:href href} label]]))]]))
  4. When a component is registered, meta data in the form of a map can be included as the second parameter to help document the component as well as its parameters.

    While arbitrary metadata can be provided, based on the developers needs, the two common keys are doc which is a string describing the component and it's parameters and example which is Hiccup data showing example usage of the component.

    Below is an expanded example of the side bar that includes doc and example metadata:

    (hc/reg-component
     :ux.components/side-bar
     {:doc
       "Responsible for constructing a \"side bar\" element that includes a list
        of links representing a menu. It is also responsible for determining
        which of the menu items are \"active\".
    
        Supported parameters:
    
        `:active-url` (optional) - The url of the menu item that should be
                                   marked as active. e.g. \"/your-profile\"
    
        `:menu-items` (required) - A vector of maps that include a `href` and
                                   `label` key which represent the menu items.
                                   e.g. `[{:href \"/your-profile\"
                                           :label \"Your profile\"}
                                          {:href \"/manage-users\"
                                           :label \"Manage users\"}]`"
    
      :example [:ux.components/side-bar
                {:active-url "/dashboard"
                 :menu-items [{:href "/dashboard" :label "Dashboard"}
                              {:href "/your-profile" :label "Your profile"}
                              {:href "/manage-users" :label "Manage users"}
                              {:href "/logout" :label "Logout"}]}]}
     (fn [{:keys [active-url menu-items]}]
       [:div.side-bar
        [:ul
         ;; Builds a list of menu items
         (for [{:keys [href label]} menu-items]
           ;; Determines if the menu item is active based on the `active-url`
           ;; and assigns a "active" CSS class
           (let [css-class (when (= href active-url)
                             "active")]
             [:li {:class css-class}
              [:a {:href href} label]]))]]))

Converting to a dynamic main content component

To convert the "main content" into a dynamic component we can do the following:

  1. Define the components responsibilities: The component is responsible for constructing a "main content" element that includes a title element (h1) as well as variable amount of child elements.

  2. Define component parameters: The component accepts a map as its first parameter that includes a title keys representing the title shown in the main area. The second parameter is a list of variable child elements (using Clojure's variadic function syntax) that will be displayed within the main content area.

    An example of referencing the components with parameters:

    [:ux.components/main-content
      ;; Component options
      {:title "Welcome to your dashboard"}
      ;; Variable child elements
      [:p "This is your dashboard, this is a test."]
      [:p "Use the following link to take an action "
        [:a {:href "/some-action"} "Take action now"]]]
  3. Register a component (including doc and example metadata) that meets the above requirements:

    (hc/reg-component
     :ux.components/main-content
     {:doc
       "Responsible for constructing a \"main content\" element that includes a
        title element (`h1`) as well as variable amount of child elements.
    
        Accepts a map as the first parameter that should include a `title` keys
        representing the title shown in the main area.
    
        The second parameter is a list of variable child elements that will be
        displayed within the main content area."
    
      :example [:ux.components/main-content
                {:title "Welcome to your dashboard"}
                [:p "This is your dashboard, this is a test."]
                [:p "Use the following link to take an action "
                 [:a {:href "/some-action"} "Take action now"]]]}
     (fn [{:keys [title]} & children]
       [:div.main-content
        ;; Title always shown
        [:h1.main-title title]
        ;; Child elements defined by consumer of component.
        children]))

With both the "side bar" and "main content" components now being dynamic we can rewrite the original Hiccup data using the new components which results in a higher level of abstraction.

Before:

[:div.centred-container
  [:div.side-bar
    [:ul
      [:li.active [:a {:href "/dashboard"} "Dashboard"]]
      [:li [:a {:href "/your-profile"} "Your profile"]]
      [:li [:a {:href "/manage-users"} "Manage users"]
      [:li [:a {:href "/logout"} "Logout"]]]]]

  [:div.main-content
   [:h1.main-title "Welcome to your dashboard"]
   [:p "This is your dashboard, this is a test."]]]

After:

[:ux.layouts/centred-container

  [:ux.components/side-bar
    {:active-url "/dashboard"
     :menu-items [{:href "/dashboard" :label "Dashboard"}
                  {:href "/your-profile" :label "Your profile"}
                  {:href "/manage-users" :label "Manage users"}
                  {:href "/logout" :label "Logout"}]}]

  [:ux.components/main-content
    {:title "Welcome to your dashboard"}
    [:p "This is your dashboard, this is a test."]
    [:p "Use the following link to take an action "
      [:a {:href "/some-action"} "Take action now"]]]]

Would produce the following HTML:

<div class="centred-container">
  <div class="side-bar">
    <ul>
      <li class="active">
          <a href="/dashboard">Dashboard</a>
      </li>      
      <li>
          <a href="/your-profile">Your profile</a>
      </li>
      <li>
          <a href="/manage-users">Manage users</a>
      </li>
      <li>
          <a href="/logout">Logout</a>
      </li>
    </ul>
  </div>
  <div class="main-content">
    <h1 class="main-title">Welcome to your dashboard</h1>
    <p>This is your dashboard, this is a test.</p>
    <p>
      Use the following link to take an action
      <a href="/some-action">Take action now</a>
    </p>
  </div>
</div>

back to top

Organising components

Organising components is done through the use of qualified keywords, Clojure namespaces, and source code structure to create a component catalogue.

When registering a component a qualified keyword must be used. Qualified keywords make components unique, provide a means of logical categorisation, and allow component references to be quickly identified when they are composed into Hiccup data.

Convention for organising components

The following namespace and source code structure is used to organise components:

  1. A top-level/parent directory (usually src/ux) with a src/ux/core.clj file representing the top level component namespace ux.core, the main entry point for all components. Requiring this namespace in other areas of your application will make all components available to consuming code.

  2. Child source files in the top-level/parent directory representing secondary component namespaces to group logically related components:

    Examples of secondary namespaces in parent directory src/ux:

    • Namespace: ux.layouts in file src/ux/layouts.clj
    • Namespace ux.buttons in file src/ux/buttons.clj
    • Namespace ux.pages in file src/ux/pages.clj
  3. Secondary component namespaces are made available by requiring them in the main component namespace:

    ;; File src/ux/core.clj
    
    (ns ux.core ;; Main component namespace
      ;; Require secondary components, making them generally available.
      (:require [ux.layouts]
                [ux.buttons]
                [ux.pages]))
  4. Tertiary component namespaces can be used for further separation of components and have their own subdirectory under the main parent directory.

    • Example: namespace: ux.layouts.html-doc in file src/ux/layouts/html_doc.clj
  5. Tertiary component namespaces are made available by requiring them in the relevant secondary component namespace:

    ;; File src/ux/layouts.clj
    
    (ns ux.layouts ;; Secondary component namespace
      ;; Include tertiary components
      (:require [ux.layouts.html-doc]))

This convention should cover 90% of component organisation needs and allow developers to build a component catalogue.

Below is an example directory structure showing component source file organisation:

ux                   # Top-level/parent directory
├── core.clj         # File: Main component namespace `ux.core`
├── layouts.clj      # File: Secondary `ux.layouts` namespace
├── layouts          # Directory: Tertiary component namespaces for layouts
│   └── html_doc.clj # File: Tertiary `ux.layouts.html-doc` namespace
├── buttons.clj      # File: Secondary `ux.buttons` namespace
└── pages.clj        # File: Secondary `ux.pages` namespace

Main entry point namespace for components

In order to make your components generally available in your application we'll employ standard Clojure source code and namespace organisation techniques.

As an example we'll create a ux directory containing a file src/ux/core.clj which represents the top level component namespace ux.core and main entry point for all your components. Requiring this file in other namespaces in your application will make all components available to consuming code.

Initially the file includes only the namespace definition:

;; File src/ux/core.clj
(ns ux.core)

Secondary component namespaces and source files

Once the top-level component namespace has been created the next step is to create Clojure source files representing secondary component namespaces.

For example if we have layout components we would have a ux.layouts namespace in file src/ux/layouts.clj. This file represents the main entry point for layout components.

Below we define the ux.layouts namespace by creating the src/ux/layouts.clj file and registering a couple of components in the :ux.layouts namespace:

;; File src/ux/layouts.clj

(ns ux.layouts
  (:require [ten-d-c.hiccup-server-components.core :as hc]))

(hc/reg-component :ux.layouts/html-doc "...")
(hc/reg-component :ux.layouts/centred-container "...")
(hc/reg-component :ux.layouts/centred-screen "...")
(hc/reg-component :ux.layouts/section "...")
(hc/reg-component :ux.layouts/form "...")
(hc/reg-component :ux.layouts/footer "...")

To expand on this we could have a ux.buttons namespace in the src/ux/buttons.clj file which represents the main entry point for button components:

;; File src/ux/buttons.clj

(ns ux.buttons
  (:require [ten-d-c.hiccup-server-components.core :as hc]))

(hc/reg-component :ux.buttons/primary-button "...")
(hc/reg-component :ux.buttons/secondary-button "...")
(hc/reg-component :ux.buttons/tertiary-button "...")
(hc/reg-component :ux.buttons/info-button "...")
(hc/reg-component :ux.buttons/success-button "...")
(hc/reg-component :ux.buttons/danger-button "...")
(hc/reg-component :ux.buttons/warning-button "...")

We could have a ux.pages namespace in the src/ux/pages.clj file which represents the main entry point for high level webpages:

;; File src/ux/pages.clj

(ns ux.pages
  (:require [ten-d-c.hiccup-server-components.core :as hc]))

(hc/reg-component :ux.pages/home "...")
(hc/reg-component :ux.pages/dashboard "...")
(hc/reg-component :ux.pages/your-profile "...")
(hc/reg-component :ux.pages/manage-users "...")

Including secondary component namespaces in main entry point file

To make these components available to the application we would then require these namespaces (ux.layouts ux.buttons and ux.pages) in the main component entry point namespace ux.core defined in file src/ux/core.clj:

;; File src/ux/core.clj

(ns ux.core
  ;; Require second level of components, making them generally available.
  (:require [ux.layouts]
            [ux.buttons]
            [ux.pages]))

Tertiary component namespaces and source files

Some components may be complex enough or have thorough documentation to warrant their own namespace or it might be the developers preference to create a high degree of separation between components.

For example, lets say we had a :ux.layouts/html-doc component that supported many options, included thorough documentation and was more complex than other layouts. We could create a further separation by creating a dedicated ux.layouts.html-doc namespace and source file src/ux/layouts/html_doc.clj

;; File src/ux/layouts/html_doc.clj

(ns ux.layouts.html-doc
  (:require [ten-d-c.hiccup-server-components.core :as hc]))

(hc/reg-component :ux.layouts/html-doc "..." )

We could then require this tertiary source file in the layout namespace ux.layouts to make it generally available.

Since the ux.layouts namespace is already required in the main component entry point namespace ux.core, the tertiary component :ux.layouts/html-doc will automatically become available:

;; File src/ux/layouts.clj

(ns ux.layouts
  (:require [ux.layouts.html-doc] ;; include the html-doc
            [ten-d-c.hiccup-server-components.core :as hc]))

;; `:ux.layouts/html-doc` component now reginsred in `ux.layouts.html-doc` namespace.

(hc/reg-component :ux.layouts/centred-container "...")
(hc/reg-component :ux.layouts/centred-screen "...")
(hc/reg-component :ux.layouts/section "...")
(hc/reg-component :ux.layouts/form "...")
(hc/reg-component :ux.layouts/footer "...")            

back to top

Generating HTML

The below functions are provided to generate HTML from Hiccup data that can include component references:

  • ->html Takes hiccup-data, that can include component references, and returns the generated HTML.

  • ->html-file Takes a file-path and hiccup-data, that can include component references, and saves the generated HTML to the given file-path.

  • component->html Generates and returns HTML of a component with the given component-element-name.

  • component->html-file Generates the HTML of a component with the given component-element-name and saves the output to the given file-path.

See API docs for details.

back to top

Built-in components

Hiccup server components ships with a number of built-in components that are common to web app development.

See the built-in component documentation for more details on built-in components.

Summary of built-in components:

  • :ux/css-classes: Constructs a list of one or more css classes that can be used as the "class" attribute of HTML elements.

  • :ux/fragment: Allows for returning multiple child elements without the need for a parent element.

  • :ux/html: Used for including strings that contain HTML markup, this component converts the provided html into an unescaped string, allowing the HTML to be rendered by the browser.

  • :ux/html-template: Returns an unescaped HTML string where interpolated variables are replaced using values in the map provided by variable-substitution-map allowing for templated HTML strings.

  • :ux/html5-doc: Constructs a HTML5 document with the correct DOCTYPE using the given document as child elements (e.g head, body) of the <html> tag.

  • :ux/include-javascripts: Includes one or more <script> tags that link to external javascript files through the src attribute.

  • :ux/include-stylesheets: Includes one or more <link> tags that link to external style sheets.

  • :ux/javascript: Used for including unescaped, executable javascript in Hiccup data.

  • :ux/javascript-tag: Wraps the supplied javascript-lines in a <script> tag which executes the javascript in the web browser.

  • :ux/string-template: Returns a string where interpolated variables are replaced using values in the map provided by variable-substitution-map allowing for templated strings.

  • :ux/style-tag: A <style> tag used to define style information (CSS) for a document.

HTTP routing middleware

Ring middleware that will generate HTML using Hiccup server components conventions and set the :body of the response to the generated HTML.

Works with Compojure and Reitit routing libraries as well as Ring compatible HTTP servers.

HTTP route handlers configured with this middleware can return a map including the following keys which will result in HTML being set on the response body:

  • :hsc/component: The qualified keyword of the component to use to generate HTML that will be set as the :body of the response. Component params can be supplied with the :hsc/params key.

  • :hsc/params (Optional): Used in conjunction with the :hsc/component key, represents params that will be passed to the component.

  • :hsc/html: Hiccup data (vectors describing HTML), that can include component references, that will be used to generate HTML that will be set as the :body of the response.

Example of Compojure routes with middleware configured:

(ns http-routing.compojure
  (:require [compojure.core :refer :all]
            [ten-d-c.hiccup-server-components.core :as hc]))

(compojure.core/defroutes app

  ;; Generates and returns the HTML for the `:ux.pages/home` component, no
  ;; component params are provided.
  (GET "/" []
       {:hsc/component :ux.pages/home})


  ;; Generates and returns the HTML for the `:ux.pages/dashboard` component,
  ;; passing the component the `hsc/params` key as params.
  (GET "/dashboard" []
       {:hsc/component :ux.pages/dashboard
        :hsc/params {:username "bobsmith"
                     :email-address "bobsmith@somemail.net"}})


  ;; Generates and returns the HTML from Hiccup data (which can include
  ;; component refererences) in the `:hsc/html` key.
  (GET "/testing" []
       {:hsc/html [:ux.layouts/html-doc {:title "A test page"}
                   [:div
                    [:h1.text-3xl "Hello world From HTML"]
                    [:p "This is a test"]]]}))


(def web-app (-> app
                 (hc/wrap-response-middleware)))

Example of Reitit routes with middleware configured:

(ns http-routing.reitit
  (:require [reitit.ring :as ring]
            [ten-d-c.hiccup-server-components.core :as hc]))


(def web-app
  (ring/ring-handler
   (ring/router
    [["/" {:handler (fn [_]
                      {:hsc/component :ux.pages/home})}]

     ["/dashboard"
      {:get
       {:handler
        (fn [_]
          {:hsc/component :ux.pages/dashboard
           :hsc/params {:username "bobsmith"
                        :email-address "bobsmith@somemail.net"}})}}]

     ["/testing"
      {:get
       {:handler
        (fn [_]
          {:hsc/html [:ux.layouts/html-doc {:title "A test page"}
                      [:div
                       [:h1.text-3xl "Hello world From HTML"]
                       [:p "This is a test"]]]})}}]]

    {:data {:middleware [hc/wrap-response-middleware]
            :enable true}})))

See wrap-response-middleware documentation for more details.

back to top

About

A server-side rendering (SSR) library for Clojure web applications that facilitates defining, composing, organising, and unit testing user interface components.

Topics

Resources

Stars

Watchers

Forks