(define _preload-cache (dict)) (define _css-hash "") (define dispatch-trigger-events :effects (mutation io) (fn (el (header-val :as string)) (when header-val (let ((parsed (try-parse-json header-val))) (if parsed (for-each (fn ((key :as string)) (dom-dispatch el key (get parsed key))) (keys parsed)) (for-each (fn ((name :as string)) (let ((trimmed (trim name))) (when (not (empty? trimmed)) (dom-dispatch el trimmed (dict))))) (split header-val ","))))))) (define init-css-tracking :effects (mutation io) (fn () (let ((meta (dom-query "meta[name=\"sx-css-classes\"]"))) (when meta (let ((content (dom-get-attr meta "content"))) (when content (set! _css-hash content))))))) (define execute-request :effects (mutation io) (fn (el (verbInfo :as dict) (extraParams :as dict)) (let ((info (or (get-verb-info el) verbInfo))) (if (nil? info) (promise-resolve nil) (let ((verb (get info "method")) (url (get info "url"))) (if (let ((media (dom-get-attr el "sx-media"))) (and media (not (browser-media-matches? media)))) (promise-resolve nil) (if (let ((confirm-msg (dom-get-attr el "sx-confirm"))) (and confirm-msg (not (browser-confirm confirm-msg)))) (promise-resolve nil) (let ((prompt-msg (dom-get-attr el "sx-prompt")) (prompt-val (if prompt-msg (browser-prompt prompt-msg) nil))) (if (and prompt-msg (nil? prompt-val)) (promise-resolve nil) (if (or (nil? verb) (nil? url) (not (validate-for-request el))) (promise-resolve nil) (do-fetch el verb verb url (if prompt-val (assoc (or extraParams (dict)) "SX-Prompt" prompt-val) extraParams)))))))))))) (define do-fetch :effects (mutation io) (fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict)) (let ((sync (dom-get-attr el "sx-sync"))) (when (= sync "replace") (abort-previous el)) (let ((target-el (resolve-target el))) (when (and target-el (not (identical? el target-el))) (abort-previous-target target-el))) (let ((ctrl (new-abort-controller))) (track-controller el ctrl) (let ((target-el (resolve-target el))) (when target-el (track-controller-target target-el ctrl))) (let ((body-info (build-request-body el method url)) (final-url (get body-info "url")) (body (get body-info "body")) (ct (get body-info "content-type")) (headers (build-request-headers el (loaded-component-names) _css-hash)) (csrf (csrf-token))) (when extraParams (for-each (fn ((k :as string)) (dict-set! headers k (get extraParams k))) (keys extraParams))) (when ct (dict-set! headers "Content-Type" ct)) (when csrf (dict-set! headers "X-CSRFToken" csrf)) (let ((cached (preload-cache-get _preload-cache final-url)) (optimistic-state (apply-optimistic el)) (indicator (show-indicator el)) (disabled-elts (disable-elements el))) (dom-add-class el "sx-request") (dom-set-attr el "aria-busy" "true") (dom-dispatch el "sx:beforeRequest" (dict "url" final-url "method" method)) (fetch-request (dict "url" final-url "method" method "headers" headers "body" body "signal" (controller-signal ctrl) "cross-origin" (cross-origin? final-url) "preloaded" cached) (fn ((resp-ok :as boolean) (status :as number) get-header (text :as string)) (do (clear-loading-state el indicator disabled-elts) (revert-optimistic optimistic-state) (if (not resp-ok) (do (dom-dispatch el "sx:responseError" (dict "status" status "text" text)) (if (and text (> (len text) 0)) (handle-fetch-success el final-url verb extraParams get-header text) (handle-retry el verb method final-url extraParams))) (do (dom-dispatch el "sx:afterRequest" (dict "status" status)) (handle-fetch-success el final-url verb extraParams get-header text))))) (fn (err) (do (clear-loading-state el indicator disabled-elts) (revert-optimistic optimistic-state) (when (not (abort-error? err)) (log-warn (str "sx:fetch error " method " " final-url " — " err)) (dom-dispatch el "sx:requestError" (dict "error" err)))))))))))) (define handle-fetch-success :effects (mutation io) (fn (el (url :as string) (verb :as string) (extraParams :as dict) get-header (text :as string)) (let ((resp-headers (process-response-headers get-header))) (let ((new-hash (get resp-headers "css-hash"))) (when new-hash (set! _css-hash new-hash))) (dispatch-trigger-events el (get resp-headers "trigger")) (process-cache-directives el resp-headers text) (cond (get resp-headers "redirect") (browser-navigate (get resp-headers "redirect")) (get resp-headers "refresh") (browser-reload) (get resp-headers "location") (fetch-location (get resp-headers "location")) :else (let ((target-el (if (get resp-headers "retarget") (dom-query (get resp-headers "retarget")) (resolve-target el))) (swap-spec (parse-swap-spec (or (get resp-headers "reswap") (dom-get-attr el "sx-swap")) (dom-has-class? (dom-body) "sx-transitions"))) (swap-style (get swap-spec "style")) (use-transition (get swap-spec "transition")) (ct (or (get resp-headers "content-type") ""))) (if (contains? ct "text/sx") (handle-sx-response el target-el text swap-style use-transition) (handle-html-response el target-el text swap-style use-transition)) (dispatch-trigger-events el (get resp-headers "trigger-swap")) (handle-history el url resp-headers) (set-timeout (fn () (when (get resp-headers "trigger-settle") (dispatch-trigger-events el (get resp-headers "trigger-settle"))) (process-settle-hooks el)) 20) (dom-dispatch el "sx:afterSwap" (dict "target" target-el "swap" swap-style))))))) (define handle-sx-response :effects (mutation io) (fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean)) (let ((cleaned (strip-component-scripts text))) (let ((final (extract-response-css cleaned))) (let ((trimmed (trim final))) (when (not (empty? trimmed)) (let ((rendered (sx-render trimmed)) (container (dom-create-element "div" nil))) (dom-append container rendered) (process-oob-swaps container (fn (t oob (s :as string)) (dispose-islands-in t) (swap-dom-nodes t (if (= s "innerHTML") (children-to-fragment oob) oob) s) (post-swap t))) (let ((select-sel (dom-get-attr el "sx-select")) (content (if select-sel (select-from-container container select-sel) (children-to-fragment container)))) (dispose-islands-in target) (with-transition use-transition (fn () (let ((swap-result (swap-dom-nodes target content swap-style))) (post-swap (if (= swap-style "outerHTML") (dom-parent (or swap-result target)) (or swap-result target)))))))))))))) (define handle-html-response :effects (mutation io) (fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean)) (let ((doc (dom-parse-html-document text))) (when doc (let ((select-sel (dom-get-attr el "sx-select"))) (dispose-islands-in target) (if select-sel (let ((container (dom-create-element "div" nil))) (dom-set-inner-html container (dom-body-inner-html doc)) (process-oob-swaps container (fn (t oob (s :as string)) (dispose-islands-in t) (swap-dom-nodes t oob s) (post-swap t))) (hoist-head-elements container) (let ((html (select-from-container container select-sel))) (with-transition use-transition (fn () (let ((swap-root (swap-dom-nodes target html swap-style))) (log-info (str "swap-root: " (if swap-root (dom-tag-name swap-root) "nil") " target: " (dom-tag-name target))) (post-swap (or swap-root target))))))) (let ((container (dom-create-element "div" nil))) (dom-set-inner-html container (dom-body-inner-html doc)) (process-oob-swaps container (fn (t oob (s :as string)) (dispose-islands-in t) (swap-dom-nodes t oob s) (post-swap t))) (hoist-head-elements container) (with-transition use-transition (fn () (swap-dom-nodes target (children-to-fragment container) swap-style) (post-swap target)))))))))) (define handle-retry :effects (mutation io) (fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict)) (let ((retry-attr (dom-get-attr el "sx-retry")) (spec (parse-retry-spec retry-attr))) (when spec (let ((current-ms (or (dom-get-attr el "data-sx-retry-ms") (get spec "start-ms")))) (let ((ms (parse-int current-ms (get spec "start-ms")))) (dom-set-attr el "data-sx-retry-ms" (str (next-retry-ms ms (get spec "cap-ms")))) (set-timeout (fn () (do-fetch el verb method url extraParams)) ms))))))) (define bind-triggers :effects (mutation io) (fn (el (verbInfo :as dict)) (let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger")) (default-trigger (dom-tag-name el))))) (for-each (fn ((trigger :as dict)) (let ((kind (classify-trigger trigger)) (mods (get trigger "modifiers"))) (cond (= kind "poll") (set-interval (fn () (execute-request el nil nil)) (get mods "interval")) (= kind "intersect") (observe-intersection el (fn () (execute-request el nil nil)) false (get mods "delay")) (= kind "load") (set-timeout (fn () (execute-request el nil nil)) (or (get mods "delay") 0)) (= kind "revealed") (observe-intersection el (fn () (execute-request el nil nil)) true (get mods "delay")) (= kind "event") (bind-event el (get trigger "event") mods verbInfo)))) triggers)))) (define bind-event :effects (mutation io) (fn (el (event-name :as string) (mods :as dict) (verbInfo :as dict)) (let ((timer nil) (last-val nil) (listen-target (if (get mods "from") (dom-query (get mods "from")) el))) (when listen-target (dom-add-listener listen-target event-name (fn (e) (let ((should-fire true)) (when (get mods "changed") (let ((val (dom-value el))) (if (= val last-val) (set! should-fire false) (set! last-val val)))) (when (and should-fire (not (and (= event-name "click") (event-modifier-key? e)))) (when (or (= event-name "submit") (and (= event-name "click") (dom-has-attr? el "href"))) (prevent-default e)) (let ((live-info (get-verb-info el)) (is-get-link (and (= event-name "click") (= (get live-info "method") "GET") (dom-has-attr? el "href") (not (get mods "delay")))) (client-routed false)) (when is-get-link (set! client-routed (try-client-route (url-pathname (get live-info "url")) (dom-get-attr el "sx-target")))) (if client-routed (do (browser-push-state (get live-info "url")) (browser-scroll-to 0 0)) (do (when is-get-link (log-info (str "sx:route server fetch " (get live-info "url")))) (if (get mods "delay") (do (clear-timeout timer) (set! timer (set-timeout (fn () (execute-request el nil nil)) (get mods "delay")))) (execute-request el nil nil)))))))) (if (get mods "once") (dict "once" true) nil)))))) (define post-swap :effects (mutation io) (fn (root) (log-info (str "post-swap: root=" (if root (dom-tag-name root) "nil"))) (activate-scripts root) (sx-process-scripts root) (sx-hydrate root) (sx-hydrate-islands root) (run-post-render-hooks) (process-elements root))) (define process-settle-hooks :effects (mutation io) (fn (el) (let ((settle-expr (dom-get-attr el "sx-on-settle"))) (when (and settle-expr (not (empty? settle-expr))) (let ((exprs (sx-parse settle-expr))) (for-each (fn (expr) (eval-expr expr (env-extend (dict)))) exprs)))))) (define activate-scripts :effects (mutation io) (fn (root) (when root (let ((scripts (dom-query-all root "script"))) (for-each (fn (dead) (when (and (not (dom-has-attr? dead "data-components")) (not (dom-has-attr? dead "data-sx-activated"))) (let ((live (create-script-clone dead))) (dom-set-attr live "data-sx-activated" "true") (dom-replace-child (dom-parent dead) live dead)))) scripts))))) (define process-oob-swaps :effects (mutation io) (fn (container (swap-fn :as lambda)) (let ((oobs (find-oob-swaps container))) (for-each (fn ((oob :as dict)) (let ((target-id (get oob "target-id")) (target (dom-query-by-id target-id)) (oob-el (get oob "element")) (swap-type (get oob "swap-type"))) (when (dom-parent oob-el) (dom-remove-child (dom-parent oob-el) oob-el)) (when target (swap-fn target oob-el swap-type)))) oobs)))) (define hoist-head-elements :effects (mutation io) (fn (container) (for-each (fn (style) (when (dom-parent style) (dom-remove-child (dom-parent style) style)) (dom-append-to-head style)) (dom-query-all container "style[data-sx-css]")) (for-each (fn (link) (when (dom-parent link) (dom-remove-child (dom-parent link) link)) (dom-append-to-head link)) (dom-query-all container "link[rel=\"stylesheet\"]")))) (define process-boosted :effects (mutation io) (fn (root) (for-each (fn (container) (boost-descendants container)) (dom-query-all (or root (dom-body)) "[sx-boost]")))) (define boost-descendants :effects (mutation io) (fn (container) (let ((boost-target (dom-get-attr container "sx-boost"))) (for-each (fn (link) (when (and (not (is-processed? link "boost")) (should-boost-link? link)) (mark-processed! link "boost") (when (and (not (dom-has-attr? link "sx-target")) boost-target (not (= boost-target "true"))) (dom-set-attr link "sx-target" boost-target)) (when (not (dom-has-attr? link "sx-swap")) (dom-set-attr link "sx-swap" "innerHTML")) (when (not (dom-has-attr? link "sx-push-url")) (dom-set-attr link "sx-push-url" "true")) (bind-client-route-link link (dom-get-attr link "href")))) (dom-query-all container "a[href]")) (for-each (fn (form) (when (and (not (is-processed? form "boost")) (should-boost-form? form)) (mark-processed! form "boost") (let ((method (upper (or (dom-get-attr form "method") "GET"))) (action (or (dom-get-attr form "action") (browser-location-href)))) (when (and (not (dom-has-attr? form "sx-target")) boost-target (not (= boost-target "true"))) (dom-set-attr form "sx-target" boost-target)) (when (not (dom-has-attr? form "sx-swap")) (dom-set-attr form "sx-swap" "innerHTML")) (bind-boost-form form method action)))) (dom-query-all container "form"))))) (define _page-data-cache (dict)) (define _page-data-cache-ttl 30000) (define page-data-cache-key :effects () (fn ((page-name :as string) (params :as dict)) (let ((base page-name)) (if (or (nil? params) (empty? (keys params))) base (let ((parts (list))) (for-each (fn ((k :as string)) (append! parts (str k "=" (get params k)))) (keys params)) (str base ":" (join "&" parts))))))) (define page-data-cache-get :effects (mutation io) (fn ((cache-key :as string)) (let ((entry (get _page-data-cache cache-key))) (if (nil? entry) nil (if (> (- (now-ms) (get entry "ts")) _page-data-cache-ttl) (do (dict-set! _page-data-cache cache-key nil) nil) (get entry "data")))))) (define page-data-cache-set :effects (mutation io) (fn ((cache-key :as string) data) (dict-set! _page-data-cache cache-key {:data data :ts (now-ms)}))) (define invalidate-page-cache :effects (mutation io) (fn ((page-name :as string)) (for-each (fn ((k :as string)) (when (or (= k page-name) (starts-with? k (str page-name ":"))) (dict-set! _page-data-cache k nil))) (keys _page-data-cache)) (sw-post-message {:type "invalidate" :page page-name}) (log-info (str "sx:cache invalidate " page-name)))) (define invalidate-all-page-cache :effects (mutation io) (fn () (set! _page-data-cache (dict)) (sw-post-message {:type "invalidate" :page "*"}) (log-info "sx:cache invalidate *"))) (define update-page-cache :effects (mutation io) (fn ((page-name :as string) data) (let ((cache-key (page-data-cache-key page-name (dict)))) (page-data-cache-set cache-key data) (log-info (str "sx:cache update " page-name))))) (define process-cache-directives :effects (mutation io) (fn (el (resp-headers :as dict) (response-text :as string)) (let ((el-invalidate (dom-get-attr el "sx-cache-invalidate"))) (when el-invalidate (if (= el-invalidate "*") (invalidate-all-page-cache) (invalidate-page-cache el-invalidate)))) (let ((hdr-invalidate (get resp-headers "cache-invalidate"))) (when hdr-invalidate (if (= hdr-invalidate "*") (invalidate-all-page-cache) (invalidate-page-cache hdr-invalidate)))) (let ((hdr-update (get resp-headers "cache-update"))) (when hdr-update (let ((data (parse-sx-data response-text))) (when data (update-page-cache hdr-update data))))))) (define _optimistic-snapshots (dict)) (define optimistic-cache-update :effects (mutation) (fn ((cache-key :as string) (mutator :as lambda)) (let ((cached (page-data-cache-get cache-key))) (when cached (let ((predicted (mutator cached))) (dict-set! _optimistic-snapshots cache-key cached) (page-data-cache-set cache-key predicted) predicted))))) (define optimistic-cache-revert :effects (mutation) (fn ((cache-key :as string)) (let ((snapshot (get _optimistic-snapshots cache-key))) (when snapshot (page-data-cache-set cache-key snapshot) (dict-delete! _optimistic-snapshots cache-key) snapshot)))) (define optimistic-cache-confirm :effects (mutation) (fn ((cache-key :as string)) (dict-delete! _optimistic-snapshots cache-key))) (define submit-mutation :effects (mutation io) (fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda)) (let ((cache-key (page-data-cache-key page-name params)) (predicted (optimistic-cache-update cache-key mutator-fn))) (when predicted (try-rerender-page page-name params predicted)) (execute-action action-name payload (fn (result) (when result (page-data-cache-set cache-key result)) (optimistic-cache-confirm cache-key) (when result (try-rerender-page page-name params result)) (log-info (str "sx:optimistic confirmed " page-name)) (when on-complete (on-complete "confirmed"))) (fn ((error :as string)) (let ((reverted (optimistic-cache-revert cache-key))) (when reverted (try-rerender-page page-name params reverted)) (log-warn (str "sx:optimistic reverted " page-name ": " error)) (when on-complete (on-complete "reverted")))))))) (define _is-online true) (define _offline-queue (list)) (define offline-is-online? :effects (io) (fn () _is-online)) (define offline-set-online! :effects (mutation) (fn ((val :as boolean)) (set! _is-online val))) (define offline-queue-mutation :effects (mutation io) (fn ((action-name :as string) payload (page-name :as string) (params :as dict) (mutator-fn :as lambda)) (let ((cache-key (page-data-cache-key page-name params)) (entry (dict "action" action-name "payload" payload "page" page-name "params" params "timestamp" (now-ms) "status" "pending"))) (append! _offline-queue entry) (let ((predicted (optimistic-cache-update cache-key mutator-fn))) (when predicted (try-rerender-page page-name params predicted))) (log-info (str "sx:offline queued " action-name " (" (len _offline-queue) " pending)")) entry))) (define offline-sync :effects (mutation io) (fn () (let ((pending (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue))) (when (not (empty? pending)) (log-info (str "sx:offline syncing " (len pending) " mutations")) (for-each (fn ((entry :as dict)) (execute-action (get entry "action") (get entry "payload") (fn (result) (dict-set! entry "status" "synced") (log-info (str "sx:offline synced " (get entry "action")))) (fn ((error :as string)) (dict-set! entry "status" "failed") (log-warn (str "sx:offline sync failed " (get entry "action") ": " error))))) pending))))) (define offline-pending-count :effects (io) (fn () (len (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue)))) (define offline-aware-mutation :effects (mutation io) (fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda)) (if _is-online (submit-mutation page-name params action-name payload mutator-fn on-complete) (do (offline-queue-mutation action-name payload page-name params mutator-fn) (when on-complete (on-complete "queued")))))) (define current-page-layout :effects (io) (fn () (let ((pathname (url-pathname (browser-location-href))) (match (find-matching-route pathname _page-routes))) (if (nil? match) "" (or (get match "layout") ""))))) (define swap-rendered-content :effects (mutation io) (fn (target rendered (pathname :as string)) (do (dispose-islands-in target) (dom-set-text-content target "") (dom-append target rendered) (hoist-head-elements-full target) (process-elements target) (sx-hydrate-elements target) (sx-hydrate-islands target) (run-post-render-hooks) (dom-dispatch target "sx:clientRoute" (dict "pathname" pathname)) (log-info (str "sx:route client " pathname))))) (define resolve-route-target :effects (io) (fn ((target-sel :as string)) (if (and target-sel (not (= target-sel "true"))) (dom-query target-sel) nil))) (define deps-satisfied? :effects (io) (fn ((match :as dict)) (let ((deps (get match "deps")) (loaded (loaded-component-names))) (if (or (nil? deps) (empty? deps)) true (every? (fn ((dep :as string)) (contains? loaded dep)) deps))))) (define try-client-route :effects (mutation io) (fn ((pathname :as string) (target-sel :as string)) (let ((match (find-matching-route pathname _page-routes))) (if (nil? match) (do (log-info (str "sx:route no match (" (len _page-routes) " routes) " pathname)) false) (let ((target-layout (or (get match "layout") "")) (cur-layout (current-page-layout))) (if (not (= target-layout cur-layout)) (do (log-info (str "sx:route server (layout: " cur-layout " -> " target-layout ") " pathname)) false) (let ((content-src (get match "content")) (closure (or (get match "closure") {})) (params (get match "params")) (page-name (get match "name"))) (if (or (nil? content-src) (empty? content-src)) (do (log-warn (str "sx:route no content for " pathname)) false) (let ((target (resolve-route-target target-sel))) (if (nil? target) (do (log-warn (str "sx:route target not found: " target-sel)) false) (if (not (deps-satisfied? match)) (do (log-info (str "sx:route deps miss for " page-name)) false) (let ((io-deps (get match "io-deps")) (has-io (and io-deps (not (empty? io-deps)))) (render-plan (get match "render-plan"))) (when render-plan (let ((srv (or (get render-plan "server") (list))) (cli (or (get render-plan "client") (list)))) (log-info (str "sx:route plan " page-name " — " (len srv) " server, " (len cli) " client")))) (when has-io (register-io-deps io-deps)) (if (get match "stream") (do (log-info (str "sx:route streaming " pathname)) (fetch-streaming target pathname (build-request-headers target (loaded-component-names) _css-hash)) true) (if (get match "has-data") (let ((cache-key (page-data-cache-key page-name params)) (cached (page-data-cache-get cache-key))) (if cached (let ((env (merge closure params cached))) (if has-io (do (log-info (str "sx:route client+cache+async " pathname)) (try-async-eval-content content-src env (fn (rendered) (if (nil? rendered) (do (log-warn (str "sx:route cache+async eval failed for " pathname " — server fallback")) (fetch-and-restore target pathname (build-request-headers target (loaded-component-names) _css-hash) 0)) (swap-rendered-content target rendered pathname)))) true) (let ((rendered (try-eval-content content-src env))) (if (nil? rendered) (do (log-warn (str "sx:route cached eval failed for " pathname)) false) (do (log-info (str "sx:route client+cache " pathname)) (swap-rendered-content target rendered pathname) true))))) (do (log-info (str "sx:route client+data " pathname)) (resolve-page-data page-name params (fn ((data :as dict)) (page-data-cache-set cache-key data) (let ((env (merge closure params data))) (if has-io (try-async-eval-content content-src env (fn (rendered) (if (nil? rendered) (do (log-warn (str "sx:route data+async eval failed for " pathname " — server fallback")) (fetch-and-restore target pathname (build-request-headers target (loaded-component-names) _css-hash) 0)) (swap-rendered-content target rendered pathname)))) (let ((rendered (try-eval-content content-src env))) (if (nil? rendered) (do (log-warn (str "sx:route data eval failed for " pathname " — server fallback")) (fetch-and-restore target pathname (build-request-headers target (loaded-component-names) _css-hash) 0)) (swap-rendered-content target rendered pathname))))))) true))) (if has-io (do (log-info (str "sx:route client+async " pathname)) (try-async-eval-content content-src (merge closure params) (fn (rendered) (if (nil? rendered) (do (log-warn (str "sx:route async eval failed for " pathname " — server fallback")) (fetch-and-restore target pathname (build-request-headers target (loaded-component-names) _css-hash) 0)) (swap-rendered-content target rendered pathname)))) true) (let ((env (merge closure params)) (rendered (try-eval-content content-src env))) (if (nil? rendered) (do (log-info (str "sx:route server (eval failed) " pathname)) false) (do (swap-rendered-content target rendered pathname) true)))))))))))))))))) (define bind-client-route-link :effects (mutation io) (fn (link (href :as string)) (bind-client-route-click link href (fn () (bind-boost-link link href))))) (define process-sse :effects (mutation io) (fn (root) (for-each (fn (el) (when (not (is-processed? el "sse")) (mark-processed! el "sse") (bind-sse el))) (dom-query-all (or root (dom-body)) "[sx-sse]")))) (define bind-sse :effects (mutation io) (fn (el) (let ((url (dom-get-attr el "sx-sse"))) (when url (let ((source (event-source-connect url el)) (event-name (parse-sse-swap el))) (event-source-listen source event-name (fn ((data :as string)) (bind-sse-swap el data)))))))) (define bind-sse-swap :effects (mutation io) (fn (el (data :as string)) (let ((target (resolve-target el)) (swap-spec (parse-swap-spec (dom-get-attr el "sx-swap") (dom-has-class? (dom-body) "sx-transitions"))) (swap-style (get swap-spec "style")) (use-transition (get swap-spec "transition")) (trimmed (trim data))) (when (not (empty? trimmed)) (dispose-islands-in target) (if (starts-with? trimmed "(") (let ((rendered (sx-render trimmed)) (container (dom-create-element "div" nil))) (dom-append container rendered) (with-transition use-transition (fn () (swap-dom-nodes target (children-to-fragment container) swap-style) (post-swap target)))) (with-transition use-transition (fn () (swap-html-string target trimmed swap-style) (post-swap target)))))))) (define bind-inline-handlers :effects (mutation io) (fn (root) (for-each (fn (el) (for-each (fn ((attr :as list)) (let ((name (first attr)) (body (nth attr 1))) (when (starts-with? name "sx-on:") (let ((event-name (slice name 6))) (when (not (is-processed? el (str "on:" event-name))) (mark-processed! el (str "on:" event-name)) (let ((exprs (sx-parse body))) (dom-on el event-name (fn (e) (let ((handler-env (env-extend (dict)))) (env-bind! handler-env "event" e) (env-bind! handler-env "this" el) (env-bind! handler-env "detail" (event-detail e)) (for-each (fn (expr) (eval-expr expr handler-env)) exprs)))))))))) (dom-attr-list el))) (dom-query-all (or root (dom-body)) "[sx-on\\:]")))) (define bind-preload-for :effects (mutation io) (fn (el) (let ((preload-attr (dom-get-attr el "sx-preload"))) (when preload-attr (let ((events (if (= preload-attr "mousedown") (list "mousedown" "touchstart") (list "mouseover"))) (debounce-ms (if (= preload-attr "mousedown") 0 100))) (bind-preload el events debounce-ms (fn () (let ((info (get-verb-info el))) (when info (do-preload (get info "url") (build-request-headers el (loaded-component-names) _css-hash))))))))))) (define do-preload :effects (mutation io) (fn ((url :as string) (headers :as dict)) (when (nil? (preload-cache-get _preload-cache url)) (fetch-preload url headers _preload-cache)))) (define VERB_SELECTOR (str "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]")) (define process-elements :effects (mutation io) (fn (root) (let ((els (dom-query-all (or root (dom-body)) VERB_SELECTOR))) (for-each (fn (el) (when (not (is-processed? el "verb")) (mark-processed! el "verb") (process-one el))) els)) (process-boosted root) (process-sse root) (bind-inline-handlers root) (process-emit-elements root))) (define process-one :effects (mutation io) (fn (el) (let ((verb-info (get-verb-info el))) (when verb-info (when (not (dom-has-attr? el "sx-disable")) (bind-triggers el verb-info) (bind-preload-for el)))))) (define process-emit-elements :effects (mutation io) (fn (root) (let ((els (dom-query-all (or root (dom-body)) "[data-sx-emit]"))) (for-each (fn (el) (when (not (is-processed? el "emit")) (mark-processed! el "emit") (let ((event-name (dom-get-attr el "data-sx-emit"))) (when event-name (dom-on el "click" (fn (e) (let ((detail-json (dom-get-attr el "data-sx-emit-detail")) (detail (if detail-json (json-parse detail-json) (dict)))) (dom-dispatch el event-name detail)))))))) els)))) (define handle-popstate :effects (mutation io) (fn ((scrollY :as number)) (let ((url (browser-location-href)) (boost-el (dom-query "[sx-boost]")) (target-sel (if boost-el (let ((attr (dom-get-attr boost-el "sx-boost"))) (if (and attr (not (= attr "true"))) attr nil)) nil)) (target-sel (or target-sel "#main-panel")) (target (dom-query target-sel)) (pathname (url-pathname url))) (when target (if (try-client-route pathname target-sel) (browser-scroll-to 0 scrollY) (let ((headers (build-request-headers target (loaded-component-names) _css-hash))) (fetch-and-restore target url headers scrollY))))))) (define engine-init :effects (mutation io) (fn () (do (init-css-tracking) (sx-process-scripts nil) (sx-hydrate nil) (process-elements nil))))