{"id":16014879,"url":"https://github.com/thheller/shadow-graft","last_synced_at":"2025-03-16T07:32:15.966Z","repository":{"id":41141367,"uuid":"508427233","full_name":"thheller/shadow-graft","owner":"thheller","description":null,"archived":false,"fork":false,"pushed_at":"2023-07-13T13:48:21.000Z","size":44,"stargazers_count":57,"open_issues_count":0,"forks_count":0,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-14T07:45:40.450Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thheller.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-06-28T19:20:33.000Z","updated_at":"2024-12-31T08:12:16.000Z","dependencies_parsed_at":"2023-02-16T09:10:34.337Z","dependency_job_id":null,"html_url":"https://github.com/thheller/shadow-graft","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thheller%2Fshadow-graft","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thheller%2Fshadow-graft/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thheller%2Fshadow-graft/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thheller%2Fshadow-graft/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thheller","download_url":"https://codeload.github.com/thheller/shadow-graft/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243806046,"owners_count":20350775,"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":[],"created_at":"2024-10-08T15:05:24.650Z","updated_at":"2025-03-16T07:32:15.602Z","avatar_url":"https://github.com/thheller.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# shadow-graft\n\n[![Clojars Project](https://img.shields.io/clojars/v/com.thheller/shadow-graft.svg)](https://clojars.org/com.thheller/shadow-graft)\n\nshadow-graft facilitates the calling of client-side functions from the server-side generated HTML.\n\nThe \"graft\" name is inspired by a similar function in horticulture.\n\n\u003e Grafting is the act of placing a portion of one plant (bud or scion) into or on a stem, root, or branch of another (\n\u003e stock) in such a way that a union will be formed and the partners will continue to grow. The part of the combination\n\u003e that provides the root is called the stock; the added piece is called the scion.\n\nhttps://www.britannica.com/topic/graft\n\n## Motivation\n\nThe concept is that the server generates the \"root/stock\" HTML tree and leaves markers on specific positions in that tree. The client-side can then implement \"scions\" which are meant to enhance/grow the actual DOM tree. Basically giving the server the ability to call client side functions.\n\nIt provides a good starting point for any [PWA](https://web.dev/progressive-web-apps/), whether you use something like and [Island Architecture](https://jasonformat.com/islands-architecture/) or a full Single Page App.\n\n## How to use\n\n### Client Side\n\n```clojure\n(ns demo.app\n  (:require\n    [shadow.graft :as graft]\n    [cljs.reader :as reader]))\n\n;; these are silly I know ;)\n(defmethod graft/scion \"disappearing-link\" [opts link]\n  (.addEventListener link \"click\"\n    (fn [e]\n      (.preventDefault e)\n      (.remove link))))\n\n(defmethod graft/scion \"just-log-data\" [opts container]\n  (js/console.log \"just-log-data\" opts container))\n\n(defn init []\n  (graft/init reader/read-string))\n```\n\nThe `init` fn should be called by shadow-cljs `:init-fn` in the build config. The first argument is the reader function\nused to parse data generated by the server. We'll use EDN as an example here. You can provide and function you want (\neg. `js/JSON.parse` or transit).\n\n### Server Side\n\nSince there are a variety of ways to manage state on the server I'm going to use the simplest example here. But the server-side parts are meant to be integrated into whatever state mechanism you use (eg. mount, component, integrant,\netc.).\n\nFor demo purposes I'm gonna use a simple var. Since there really is no need to cleanup its state this is fine.\n\n```clojure\n(ns demo.server\n  (:require\n    [hiccup.core :refer (html)]\n    [shadow.graft :as graft]))\n\n;; using EDN as the data format via pr-str, could be anything\n(def graft (graft/start pr-str))\n\n(defn sample-server-component [req]\n  (html\n    [:a {:href \"http://google.com\"} \"google.com\"]\n    (graft \"disappearing-link\" :prev-sibling)\n    ;; or, slightly more verbose\n    (graft/add graft \"disappearing-link\" :prev-sibling)\n    ))\n\n...\n```\n\n`graft` here takes at least two arguments. The id of the scion, which is also the dispatch value used in the client side `graft/scion` multi-method. The second argument specifies which DOM element this function should be targeting. In\nthis case it targets the previous sibling DOM element.\n\nValid values here include\n\n- `:none` - no reference node\n- `:self` - the node created for the placeholder itself\n- `:parent` - the DOM element parent containing the placeholder\n- `:prev-sibling` or `:next-sibling`\n\nThe third argument is the more interesting one. Many things will require passing data to the client and that is where this goes.\n\n```clojure\n(defn sample-hiccup-with-data [req]\n  (html\n    [:div\n     [:h1 \"nonsense example\"]\n     (graft \"just-log-data\" :parent {:hello \"world\"})]))\n```\n\nThe graft on the server-side generates simple script tags, eg.\n\n```html\n\u003cscript type=\"shadow/graft\" data-id=\"just-log-data\" data-ref=\"parent\"\u003e\noptional-encoded-text\n\u003c/script\u003e\n```\n\nThey are not visible and are not further interpreted by browsers until our code looks for them. They just represent data, using the encoding you specified (e.g. `pr-str`).\n\nThis is intentionally simple so that any server can generate this and still hand off data to the client this way. The default implementation assumes a CLJ server but that is by no means necessary. Anything that is capable of generating this kind of script tag is viable.\n\n## A Closer To Real-World Example\n\nIn a typical reagent/re-frame app you'll have something like\n\n```clojure\n(ns graft-example.core\n  (:require\n    [reagent.dom :as rdom]\n    [re-frame.core :as re-frame]\n    ...\n    ))\n\n(defn ^:dev/after-load mount-root []\n  (re-frame/clear-subscription-cache!)\n  (let [root-el (.getElementById js/document \"app\")]\n    (rdom/unmount-component-at-node root-el)\n    (rdom/render [views/main-panel] root-el)))\n\n(defn init []\n  (re-frame/dispatch-sync [::events/initialize-db])\n  (mount-root))\n```\n\nWith a `\u003cdiv id=\"app\"\u003e` generated by the server somehow.\n\nInstead, this now becomes\n\n```clojure\n(ns graft-example.core\n  (:require\n    [reagent.dom :as rdom]\n    [re-frame.core :as re-frame]\n    [shadow.graft :as graft]\n    [cljs.reader :as reader]\n    ...\n    ))\n\n(defmethod graft/scion \"app\"\n  [{:keys [data props] :as opts} root-el]\n  ;; runs once\n  (re-frame/dispatch-sync [::events/initialize-db data])\n\n  ;; runs on init and again for each hot-reload\n  (graft/reloadable\n    (re-frame/clear-subscription-cache!)\n    (rdom/unmount-component-at-node root-el)\n    (rdom/render [views/main-panel props] root-el)))\n\n(defn init []\n  (graft/init reader/read-string))\n```\n\nLooks somewhat similar, but we gained the ability to pass data into our `::events/initialize-db` event and can pass props to the root component.\n\nIt also becomes much easier to add more scions in case you want to go for more of [Island Architecture](https://jasonformat.com/islands-architecture/) type setup and not purely a SPA.\n\nFor example you could add a `\"nav\"` scion, that targets and enhances the HTML generated by the server. And a `\"configure` scion that sets up some shared state for later maybe?\n\n```clojure\n(defmethod graft/scion \"configure\"\n  [{:keys [current-user locale]} _]\n  ...)\n\n(defmethod graft/scion \"nav\"\n  [_ root-el]\n  ...)\n```\n\nOn the server this all looks something like \n\n```clojure\n(ns graft-example.server\n  (:require\n    [hiccup.core :refer (html)]\n    [hiccup.page :refer (html5)]\n    [shadow.graft :as graft]\n    ))\n\n(def graft (graft/start pr-str))\n\n(defn ui-page []\n  (html5 {}\n    [:head\n     [:link {:rel \"preload\" :as \"script\" :href \"/js/main.js\"}]\n     \n     [:title \"My Page\"]\n     (graft \"configure\" :none\n       {:current-user \"thheller\"\n        :locale \"de_DE\"})]\n\n    [:body\n     [:nav\n      [:ul\n       [:li \"Page 1\"]\n       [:li \"Page 2\"]]\n      (graft \"nav\" :parent)]\n\n     [:div\n      (graft \"app\" :parent\n        {:data (get-init-data)\n         :props {:hello \"world\"}})]\n     \n     [:script {:type \"text/javascript\" :src \"/js/main.js\" :defer true}]]))\n```\n\nI simplified the non-graft things a little for brevity. The point is that the server just generates some HTML and leaves some graft markers for later use.\n\nNote that the graft points are all traversed in the DOM (depth-first) order. They all execute when the script `init` fn is called. Since the order is guaranteed the `\"configure\"` scion runs first and all later scions can rely on `configure` having executed first. Of course if that ends up doing something async, you'll need to coordinate that further yourself.\n\n## Notes\n\nIf you have been long around enough in web development you might remember something like `$(\".some-element\").doStuff()` jquery-style plugins. They are similar in nature, but also made suffered the hardcoded id/class problems and made it difficult to pass data as well. It also had the issue of often looking for stuff that wasn't even on the page, just because it was on 1 or 50, and it was easier to just have the script always look them than to modify the script for that one page.\n\nI have used a [similar method](https://code.thheller.com/blog/web/2014/12/20/better-javascript-html-integration.html) exclusively for many years. It was time to create a proper library for this, so I can throw away my old hacky functions and finally have a proper name for the technique.\n\nAlso took the time to make this work with multiple `:modules` and generating the necessary info via a [shadow-cljs](https://github.com/thheller/shadow-cljs) build hook. Docs on that to follow.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthheller%2Fshadow-graft","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthheller%2Fshadow-graft","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthheller%2Fshadow-graft/lists"}