(l/script- :js
{:runtime :basic
:require [[xt.lang.spec-base :as xt]
[xt.lang.common-lib :as k]
[xt.lang.common-repl :as repl]
[xt.lang.common-notify :as notify]
[xt.lang.spec-promise :as promise]
[xt.event.base-listener :as event]
[xt.event.base-box :as box]
[xt.event.base-form :as form]
[xt.event.base-route :as route]
[xt.event.base-model :as model]]})
(fact:global
{:setup [(l/rt:restart)]
:teardown [(l/rt:stop)]})
xt.event
Event, model, route, form, log, and validation layers.
xt.event is a portable event/application layer for boxes, forms, models, routes, listeners, logs, animation, validation, decoration, tasks, and throttling.
1 Motivation
Application state and UI flows need common event structures across runtimes. The event layer gives generated systems a shared model for routing, forms, logs, listeners, animation, and validation.
2 Internal usage
Tests under test-lang/xt/event cover each base and utility namespace. React and substrate examples use these concepts when coordinating routes, models, and UI events.
3 Walkthrough
3.1 Listener containers
xt.event.base-listener provides the shared machinery used by every other event namespace: containers that hold data plus a map of listeners, and functions to add, remove, list, and trigger those listeners.
create a container and manage listeners
^{:refer xt.event.base-listener/blank-container :added "4.0"}
(!.js
(var c (event/blank-container "demo" {:count 0}))
(event/add-listener c
"a1"
"demo"
(fn [id data t meta]
(return data))
nil
nil)
(var before (event/list-listeners c))
(event/remove-listener c "a1")
[before (event/list-listeners c)])
=> [["a1"] []]
trigger listeners with a payload
^{:refer xt.event.base-listener/trigger-listeners :added "4.0"}
(!.js
(var c (event/blank-container "demo" {}))
(var calls [])
(event/add-listener c
"a1"
"demo"
(fn [id data t meta]
(xt/x:arr-push calls data))
nil
nil)
(event/trigger-listeners c {:value 1})
calls)
=> [{"value" 1}]
3.2 Observable boxes
xt.event.base-box wraps a nested data structure in a container. Setting, merging, appending, or deleting values triggers listeners whose path predicates match the changed location.
boxes hold and update nested data
^{:refer xt.event.base-box/make-box :added "4.1"}
^{:refer xt.event.base-box/get-data :added "4.1"}
(!.js
(var b (box/make-box (fn:> {:a 1 :items []})))
(box/merge-data b [] {:b 2})
(box/set-data b ["a"] 3)
(box/append-data b ["items"] "hello")
[(box/get-data b [])
(box/get-data b ["a"])
(box/get-data b ["b"])
(box/get-data b ["items"])])
=> [{"a" 3 "b" 2 "items" ["hello"]}
3
2
["hello"]]
box listeners fire for matching paths
^{:refer xt.event.base-box/add-listener :added "4.1"}
(!.js
(var b (box/make-box (fn:> {:a {:b 1}})))
(var calls [])
(box/add-listener b
"a1"
["a"]
(fn [id data t meta]
(xt/x:arr-push calls (. data ["value"])))
nil)
(box/set-data b ["a" "b"] 2)
calls)
=> [2]
reset a box back to its initial value
^{:refer xt.event.base-box/reset-data :added "4.1"}
(!.js
(var b (box/make-box (fn:> {:a 1})))
(box/set-data b ["b"] 2)
(box/reset-data b)
(box/get-data b []))
=> {"a" 1}
3.3 Forms and validation
xt.event.base-form stores field values and validator guards. Fields can be set individually or in bulk, and validation runs asynchronously so guards can perform remote checks.
forms track field values
^{:refer xt.event.base-form/make-form :added "4.0"}
^{:refer xt.event.base-form/set-field :added "4.0"}
^{:refer xt.event.base-form/toggle-field :added "4.0"}
(!.js
(var f (form/make-form
(fn:> {:email "" :active false})
{:email [] :active []}))
(form/set-field f "email" "[email protected]")
(form/toggle-field f "active")
[(form/get-field f "email")
(form/get-field f "active")
(form/get-data f)])
=> ["[email protected]"
true
{"email" "[email protected]" "active" true}]
validation results update after checking
^{:refer xt.event.base-form/validate-all :added "4.0"}
^{:refer xt.event.base-form/check-all-passed :added "4.0"}
(notify/wait-on :js
(var f (form/make-form
(fn:> {:email ""})
{:email [["required"
{:check (fn [v rec]
(return (and (k/not-nil? v)
(< 0 (xt/x:len v)))))}]]}))
(form/set-field f "email" "[email protected]")
(form/validate-all
f
nil
(fn [ok res]
(repl/notify (form/check-all-passed f)))))
=> true
3.4 Routes and URLs
xt.event.base-route parses URLs into path and parameter trees, lets you update them programmatically, and notifies listeners when the URL, path, or params change.
parse and inspect a route
^{:refer xt.event.base-route/make-route :added "4.0"}
^{:refer xt.event.base-route/get-url :added "4.0"}
^{:refer xt.event.base-route/get-param :added "4.0"}
(!.js
(var r (route/make-route "users/123?tab=profile"))
[(route/get-url r)
(route/get-segment r [])
(route/get-segment r ["users"])
(route/get-param r "tab" nil)])
=> ["users/123?tab=profile"
"users"
"123"
"profile"]
update a route and notify listeners
^{:refer xt.event.base-route/set-url :added "4.0"}
^{:refer xt.event.base-route/add-url-listener :added "4.0"}
(!.js
(var r (route/make-route "hello?auth=sign_in"))
(var calls [])
(route/add-url-listener
r
"url"
(fn [id data t meta]
(xt/x:arr-push calls (. data ["type"])))
nil)
(route/set-url r "hello/world?auth=sign_out" nil)
[(route/get-url r) calls])
=> ["hello/world?auth=sign_out" ["route.url"]]
3.5 Async models
xt.event.base-model wraps a handler pipeline with input/output records, pending and elapsed tracking, and support for sync, remote, and main execution paths.
models expose input and output state
^{:refer xt.event.base-model/create-model :added "4.0"}
^{:refer xt.event.base-model/init-model :added "4.0"}
^{:refer xt.event.base-model/get-success :added "4.0"}
^{:refer xt.event.base-model/is-disabled :added "4.0"}
(!.js
(var m (model/create-model
(fn:> [x] {:value x})
{}
[3]
{:value 0}
nil
nil))
(model/init-model m)
[(model/is-disabled m)
(model/get-success m nil)])
=> [false {"value" 0}]
models can be disabled by input
^{:refer xt.event.base-model/set-input :added "4.0"}
(!.js
(var m (model/create-model
(fn:> [x] {:value x})
{}
[3]
{:value 0}
nil
nil))
(model/set-input m {:data [3]})
(model/is-disabled m))
=> false
run a model pipeline asynchronously
^{:refer xt.event.base-model/pipeline-run :added "4.0"}
(notify/wait-on :js
(var m (model/create-model
(fn [x]
(return (promise/x:with-delay
10
(fn:> [] {:value x}))))
{}
[3]
{:value 0}
nil
nil))
(model/init-model m)
(var [context disabled] (model/pipeline-prep m))
(-> (model/pipeline-run context
false
model/async-fn-promise
nil
nil
nil)
(promise/x:promise-then
(fn [out]
(repl/notify (model/get-current m nil))))))
=> {"value" 3}
3.6 End-to-end: a tiny login flow
Combining a form, validation, and a box gives a concise login flow. The form holds the fields, validation guards enforce constraints, and a box can hold the resulting session state.
validate login fields and store the result
(notify/wait-on :js
(var f (form/make-form
(fn:> {:email "" :password ""})
{:email [["required"
{:check (fn [v rec]
(return (and (k/not-nil? v)
(< 0 (xt/x:len v)))))}]]
:password [["required"
{:check (fn [v rec]
(return (and (k/not-nil? v)
(< 0 (xt/x:len v)))))}]]}))
(form/set-data f {:email "[email protected]" :password "secret"})
(form/validate-all
f
nil
(fn [ok res]
(repl/notify [(form/check-all-passed f)
(form/check-any-errored f)
(form/get-data f)]))))
=> [true false {"email" "[email protected]" "password" "secret"}]