Skip to content

Commit

Permalink
♻️ Use Font Awesome icons without JavaScript on SSR pages
Browse files Browse the repository at this point in the history
Exports the SVG icons to the resources folder for the Clojure backend to
read. Adds the same CSS classes as the Font Awesome (FA) JavaScript
library would normally add.

Why:
- Reduces the amount of unnecessary JavaScript dependencies.
- The pages load faster, when the icons are rendered correctly
  immediately, instead of after the FA JS has finished loading.
- We can avoid the workaround of hiding the page content before FA has
  loaded (i.e. has added the .fontawesome-i2svg-active class on body),
  which has the risk of breaking everything if FA breaks.
    https://docs.fontawesome.com/web/dig-deeper/svg-async
- The SVG files were named according to how this application uses them,
  instead of how FA originally calls them. This will make it easier to
  switch individual icons, when the only line to change will be the
  import statement.
- Instead of adding custom styling, as FA documentation advices when
  using bare SVGs (https://docs.fontawesome.com/web/add-icons/svg-bare),
  we use the same CSS as FA's JS library uses. This is to avoid minor
  differences in implementation. It also comes with .fa-rotate-90 and
  similar helper classes for basic transformations.
- The solution uses Clojure macros to precompute the SVG images where
  possible, to speed up generating HTML. If the SVG has dynamically
  computed attributes, it'll fall back to preparing the SVG at runtime.
  • Loading branch information
luontola committed Jul 20, 2024
1 parent 66dd031 commit d9715e9
Show file tree
Hide file tree
Showing 18 changed files with 153 additions and 107 deletions.
44 changes: 35 additions & 9 deletions src/territory_bro/ui/html.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
;; The license text is at http://www.apache.org/licenses/LICENSE-2.0

(ns territory-bro.ui.html
(:require [clojure.string :as str]
(:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.logging :as log]
[hiccup.util :as hiccup.util]
[hiccup2.core :as h]
[reitit.core :as reitit]
[ring.util.http-response :as http-response]
[ring.util.response :as response])
Expand Down Expand Up @@ -44,14 +47,6 @@
;; visualize select field's selected option
(str/replace #"<option\b[^>]*\bselected\b.*?>(.*?)</option>" " [$1] ") ; keep selected option
(str/replace #"<option\b.*?>(.*?)</option>" "") ; remove all other options
;; visualize Font Awesome icons
(str/replace #"<i\b[^>]*\bclass=\"(fa-.*?)\".*?></i>"
(fn [[_ class]]
(let [class (->> (str/split class #" ")
(filter font-awesome-class?)
(remove font-awesome-icon-styles)
(str/join " "))]
(str " {" class "} "))))
;; hide template elements
(str/replace #"<template\b[^>]*>.*?</template>" " ")
;; strip all HTML tags
Expand Down Expand Up @@ -90,3 +85,34 @@
wildcard-url (str/replace url #"-[0-9a-f]{8}\." "-*.")] ; mapping for content-hashed filenames
[wildcard-url url])))
(into {}))))

(defn inline-svg* [path args]
{:pre [(string? path)
(or (nil? args) (map? args))]}
(if-some [resource (io/resource path)]
(let [args-tmp (str (h/html [:svg (-> args
(assoc :data-test-icon (str "{" (.getName (io/file path)) "}"))
(dissoc :class))]))
args-html (second (re-find #"(<svg.*?)>" args-tmp))]
;; TODO: this is getting too complicated - use a HTML/XML transformation library like Enlive or Hickory
(-> (slurp resource)
(str/replace #"(class=\".*?)(\")" (if-some [class (:class args)]
(str "$1 "
(str/escape class {\< "&lt;"
\> "&gt;"
\& "&amp;"})
"$2")
"$1$2"))
(str/replace-first ">" (if-some [title (:title args)]
(str ">" (h/html [:title title]))
">"))
(str/replace "<svg" args-html)))
(log/warn "territory-bro.ui.html/inline-svg: Resource not found:" path)))

(defmacro inline-svg
([path]
(when-some [svg (inline-svg* path nil)]
`(h/raw ~svg)))
([path args]
;; if the args are dynamic, this macro can't precompute the SVG at compile time
`(h/raw (inline-svg* ~path ~args))))
5 changes: 3 additions & 2 deletions src/territory_bro/ui/info_box.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@

(ns territory-bro.ui.info-box
(:require [hiccup2.core :as h]
[territory-bro.ui.css :as css]))
[territory-bro.ui.css :as css]
[territory-bro.ui.html :as html]))

(defn view [{:keys [title]} content]
(let [styles (:InfoBox (css/modules))]
(h/html
[:div {:class (:root styles)}
(when (some? title)
[:div {:class (:title styles)}
[:i.fa-solid.fa-info-circle]
(html/inline-svg "icons/info.svg")
" "
title])
[:div {:class (:content styles)}
Expand Down
31 changes: 8 additions & 23 deletions src/territory_bro/ui/layout.clj
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
:title (i18n/t "Navigation.opensInNewWindow")}
title
" "
[:i.fa-solid.fa-external-link-alt]]))
(html/inline-svg "icons/external-link.svg")]))

(defn home-navigation []
(let [styles (:Layout (css/modules))]
Expand Down Expand Up @@ -129,8 +129,9 @@
(h/html
[:form.pure-form {:method "get"}
[:label
[:i.fa-solid.fa-language {:title (i18n/t "Navigation.changeLanguage")
:class (:languageSelectionIcon styles)}]
(html/inline-svg "icons/language.svg"
{:title (i18n/t "Navigation.changeLanguage")
:class (:languageSelectionIcon styles)})
" "
[:select#language-selection {:name "lang"
:aria-label (i18n/t "Navigation.changeLanguage")
Expand All @@ -146,8 +147,9 @@

(defn authentication-panel [{:keys [user login-url dev?]}]
(if (some? user)
(h/html [:i.fa-solid.fa-user-large {:style {:font-size "1.25em"
:vertical-align "middle"}}]
(h/html (html/inline-svg "icons/user.svg"
{:style {:font-size "1.25em"
:vertical-align "middle"}})
" " (:name user) " "
[:a#logout-button.pure-button {:href "/logout"}
(i18n/t "Navigation.logout")])
Expand Down Expand Up @@ -182,25 +184,8 @@
(str title " - "))
"Territory Bro"]
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
;; https://fontawesome.com/v5/docs/web/use-with/wordpress/install-manually#set-up-svg-with-cdn
;; https://cdnjs.com/libraries/font-awesome
[:script {:src "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/solid.min.js"
:integrity "sha512-+fI924YJzeYFv7M0R29zJvRThPinSUOAmo5rpR9v6G4eWIbva/prHdZGSPN440vuf781/sOd/Fr+5ey0pqdW9w=="
:defer true
:crossorigin "anonymous"
:referrerpolicy "no-referrer"}]
[:script {:src "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/regular.min.js"
:integrity "sha512-T4H/jsKWzCRypzaFpVpYyWyBUhjKfp5e/hSD234qFO/h45wKAXba+0wG/iFRq1RhybT7dXxjPYYBYCLAwPfE0Q=="
:defer true
:crossorigin "anonymous"
:referrerpolicy "no-referrer"}]
[:script {:src "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/fontawesome.min.js"
:integrity "sha512-C8qHv0HOaf4yoA7ISuuCTrsPX8qjolYTZyoFRKNA9dFKnxgzIHnYTOJhXQIt6zwpIFzCrRzUBuVgtC4e5K1nhA=="
:defer true
:crossorigin "anonymous"
:referrerpolicy "no-referrer"}]
(head-injections)]
[:body {:class (:wait-for-icons styles)}
[:body
[:nav.no-print {:class (:navbar styles)}
(if (some? (:congregation model))
(congregation-navigation model)
Expand Down
2 changes: 1 addition & 1 deletion src/territory_bro/ui/territory_list_page.clj
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
:hx-swap "outerHTML"
:hx-trigger "load"
:hx-get (str html/*page-path* "/map")}
[:i.fa-solid.fa-map-location-dot]]
(html/inline-svg "icons/map-location.svg")]
(territory-list-map model))]

[:form.pure-form {:class (:search styles)
Expand Down
6 changes: 3 additions & 3 deletions src/territory_bro/ui/territory_page.clj
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
:class (when open?
"pure-button-active")
:aria-expanded (if open? "true" "false")}
[:i.fa-solid.fa-share-nodes]
(html/inline-svg "icons/share.svg")
" "
(i18n/t "TerritoryPage.shareLink.button")]

Expand All @@ -85,7 +85,7 @@
:class (:closeButton styles)
:aria-label (i18n/t "TerritoryPage.shareLink.closePopup")
:title (i18n/t "TerritoryPage.shareLink.closePopup")}
[:i.fa-solid.fa-xmark]]
(html/inline-svg "icons/close.svg")]

[:label {:htmlFor "share-link"}
(i18n/t "TerritoryPage.shareLink.description")]
Expand All @@ -99,7 +99,7 @@
:data-clipboard-target "#share-link"
:aria-label (i18n/t "TerritoryPage.shareLink.copy")
:title (i18n/t "TerritoryPage.shareLink.copy")}
[:i.fa-solid.fa-copy]]]])])))
(html/inline-svg "icons/copy.svg")]]])])))

(defn share-link--open! [request]
(let [share (:body (api/share-territory-link request))]
Expand Down
6 changes: 3 additions & 3 deletions test/territory_bro/ui/error_page_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
"Territory Bro
🏠 Home
User guide {fa-external-link-alt}
News {fa-external-link-alt}
User guide {external-link.svg}
News {external-link.svg}
🛟 Support
{fa-language} [English]
{language.svg} Change language [English]
Login
Sorry, something went wrong 🥺
Expand Down
45 changes: 29 additions & 16 deletions test/territory_bro/ui/html_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

(ns territory-bro.ui.html-test
(:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.test :refer :all]
[hiccup.util :as hiccup.util]
[hiccup2.core :as h]
[territory-bro.ui.html :as html]))

Expand Down Expand Up @@ -37,22 +39,6 @@
(is (= "x 🟢 y z" (html/visible-text "x<div data-test-icon=\"🟢\">y</div>z"))
"spacing before, inside and after element"))

(testing "Font Awesome icons are replaced with the icon name"
(is (= "{fa-share-nodes}" (html/visible-text "<i class=\"fa-solid fa-share-nodes\"></i>"))
"icon style solid")
(is (= "{fa-share-nodes}" (html/visible-text "<i class=\"fa-regular fa-share-nodes\"></i>"))
"icon style regular")
(is (= "{fa-bell}" (html/visible-text "<i class=\"fa-bell\"></i>"))
"icon style missing")
(is (= "{fa-share-nodes}" (html/visible-text "<i attr1=\"\" class=\"fa-solid fa-share-nodes\" attr2=\"\"></i>"))
"more attributes")
(is (= "{fa-language}" (html/visible-text "<i class=\"fa-solid fa-language Layout-module__languageSelectionIcon--VcMOP\"></i>"))
"more classes")
(is (= "foo {fa-share-nodes} bar" (html/visible-text "foo<i class=\"fa-solid fa-share-nodes\"></i>bar"))
"add spacing around icon")
(is (= "" (html/visible-text "<i class=\"whatever\"></i>"))
"not an icon"))

(testing "hides template elements"
(is (= "" (html/visible-text "<template>stuff</template>")))
(is (= "" (html/visible-text "<template id=\"xyz\">stuff</template>"))))
Expand Down Expand Up @@ -87,3 +73,30 @@
(let [path (get html/public-resources "/assets/crop-mark-*.svg")]
(is (some? path))
(is (some? (io/resource (str "public" path)))))))

(deftest inline-svg-test
(testing "returns the SVG image"
(let [svg (html/inline-svg "icons/info.svg")]
(is (hiccup.util/raw-string? svg))
(is (str/starts-with? svg "<svg"))
(is (str/includes? svg " class=\"svg-inline--fa\""))
(is (str/includes? svg " data-test-icon=\"{info.svg}\""))))

(testing "supports custom attributes"
(is (str/includes? (html/inline-svg "icons/info.svg" {:foo "bar"})
" foo=\"bar\"")
"known at compile time")
(is (str/includes? (html/inline-svg "icons/info.svg" {:foo (str/upper-case "bar")})
" foo=\"BAR\"")
"dynamically computed"))

(testing "supports extra CSS classes"
(is (str/includes? (html/inline-svg "icons/info.svg" {:class "custom-class"})
" class=\"svg-inline--fa custom-class\"")))

(testing "supports titles using the SVG <title> element"
(is (str/includes? (html/inline-svg "icons/info.svg" {:title "The Title"})
"<title>The Title</title>")))

(testing "error: file not found"
(is (nil? (html/inline-svg "no-such-file")))))
32 changes: 16 additions & 16 deletions test/territory_bro/ui/layout_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@
"Territory Bro
🏠 Home
User guide {fa-external-link-alt}
News {fa-external-link-alt}
User guide {external-link.svg}
News {external-link.svg}
🛟 Support
{fa-language} [English]
{language.svg} Change language [English]
Login
Sorry, something went wrong 🥺
Expand All @@ -133,12 +133,12 @@
"the title - Territory Bro
🏠 Home
User guide {fa-external-link-alt}
News {fa-external-link-alt}
User guide {external-link.svg}
News {external-link.svg}
🛟 Support
{fa-language} [English]
{fa-user-large} John Doe
{language.svg} Change language [English]
{user.svg} John Doe
Logout
Sorry, something went wrong 🥺
Expand All @@ -162,8 +162,8 @@
⚙️ Settings
🛟 Support
{fa-language} [English]
{fa-user-large} John Doe
{language.svg} Change language [English]
{user.svg} John Doe
Logout
Sorry, something went wrong 🥺
Expand All @@ -185,8 +185,8 @@
📍 Territories
🛟 Support
{fa-language} [English]
{fa-user-large} John Doe
{language.svg} Change language [English]
{user.svg} John Doe
Logout
Sorry, something went wrong 🥺
Expand All @@ -209,13 +209,13 @@
🖨️ Printouts
🛟 Support
{fa-language} [English]
{language.svg} Change language [English]
Login
Sorry, something went wrong 🥺
Close
{fa-info-circle} Welcome to the demo
{info.svg} Welcome to the demo
This demo is limited to only viewing a congregation. Some features are restricted.
the title
Expand Down Expand Up @@ -299,18 +299,18 @@

(testing "logged in"
(is (= (html/normalize-whitespace
"{fa-user-large} John Doe
"{user.svg} John Doe
Logout")
(html/visible-text
(layout/authentication-panel logged-in-model))))))

(deftest language-selection-test
(testing "the current language is shown using only its native name"
(is (= "{fa-language} [English]"
(is (= "{language.svg} Change language [English]"
(html/visible-text
(layout/language-selection anonymous-model))))
(binding [i18n/*lang* :fi]
(is (= "{fa-language} [suomi]"
(is (= "{language.svg} Vaihda kieltä [suomi]"
(html/visible-text
(layout/language-selection anonymous-model))))))

Expand Down
4 changes: 2 additions & 2 deletions test/territory_bro/ui/map_interaction_help_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
(map-interaction-help/model {:headers {"user-agent" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1"}})))))

(def default-visible-text
"{fa-info-circle} How to interact with the maps?
"{info.svg} How to interact with the maps?
Move: drag with two fingers / drag with the left mouse button
Zoom: pinch or spread with two fingers / hold Ctrl and scroll with the mouse wheel
Rotate: rotate with two fingers / hold Alt + Shift and drag with the left mouse button")
(def mac-visible-text
"{fa-info-circle} How to interact with the maps?
"{info.svg} How to interact with the maps?
Move: drag with two fingers / drag with the left mouse button
Zoom: pinch or spread with two fingers / hold ⌘ Command and scroll with the mouse wheel
Rotate: rotate with two fingers / hold ⌥ Option + ⇧ Shift and drag with the left mouse button")
Expand Down
2 changes: 1 addition & 1 deletion test/territory_bro/ui/settings_page_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
Territory loans CSV URL (optional) [https://docs.google.com/spreadsheets/123] Link to a Google Sheets spreadsheet published to the web as CSV
{fa-info-circle} Early Access Feature: Integrate with territory loans data from Google Sheets
{info.svg} Early Access Feature: Integrate with territory loans data from Google Sheets
If you keep track of your territory loans using Google Sheets, it's possible to export the data from there
and visualize it on the map on Territory Bro's Territories page. Eventually Territory Bro will handle the
Expand Down
6 changes: 3 additions & 3 deletions test/territory_bro/ui/territory_list_page_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
;; Loan data is currently loaded from Google Sheets, which can easily take a couple of seconds.
;; Lazy loading is needed to load the territory list page faster and defer rendering the map.
(let [map-html "<territory-list-map"
placeholder-icon "{fa-map-location-dot}"]
placeholder-icon "{map-location.svg}"]
(testing "loans disabled -> show map immediately"
(let [rendered (territory-list-page/view model)]
(is (str/includes? rendered map-html))
Expand All @@ -176,7 +176,7 @@
(is (= (html/normalize-whitespace
"Territories
{fa-info-circle} Why so few territories?
{info.svg} Why so few territories?
Only those territories which have been shared with you are currently shown.
You will need to login to see the rest.
Expand All @@ -190,7 +190,7 @@
(is (= (html/normalize-whitespace
"Territories
{fa-info-circle} Why so few territories?
{info.svg} Why so few territories?
Only those territories which have been shared with you are currently shown.
You will need to request access to see the rest.
Expand Down
Loading

0 comments on commit d9715e9

Please sign in to comment.