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 q
or
pull
and 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.reagent
uses 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-id
entity:
(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
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:
Require in Reagent app files:
(ns example
(:require [reagent.core :as r]
[posh.reagent :refer [pull q posh!]]
[datascript.core :as d]))
q
and 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-pull
to posh.reagent
get-else
now works with q
, but still no pull
in q.q
with no :in
args now works properlyposh.reagent
in 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!)q
took the conn
as the first argument. Now,
the conn
is placed behind the query, in the args, as in DataScript
or Datomic's q
.pull
are exact and for q
are pretty thorough.q
with get-else
and pull
do 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: pull
and 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 q
query:
(posh! conn users-conn styles-conn)
(pull [conn] [pull pattern] [entity id])
pull
retrieves the data specified in pull-pattern
for the entity
with entity-id
. pull
can be called from within any Reagent
component and will re-render the component only when the pulled
information has changed.
Posh's pull
operates just like Datomic / Datascript's pull
except it takes a
conn
instead of a db
. (See
Datomic's pull)
Posh's pull
only 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/name
or :person/age
for entity 1234
.
Below is an example that pulls all of the info from the entity with id
whenever id
is 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)
q
queries 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. args
are extra variables, including the
conn or conns from which you will be querying, that DataScript's q
looks for
after the [:find ...]
query if the query has an :in
specification.
Note that Posh's q
takes conns rather than dbs.
Whenever the database has changed, q
will check the transacted
datoms to see if anything relevant to its query has occured. If so,
q
runs Datascript's q
and 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, pull
is 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 filter
function 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-tx
takes 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 q
would return a list of all the task and category names.
Because filter datom evaluation is currently lazy, the q
query would have to
check every single entity in the database to see if it passes the
filter, and is thus not very efficient.
filter-pull
creates 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-q
queries for entity id's and creates a filtered db
consisting of those entities and all their attributes. Although q
and filter-q
can 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-merge
on multiple
filters to or
them 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-tree
in 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 :edit
entity 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 :edit
entity. The
"cancel" button just deletes the :edit
entity.
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.core
should be able to run on Datomic
databases and keep track of all queries. It can also generate, for any
q
or pull
, the "necessary datoms" needed in a bare database to get
the same result for that q
or 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-in
for Emacs users.
Start a CLJS REPL via lein trampoline cljsbuild repl-listen
Files of interest:
Run lein kaocha
from project root
Copyright © 2019 Denis Krivosheev
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.