{"id":13769855,"url":"https://whamtet.github.io/simpleui/","last_synced_at":"2025-05-11T02:33:36.544Z","repository":{"id":41134804,"uuid":"323190452","full_name":"whamtet/simpleui","owner":"whamtet","description":"JS Free Single Page Applications","archived":false,"fork":false,"pushed_at":"2024-05-21T17:23:10.000Z","size":508,"stargazers_count":234,"open_issues_count":2,"forks_count":14,"subscribers_count":9,"default_branch":"main","last_synced_at":"2024-05-21T18:38:12.484Z","etag":null,"topics":["htmx","simpleui"],"latest_commit_sha":null,"homepage":"https://whamtet.github.io/simpleui/","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/whamtet.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-12-21T00:08:51.000Z","updated_at":"2024-05-21T18:38:15.976Z","dependencies_parsed_at":"2023-11-07T06:27:24.326Z","dependency_job_id":"5fd6b407-7a8e-44b0-8217-b70fe642fdac","html_url":"https://github.com/whamtet/simpleui","commit_stats":{"total_commits":132,"total_committers":4,"mean_commits":33.0,"dds":0.05303030303030298,"last_synced_commit":"1fd1eea07835cf76207f25b19291e066d7563bc1"},"previous_names":["whamtet/simpleui","whamtet/ctmx"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whamtet%2Fsimpleui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whamtet%2Fsimpleui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whamtet%2Fsimpleui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whamtet%2Fsimpleui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/whamtet","download_url":"https://codeload.github.com/whamtet/simpleui/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225004688,"owners_count":17405658,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["htmx","simpleui"],"created_at":"2024-08-03T17:00:32.210Z","updated_at":"2024-11-17T05:31:42.918Z","avatar_url":"https://github.com/whamtet.png","language":"Clojure","funding_links":[],"categories":["Tools"],"sub_categories":[],"readme":"# SimpleUI\n\nClojure backend for [htmx](https://htmx.org/).  Previously known as ctmx.\n\n\u003c!-- TOC start (generated with https://github.com/derlin/bitdowntoc) --\u003e\n\n- [Rationale](#rationale)\n- [Getting started](#getting-started)\n- [Usage](#usage)\n  * [Authentication, IAM](#authentication-iam)\n  * [Parameter Casting](#parameter-casting)\n  * [Additional Parameters](#additional-parameters)\n  * [Commands](#commands)\n  * [top-level?](#top-level)\n  * [Updating multiple components](#updating-multiple-components)\n  * [Responses](#responses)\n  * [Updating Session](#updating-session)\n  * [Script Responses](#script-responses)\n  * [Unsafe HTML](#unsafe-html)\n  * [Hanging Components](#hanging-components)\n  * [si-set, si-clear](#si-set-si-clear)\n  * [Using SimpleUI from a CDN](#using-simpleui-from-a-cdn)\n  * [Extra hints](#extra-hints)\n- [Advanced Usage](#advanced-usage)\n- [Pros and Cons of SimpleUI](#pros-and-cons-of-simpleui)\n- [Testing](#testing)\n- [Contributing](#contributing)\n- [License](#license)\n\n\u003c!-- TOC end --\u003e\n\n\u003c!-- TOC --\u003e\u003ca name=\"rationale\"\u003e\u003c/a\u003e\n## Rationale\n\n[htmx](https://htmx.org/) enables web developers to create powerful webapps without writing any Javascript.  Whenever `hx-*` attributes are included in html the library will update the dom in response to user events.  The architecture is simpler and pages load more quickly than in Javascript-oriented webapps.\n\nSimpleUI is a backend accompaniment which makes htmx even easier to use.  It works in conjunction with [hiccup](https://weavejester.github.io/hiccup/) for rendering and [reitit](https://cljdoc.org/d/metosin/reitit/0.5.10/doc/introduction) for routing.\n\n\u003c!-- TOC --\u003e\u003ca name=\"getting-started\"\u003e\u003c/a\u003e\n## Getting started\n\nAdd the following dependency to your `deps.edn` file:\n\n    io.simpleui/simpleui {:mvn/version \"1.6.0\"}\n\nOr to your Leiningen `project.clj` file:\n\n    [io.simpleui/simpleui \"1.6.0\"]\n\nGetting started is easy with clojure tools and the excellent [kit](https://kit-clj.github.io) framework.\n\n```bash\nclojure -Ttools install com.github.seancorfield/clj-new '{:git/tag \"v1.2.404\"}' :as new\nclojure -Tnew create :template io.github.kit-clj :name yourname/guestbook\ncd guestbook\nmake repl\n```\n\n```clojure\n(kit/sync-modules)\n(kit/install-module :kit/simpleui)\n```\n\nQuit the process, `make repl` then\n\n```clojure\n(go)\n```\n\nVisit [localhost:3000](http://localhost:3000).  To reload changes\n\n```clojure\n(reset)\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"usage\"\u003e\u003c/a\u003e\n## Usage\n\nFirst require the library\n\n```clojure\n(require '[simpleui.core :refer :all])\n```\n\nThe core of SimpleUI is the `defcomponent` macro.\n\n```clojure\n(defcomponent ^:endpoint hello [req my-name]\n  [:div#hello \"Hello \" my-name])\n```\n\nThis defines an ordinary function which also expands to an endpoint `/hello`.\n\nTo use our endpoint we call `make-routes`\n\n```clojure\n;; make-routes generates a reitit handler with the root page at /demo\n;; and all subcomponents on their own routes\n(make-routes\n  \"/demo\"\n  (fn [req]\n    (page ;; page renders the rest of the page, htmx script etc\n      [:div\n       [:label \"What is your name?\"]\n       [:input {:name \"my-name\" :hx-patch \"hello\" :hx-target \"#hello\"}]\n       (hello req \"\")])))\n```\n\n![](screenshot.png)\n\nHere the only active element is the text input.  On the input's default action (blur) it will request to `/hello` and replace `#hello` with the server response.  We are using `hello` both as a function and an endpoint.  When called as an endpoint arguments are set based on the http parameter `my-name`.\n\n**The first argument to defcomponent is always the req object**\n\n\u003c!-- TOC --\u003e\u003ca name=\"authentication-iam\"\u003e\u003c/a\u003e\n### Authentication, IAM\n\nYou may check a user's permissions inside the component, however for page level checks remember that `make-routes` is just generating reitit vectors\n\n```clojure\n(make-routes\n  \"/demo\"\n  (fn [req] ...))\n\n;; returns\n;; [\"/demo\"\n;;   [\"/my-component1\" my-component1]\n;;   [\"/my-component2\" my-component2]\n;; ...]\n```\n\nYou can attach page level checks using [standard Reitit techniques](https://github.com/metosin/reitit).\n\n\u003c!-- TOC --\u003e\u003ca name=\"parameter-casting\"\u003e\u003c/a\u003e\n### Parameter Casting\n\nhtmx submits all parameters as strings.  It can be convenient to cast parameters to the required type\n\n```clojure\n(defcomponent my-component [req ^:long int-argument ^:boolean boolean-argument] ...)\n```\n\nCasts available include the following\n\n- **^:long** Casts to long\n- **^:long-option** Casts to long (ignores empty string)\n- **^:double** Casts to double\n- **^:double-option** Casts to double (ignores empty string)\n- **^:longs** Casts to array of longs\n- **^:doubles** Casts to array of doubles\n- **^:array** Puts into an array\n- **^:set** Puts into a set\n- **^:boolean** True when `(contains? #{\"true\" \"on\"} argument)`.  Useful with checkboxes.\n- **^:boolean-true** True when `(not= argument \"false\")`\n- **^:edn** Reads string into edn\n- **^:keyword** Casts to keyword\n- **^:nullable** Ensures the strings \"\", \"nil\" and \"null\" are parsed as nil\n- **^:trim** Trims string and sets it to nil when empty\n- **^:json** Parses json\n- **^:prompt** Takes value from `hx-prompt` header\n\n\u003c!-- TOC --\u003e\u003ca name=\"additional-parameters\"\u003e\u003c/a\u003e\n### Additional Parameters\n\nIn most cases htmx will supply all required parameters.  If you need to include extra ones, set the `hx-vals` attribute.  To serialize the map as json on initial render walk the body with `simpleui.render/walk-attrs` ([example](https://github.com/whamtet/simpleui/blob/main/demo/src/clj/demo/middleware/formats.clj#L32)).\n\n```clojure\n[:button.delete\n  {:hx-delete \"trash-can\"\n   :hx-vals {:hard-delete true}}\n   \"Delete\"]\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"commands\"\u003e\u003c/a\u003e\n### Commands\n\nCommands provide a shorthand to indicate custom actions.\n\n```clojure\n(defcomponent ^:endpoint component [req command]\n  (case command\n    \"print\" (print req)\n    \"save\" (save req)\n    nil)\n  [:div\n    [:button {:hx-post \"component:print\"} \"Print\"]\n    [:button {:hx-post \"component:save\"} \"Save\"]])\n```\n\n`command` will be bound to the value after the colon in any endpoints.\n\n\u003c!-- TOC --\u003e\u003ca name=\"top-level\"\u003e\u003c/a\u003e\n### top-level?\n\nSimpleUI sets `top-level?` true when a component is being invoked as an endpoint.\n\n```clojure\n(defcomponent ^:endpoint my-component [req]\n  (if top-level?\n    [:div \"This is an update\"]\n    [:div \"This is the original render\"]))\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"updating-multiple-components\"\u003e\u003c/a\u003e\n### Updating multiple components\n\nWhen you return multiple components as a list, SimpleUI will set [hx-swap-oob](https://htmx.org/attributes/hx-swap-oob/) on all but the last.  Those elements will be swapped in by id at various points on the page.\n\n```clojure\n(defcomponent my-component [req]\n  (list\n   ;; update these as well\n   [:div#title ...]\n   [:div#sidebar ...]\n   ;; main element\n   [:div.main-element {:id id} ...]))\n```\n\nBe careful to only include `hx-swap-oob` elements when `top-level?` is true.\n\n\u003c!-- TOC --\u003e\u003ca name=\"responses\"\u003e\u003c/a\u003e\n### Responses\n\nBy default SimpleUI expects components to return hiccup vectors which are rendered into html.\n\n`nil` returns http **204 - No Content** and htmx will not update the dom.\n\nYou may also return an explicit ring map if you wish.  A common use case is to refresh the page after an operation is complete\n\n```clojure\n(defcomponent ^:endpoint my-component [req]\n  (case (:request-method req)\n    :post\n    (do\n      (save-to-db ...)\n      simpleui.response/hx-refresh)\n    :get ...))\n```\n\n`simpleui.response/hx-refresh` sets the \"HX-Refresh\" header to \"true\" and htmx will refresh the page.\n\n\u003c!-- TOC --\u003e\u003ca name=\"updating-session\"\u003e\u003c/a\u003e\n### Updating Session\n\nWhen a component returns a response map without a `body` key SimpleUI assumes it is a session update and wraps the response in **204 - No Content**.\n\n```clojure\n(defcomponent ^:endpoint my-component [req shopping-item]\n  (update session :cart conj shopping-item))\n```\n\nThe response won't update anything on the page, but the session will be updated.\n\n\u003c!-- TOC --\u003e\u003ca name=\"script-responses\"\u003e\u003c/a\u003e\n### Script Responses\n\nhtmx will execute any script tags you include.\n\n```clojure\n[:script \"alert('Application successful')\"]\n```\n\nYou can also mix scripts with visual content.  Once you're inside Javascript you can invoke SimpleUI with the HTMX commands [ajax](https://htmx.org/api/#ajax) and [trigger](https://htmx.org/api/#trigger).\n\n\u003c!-- TOC --\u003e\u003ca name=\"unsafe-html\"\u003e\u003c/a\u003e\n### Unsafe HTML\n\nThe default hiccup rendering mode blocks HTML strings from being inserted into the DOM.  If you need this disable render-safe\n\n```clojure\n(simpleui.config/set-render-safe false)\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"hanging-components\"\u003e\u003c/a\u003e\n### Hanging Components\n\nIf you don't include components in an initial render, reference them as symbols so they are still available as endpoints.\n\n```clojure\n(defcomponent ^:endpoint next-month [req] [:p \"next-month\"])\n(defcomponent ^:endpoint previous-month [req] [:p \"previous-month\"])\n\n(defcomponent ^:endpoint calendar [req]\n              next-month\n              previous-month\n              [:div#calendar ...])\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"si-set-si-clear\"\u003e\u003c/a\u003e\n### si-set, si-clear\n\nSimpleUI contains complex state in forms.  On wizards and multistep forms some elements may disappear while we still wish to retain the state.\nTo handle this situation create a 'stack' of hidden elements on initial page render\n\n```clojure\n[:input#first-name {:type \"hidden\"}]\n[:input#second-name {:type \"hidden\"}]\n...\n```\n\nWhen you proceed from one form to the next you may push onto the stack\n\n```clojure\n[:button {:hx-post \"next-step\"\n          :si-set [:first-name :second-name]\n          :si-set-class \"my-stack\"}]\n```\n\n`si-set` will [oob-swap](https://htmx.org/attributes/hx-swap-oob/) `first-name` and `second-name` into the hidden `#first-name` and `#second-name` inputs\nand set their class to `my-stack`.  If we wish to return to this step in the wizard pop `first-name` and `second-name` back off the stack.\n\n```clojure\n[:button {:hx-post \"previous-step\"\n          :hx-include \".my-stack\"\n          :si-clear [:first-name :second-name]}]\n```\n\n`hx-include` class selects the `first-name` and `second-name` fields when rendering `previous-step` and `si-clear` clears the stack.  It is important to clear the stack because multiple inputs with the same name become an array which you may not be expecting.\n\n\u003c!-- TOC --\u003e\u003ca name=\"using-simpleui-from-a-cdn\"\u003e\u003c/a\u003e\n### Using SimpleUI from a CDN\n\nYou will find that SimpleUI is already very fast and lightweight compared to JS-oriented frameworks, sometimes it is convenient to push that even further.\nYou can bootstrap into SimpleUI from a CDN page by referencing HTMX and including a hidden element that triggers the backend.\n\n```html\n\u003cdiv hx-get=\"https://my-backend.com\" hx-trigger=\"load\" /\u003e\n\u003cscript src=\"https://unpkg.com/htmx.org@1.9.12\"\u003e\u003c/script\u003e\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"extra-hints\"\u003e\u003c/a\u003e\n### Extra hints\n\nhtmx does not include disabled fields when submitting requests.  If you wish to retain state in this case use the following pattern.\n\n```clojure\n[:input {:type \"text\" :name (path \"input\") :value (value \"input\") :disabled disabled?}]\n(when disabled?\n  [:input {:type \"hidden\" :name (path \"input\") :value (value \"input\")}])\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"advanced-usage\"\u003e\u003c/a\u003e\n## Advanced Usage\n\nSimpleUI makes it possible to build dynamic forms, for details please see [advanced usage](doc/advanced_usage.md).\n\n\u003c!-- TOC --\u003e\u003ca name=\"pros-and-cons-of-simpleui\"\u003e\u003c/a\u003e\n## Pros and Cons of SimpleUI\n\n\nSimpleUI offers two big advantages over JS-oriented frameworks.  You get about a 30% saving on development time due to the simplified architecture.\nNo http client, routing library, state management complexity etc.  Even more importantly for users the bundle size is reduced 90 - 99%.  Initial page load is as little as 2kb, HTMX loads asyncronously while the user is absorbing page content.\n\nThe limitation of both SimpleUI and HTMX occurs when there are complex dependencies between different parts of the page.  If a change in one element triggers updates in one or two others you can [swap in oob](#updating-multiple-components), but once you do\nthis too much you're loading half the page with every state change.  This is the same as bad old plain HTML and you should consider switching to a JS-oriented solution.\n\nIn practice this situation is rare for business apps and even games can be developed using SimpleUI (e.g. [War of the Ring](https://wotr.online)).\n\n\u003c!-- TOC --\u003e\u003ca name=\"testing\"\u003e\u003c/a\u003e\n## Testing\n\n```clojure\nlein auto test\n```\n\nIntegration tests are run with puppeteer against the [demo](demo) subproject.\n\n```clojure\ncd demo\nclj -M:run\n```\n\nIn a separate tab\n\n```clojure\ncd test-integration\nnpm i\nnode index.js\n```\n\n## Contributing\n\nWe are looking for a SimpleUI 'killer app' to help grow the technology.  If you have an idea please contact the [main author](https://github.com/whamtet).\n\n\u003c!-- TOC --\u003e\u003ca name=\"license\"\u003e\u003c/a\u003e\n## License\n\nCopyright © 2024 Matthew Molloy\n\nThis program and the accompanying materials are made available under the\nterms of the Eclipse Public License 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0.\n\nThis Source Code may also be made available under the following Secondary\nLicenses when the conditions for such availability set forth in the Eclipse\nPublic License, v. 2.0 are satisfied: GNU General Public License as published by\nthe Free Software Foundation, either version 2 of the License, or (at your\noption) any later version, with the GNU Classpath Exception which is available\nat https://www.gnu.org/software/classpath/license.html.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/whamtet.github.io%2Fsimpleui%2F","html_url":"https://awesome.ecosyste.ms/projects/whamtet.github.io%2Fsimpleui%2F","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/whamtet.github.io%2Fsimpleui%2F/lists"}