-
Notifications
You must be signed in to change notification settings - Fork 10
Differences from Om Next
Besides using a different graph query language, qlkit is a brand new codebase that adopts similar design principles as Om Next. However, here are some differences that are worth noting.
qlkit-renderer
(if you choose to use our rendering library) returns HTML in the form as an edn data structure, similar to clojure hiccup:
[:div [:button {:label "Submit"}]]
Qlkit also elides nil children and expands seqs just like hiccup, so the following two expressions are equivalent:
[:div (list [:button {:label "Submit"}] [:button {:label "Cancel"}]) nil]
[:div [:button {:label "Submit"}] [:button {:label "Cancel"}]]
(Note that qlkit does not support the id
and class
declaration sugar of hiccup, since these attributes are deemphasized in modern virtual-dom based web development)
qlkit-material-ui
automatically substitutes common controls such as button
s and input
s with modernized versions of those controls as made available by the material-ui library. The full list of substitutions can be seen in qlkit.qlkit/component-registry
and can be overridden via qlkit.qlkit/register-component
.
One additional convenience feature in Qlkit is that html styles can be specified directly at the attribute level, so these two expressions are equivalent:
[:button {:margin "2rem" :label "Submit"}]
[:button {:style {margin "2rem"} :label "Submit"}]
Qlkit accomplishes this by recognizing all legal style
attributes and automatically moving those into the style declaration- So don't define attributes for your custom components that mirror legal CSS attributes (i.e. avoid attribute names like color
or left
or width
, which probably are bad names for custom attributes anyway, precisely because they can be confused for CSS attributes.) If you absolutely HAVE TO refer to a mirroring attribute, simply declare the attribute as a string and it will not be treated as a CSS attribute (i.e. use "width"
instead of :width
)
(One other small feature: keyword attribute values are turned into strings: i.e. you can write {:color :red}
instead of {:color "red"}
)
As discussed, the output of a renderer should be a pure edn data structure, which is desirable for easy unit testing. The only exception is DOM event handlers, which are declared as functions. When a DOM element keyword is namespaced it is assumed to be a user-declared component via the ql/defcomponent
macro: This means that a keyword like :foo/Bar
is assumed to be a user-defined component- Note that the ql/component
macro also will define a symbol Bar
and set it equal to :foo/Bar
, for convenience.
Qlkit does not distinguish between computed attributes and "props" as OmNext does. Instead, all attributes can be passed to the child, such as [MyComponent {:color "blue" :computed-value 3423}]
. (OmNext distinguishes between these two types of "attributes" for optimization reasons, but qlkit prioritizes simplicity over performance.)
New user-defined components are declared with the component
macro. It differs from OmNext/defui
in the following ways:
- State and query initializations are done by declaring data structures using the
state
andquery
identifiers (OmNext differs by requiring the user to provide functions, not structures) - Component functions are not given a copy of the
this
parameter, as it is usually not needed in qlkit. (In the rare cases where thethis
parameter is needed it can still be accessed viaqlkit.core/*this*
) - The
update-state!
andtransact!
functions in qlkit are always assumed to refer to the current component (which is why thethis
parameter is typically not needed.)
Qlkit supports read
and mutate
parsers, just like OmNext. However, unlike with OmNext, the read
parsers always return a value (as opposed to OmNext, where they return a :remote
/:action
/:value
map) and the mutate
parsers are assumed to perform a side effect without a meaningful return value. If a parser has a remote component (i.e. it needs to query the server or needs to interact with a blockchain) this is NOT done through the read
/mutate
parsers, but is instead done via an additional parser called the remote
parser.
When a parser is called in qlkit it does not receive params or a dispatch key, as in OmNext: Instead, it receives a copy of the query term that triggered the parser- Note that this query will always be normalized in the form [:my/key {:some :params} :child1 :child2]
so it's easy to extract the dispatch-key via (first query-term)
or the params via (second query-term)
or extract the children via (drop 2 query-term)
.
The return value for a remote
parser should be the remote query, which may or may not be different than the local query. (This differs from OmNext where the :remote
declaration may simply be set to true
instead of a full query, which causes OmNext to default to the local query) The usual appropriate design pattern is to make remote queries as simple as possible, so the remote parsers should be written to remove extraneous nodes from the query and strip it down as small as possible (usually by calling additional remote
parsers that also simplify children in the larger query)
Note that a remote
parser has the option of returning >1 query items, which is sometimes necessary for eliding branch nodes from the query. This is accomplished by returning a sequence (which will be spliced into the parent query, exactly as expected from our earlier discussion of the query syntax.)
The last parser type is the sync
parser, which is called when a result is returned by the server-side parsers. The job of a sync
parser is to update the local state atom to reflect data received from the server. Note that all read
queries with a remote
declaration MUST have a sync
declaration (i.e. if you ask for data from the server you must explain what to do when that data is received) or an exception is thrown, but mutate
queries with remote
declarations do not need to have a sync
parser (but are still permitted to optionally provide one).
IMPORTANT: It is usually a bad design to have a sync
parser for a remote mutation- Instead, simply bundle the remote mutation with a read to synchronize the local state, such as (transact! [[example/set-number! 22] [example/number]])
- These will both be sent to the server at the same time and can be handled efficiently. The sole exception for this is if the local app had to declare a tempid for a record, which needs to be linked with the permanent ID after the mutation has completed: In that case, a sync
parser for a mutation is appropriate.