A luxuriously simple and powerful way to make front-ends with DataScript and Reagent in Clojure.
Posh is a ClojureScript / React library that lets you use a single DataScript database to store your app state. Components access the data they need to render by calling DataScript queries with
qor
pulland are only updated when the query changes.
transact!is used within components to change the global state. If you are familiar with Datomic, you will find Posh incredibly easy to use. If not, it's worth learning because of the power and versatility it will give your components.
Posh is now self-contained and can be used with multiple front-ends (see
posh.core), such as Reagent, Rum, or Quiescent. Only Reagent is currently well-supported by Posh, and is the focus of this documentation.
posh.reagentuses Reagent and can be integrated with your current Reagent project. Because it uses a single database to store app state, like Om or re-frame, it is fitting to write large, extensible apps and reusable components, with the added benefit of being much simpler to use and having a more expressive data retrieval and state updating syntax.
Posh is also very fast because the in-component data queries only run when the database is updated with relevant data (found by pattern matching on the tx report).
For example, below is a component that displays a list of a person's age, name, and weight. The component will only re-render when something in the database changed an attribute of the
person-identity:
(defn person [conn person-id] (let [p @(pull conn '[*] person-id)] [:ul [:li (:person/name p)] [:li (:person/age p)] [:li (:person/weight p)]]))
Posh chat room on Gitter: https://gitter.im/mpdairy/posh
I am also currently looking for contract work or employment on a project that uses Posh.
Posh Todo List - A todo list with categories, edit boxes, checkboxes, and multi-stage delete buttons (trashy live demo).
Start a Reagent project and include these dependencies:
[posh "0.5.5"]
Require in Reagent app files:
clj (ns example (:require [reagent.core :as r] [posh.reagent :refer [pull q posh!]] [datascript.core :as d]))
qand
pull. Currently the only option is
:cache :forever, which will keep the query results caches forever, even after the component using that query is un-rendered. (Thanks, metasoarous)
filter-tx,
filter-q, and
filter-pullto
posh.reagent
get-elsenow works with
q, but still no
pullin q.
qwith no
:inargs now works properly
posh.reagentin your ns's instead of
posh.core. This is because Posh 0.5's core is now front-end agnostic and Reagent is just one of the front-ends it will work with (Rum and Quiescent soon to come!)
qtook the
connas the first argument. Now, the
connis placed behind the query, in the args, as in DataScript or Datomic's
q.
pullare exact and for
qare pretty thorough.
qwith
get-elseand
pulldo not currently work in 0.5, though they sort-of worked in the older version. If you need to use those, just keep using the older version until those expressions are supported.
Posh gives you two functions to retrieve data from the database from within Reagent components:
pulland
q. They watch the database's transaction report and only update (re-render) the hosting component when one of the transacted datoms affects the requested data.
(posh! [DataScript conn1] ...)
Sets up the tx-report listener for a conn.
(def conn (d/create-conn))(posh! conn)
New in Posh 0.5, you can
posh!multiple conns together if you intend to ever use them together in a
qquery:
(posh! conn users-conn styles-conn)
(pull [conn] [pull pattern] [entity id])
pullretrieves the data specified in
pull-patternfor the entity with
entity-id.
pullcan be called from within any Reagent component and will re-render the component only when the pulled information has changed.
Posh's
pulloperates just like Datomic / Datascript's
pullexcept it takes a
conninstead of a
db. (See Datomic's pull)
Posh's
pullonly attempts to pull any new data if there has been a transaction of any datoms that have changed the data it is looking at. For example:
(pull conn '[:person/name :person/age] 1234)
Would only do a pull into Datascript if there has been a transaction changing
:person/nameor
:person/agefor entity
1234.
Below is an example that pulls all of the info from the entity with
idwhenever
idis updated and increases its age whenever clicked:
(defn pull-person [id] (let [p @(pull conn '[*] id)] (println "Person: " (:person/name p)) [:div {:on-click #(transact! conn [[:db/add id :person/age (inc (:person/age p))]])} (:person/name p) ": " (:person/age p)]))
(q [query] & args)
qqueries for data from the database according to the datalog rules specified in the query. It must be called within a Reagent component and will only update the component whenever the data it is querying has changed. See Datomic's Queries and Rules for how to do datalog queries.
argsare extra variables, including the conn or conns from which you will be querying, that DataScript's
qlooks for after the
[:find ...]query if the query has an
:inspecification. Note that Posh's
qtakes conns rather than dbs.
Whenever the database has changed,
qwill check the transacted datoms to see if anything relevant to its query has occured. If so,
qruns Datascript's
qand compares the new query to the old. If it is different, the hosting component will update with the new data.
Below is an example of a component that shows a list of people's names who are younger than a certain age. It only attempts to re-query when someone's age changes or a young person's name changes:
(q '[:find [?name ...] :in $ ?old :where [?p :person/age ?age] [(< ?age ?old)] [?p :person/name ?name]] conn old-age)
Currently,
pullis not supported inside
q. It is recommended to query for the eids, manually send them to components with a separate pull for each eid.
Filters allow you to select a subset of the database to be accessed by queries. Filters can be faster because TX datoms must first pass through a filter before passing on to any queries that use that filter. However, the filters currently just use Datascript's
filterfunction and lazily check each queried datom with a pattern matching predicate to see if it passes the filter, so in reality filters might just slow you down. In the future there will be an option to cache the filtered db, which should improve speed of reliant queries.
Filters return a value that can be passed in to queries or other filters in place of the root
conn. They should not be dereffed.
filter-txtakes a poshdb or conn and a list of tx-patterns. The resulting filtered db consists only of datoms that satisfy one of those patterns.
The following filter would make a db of only task and category names.
(defn test-filter-tx [conn] (let [filter0 (p/filter-tx conn '[[_ :task/name] [_ :category/name])] [:div [:p "filter-tx: "(pr-str filter0) (rand-int 999999)] (pr-str @(p/q '[:find ?v :where [_ _ ?v]] filter0))]))
The
qwould return a list of all the task and category names. Because filter datom evaluation is currently lazy, the
qquery would have to check every single entity in the database to see if it passes the filter, and is thus not very efficient.
filter-pullcreates a filtered db consisting of everything touched by the pull query. For example:
(p/filter-pull conn '[{:task/_category [:task/name]}] [:category/name "Hobby"])
This would return a filtered db that consists of the name of every task belonging to the "Hobby" category.
filter-qqueries for entity id's and creates a filtered db consisting of those entities and all their attributes. Although
qand
filter-qcan query from multiple db's/filters, the first argument after the
[:find ... :where...]query is assumed to be the "parent" db.
(p/filter-q '[:find ?task ?cat :in $ ?todo :where [?cat :category/todo ?todo] [?task :task/category ?cat]] conn [:todo/name "Matt's List"])
The above would make a filtered db of all the category and task entities belonging to the todo list named "Matt's List".
You can call filters on filters:
(def hobby-tasks (p/filter-pull conn '[{:task/_category [:task/name]}] [:category/name "Hobby"])) (def hobby-task-names (p/filter-tx hobby-tasks '[[_ :task/name]]))
And soon-to-come you'll be able to use
filter-mergeon multiple filters to
orthem together.
posh.reagent's
transact!takes a conn or a posh filter and transacts to the conn or the root conn of the filter.
(transact! conn [[:db/add 123 :person/name "Jim"]])
(get-posh-atom conn)
The cache of all the queries is stored inside the posh-atom, which is pointed to by the conn. If you want to see or edit things "under the hood", this is where to go. The dereffed posh-atom can be used as the
posh-treein the functions in
posh.core. A wiki will one day explain further.
This component will show the text value for any entity and attrib combo. There is an "edit" button that, when clicked, creates an
:editentity that keeps track of the temporary text typed in the edit box. The "done" button resets the original value of the entity and attrib and deletes the
:editentity. The "cancel" button just deletes the
:editentity.
The state is stored entirely in the database for this solution, so if you were to save the db during the middle of an edit, if you restored it later, you would be in the middle of the edit still.
(defn edit-box [conn edit-id id attr] (let [edit @(p/pull conn [:edit/val] edit-id)] [:span [:input {:type "text" :value (:edit/val edit) :onChange #(p/transact! conn [[:db/add edit-id :edit/val (-> % .-target .-value)]])}] [:button {:onClick #(p/transact! conn [[:db/add id attr (:edit/val edit)] [:db.fn/retractEntity edit-id]])} "Done"] [:button {:onClick #(p/transact! conn [[:db.fn/retractEntity edit-id]])} "Cancel"]]))(defn editable-label [conn id attr] (let [val (attr @(p/pull conn [attr] id)) edit @(p/q '[:find ?edit . :in $ ?id ?attr :where [?edit :edit/id ?id] [?edit :edit/attr ?attr]] conn id attr)] (if-not edit [:span val [:button {:onClick #(new-entity! conn {:edit/id id :edit/val val :edit/attr attr})} "Edit"]] [edit-box conn edit id attr])))
This can be called with any entity and its text attrib, like
[editable-label conn 123 :person/name]or
[editable-label conn 432 :category/title].
As of version 0.5,
posh.coreshould be able to run on Datomic databases and keep track of all queries. It can also generate, for any
qor
pull, the "necessary datoms" needed in a bare database to get the same result for that
qor
pull, which means that the front-end can send its graph of queries to the backend and get back any datoms needed to update its db whenever anything relevant changes.
Datsync is a utility that eventually will do this, though currently it just copies the entire Datomic db over to DataScript.
See our Gitter room for updates: https://gitter.im/mpdairy/posh
Start a Clojure REPL via your normal way --
M-x cider-jack-infor Emacs users.
Start a CLJS REPL via
lein trampoline cljsbuild repl-listen
Files of interest:
Run
lein testfrom project root
Copyright © 2015 Matt Parker
If somebody needs to BSD then sure, it's under that too. Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.