teiaTeia is a Simple, Fast and Reliable HyperMedia library. | (this space intentionally left almost blank) |
ComponentsComponents are Teia's most basic building block. A component contains the following data:
| (ns j0suetm.teia.component (:refer-clojure :exclude [compile])) |
Lifetime | |
The Stateful protocol defines the point on when a component has both props and inner components defined.
| (defprotocol Stateful
(build
[cmp]
[cmp props]
[cmp props components])
(compile
[cmp])) |
Barebones component for failure showcase. Defined like this because the Component record isn't defined yet. | (def failure-cmp
{:name :failure
:template (fn [{:keys [props]}]
[:div
[:p (:reason props)]
(let [ex (:exception props)
stacktrace (map str (.getStackTrace ex))]
[:ul
[:li [:p (.getMessage ex)]]
(for [trace stacktrace]
[:li [:p trace]])])])}) |
Inner components come as a list. Here we reduce them to a map in order for easier retrieval when applying the parent component TODO: provide better ways to handle exceptions. Enable the end user to control it as well. | (defrecord Component [name template]
Stateful
(build
[cmp]
(build cmp {} []))
(build
[cmp props]
(build cmp props []))
(build
[cmp props components]
(merge cmp {:props props
:components components}))
(compile
[cmp]
(let [{:keys [name template props components]} cmp]
(try
(assoc
cmp :compiled
(template
{:props props
:components (reduce
(fn [cmp-map cmp]
(assoc
cmp-map
(:name cmp)
(or (:compiled cmp) cmp)))
{} components)}))
(catch Exception e
(compile
(build
(map->Component failure-cmp)
{:reason (str "failed to compile component " name)
:exception e}))))))) |
Alias to pipe | (defn $
([cmp] ($ cmp {} []))
([cmp props] ($ cmp props []))
([cmp props components]
(:compiled
(compile
(build cmp props components))))) |
(defn component? [data] (instance? Component data)) | |
Teia is a Simple, Fast and Reliable HyperMedia library. | (ns j0suetm.teia.core) |
Helper macro that defines a component. Simply wraps | (defmacro defcmp
[cmp-name args body]
`(j0suetm.teia.component/->Component
(keyword '~cmp-name)
(fn ~args
~@body))) |
(ns j0suetm.teia.html) | |
(defn populate [tree]) | |
RoutingTeia eases the process of routing a component. Besides having this
as its priority, it still offers support for other purposes, like
a | (ns j0suetm.teia.router (:require [hiccup2.core :as hiccup] [j0suetm.teia.component :as teia.cmp] [malli.util] [muuntaja.core :as muuntaja] [muuntaja.format.core :as muuntaja.fmt] [reitit.coercion.malli :as reitit.coercion.malli] [reitit.dev.pretty :as reitit.pretty] [reitit.ring :as reitit] [reitit.ring.coercion :as reitit.coercion] [reitit.ring.middleware.exception :as reitit.mddwr.ex] [reitit.ring.middleware.muuntaja :as reitit.mddwr.muuntaja] [reitit.ring.middleware.parameters :as reitit.mddwr.params]) (:import [java.io OutputStream])) |
Muuntaja encoder that renders a given component through hiccup. See: https://github.com/metosin/muuntaja/blob/master/modules/muuntaja/src/muuntaja/format/json.clj#L39 | (def component->html-encoder
(reify
muuntaja.fmt/EncodeToBytes
(encode-to-bytes [_ data charset]
(-> (if (teia.cmp/component? data)
(:compiled (teia.cmp/compile data))
data)
(hiccup/html)
(str)
(.getBytes ^String charset)))
muuntaja.fmt/EncodeToOutputStream
(encode-to-output-stream [_ data charset]
(fn [^OutputStream output-stream]
(.write
output-stream
(-> (if (teia.cmp/component? data)
(:compiled (teia.cmp/compile data))
data)
(hiccup/html)
(str)
(.getBytes ^String charset))))))) |
Muuntaja format for text/html | (def html-format
(let [enc-fn (fn [_]
component->html-encoder)]
(muuntaja.fmt/map->Format
{:name "text/html"
:encoder [enc-fn]}))) |
RouteTeia uses reitit in its back-end. So you can define routes the same way you have been doing with reitit, while also being able to define component routes in-between the other routes. Teia will parse them for you, and re-arrange the entire route tree, with reitit routes wrapping your pre-defined component routes. | |
For example, a sample component route defined like this: will be adapted to: which renders your component, using the states from the handler function. | (comment
["/greet/:name"
{:get
{:component (teia.cmp/->Component
:greeting
(fn [{:keys [props]}]
[:p (str (:greeting props) ", "
(:name props) "!")]))
:parameters {:path {:name :string}
:headers {:greeting :string}}
:handler (fn [{:keys [path-params headers]}]
{:status 200
:props (merge path-params headers)})}}]
["/greet/:name"
{:get
{:parameters {:path {:name :string}
:headers {:greeting :string}}
:handler 'j0suetm.teia.router/component-route-handler
}}]
) |
Generic reitit handler that uses the return from the defined handler as the state to render a route's component. The handler's response should be a map containing the props and the inner components be used by the to-be-built component. | (defn component-route-handler
[handler component request]
(try
(let [{:keys [status props components]
:or {status 200
props {}
components []}} (handler request)]
{:status status
:body (teia.cmp/compile
(teia.cmp/build component props components))})
(catch Exception e
{:status 500
:body (teia.cmp/build
(teia.cmp/map->Component teia.cmp/failure-cmp)
{:reason "failed to handle component route"
:exception e})}))) |
Adapts a component route to a reitit router. Recursively applies itself to any sibling route. | (defn component-route->reitit-route
[uri methods & sibling-routes]
(into
[uri (reduce-kv
(fn [methods method
{:keys [handler component]
:as definition}]
(let [handler' (partial component-route-handler
handler component)]
(assoc methods method
(if component
(-> (assoc definition :handler handler')
(dissoc :component))
definition))))
{} methods)]
(mapv
(partial apply component-route->reitit-route)
sibling-routes))) |
Builds a reitit router, adapting component routes to reitit routes through the way. Options:
No need to verify response when UI router. | (defn build
[routes & [options]]
(let [{:keys [default-format]
:or {default-format "text/html"}} options
ui-router? (= default-format "text/html")
coercion (reitit.coercion.malli/create
{:error-keys #{:type :in :value :errors
:humanized :schema}
:compile malli.util/closed-schema
:strip-extra-keys true
:default-values true
:options nil})
muuntaja (muuntaja/create
(-> (assoc-in
muuntaja/default-options
[:formats "text/html"]
html-format)
(assoc :default-format default-format)))
middlewares (into
[reitit.mddwr.ex/exception-middleware
reitit.mddwr.muuntaja/format-negotiate-middleware
reitit.mddwr.muuntaja/format-request-middleware
reitit.mddwr.muuntaja/format-response-middleware
reitit.coercion/coerce-request-middleware
reitit.mddwr.params/parameters-middleware]
(when ui-router?
[reitit.coercion/coerce-response-middleware]))]
(reitit/router
(map #(apply component-route->reitit-route %) routes)
{:exception reitit.pretty/exception
:data {:coercion coercion
:muuntaja muuntaja
:middleware middlewares}}))) |