update dependencies and fix things in views.reagent(+ .sente)

This commit is contained in:
Gered 2022-01-12 17:18:04 -05:00
parent b832409dfa
commit 17667c274a
6 changed files with 172 additions and 199 deletions

View file

@ -1,6 +1,7 @@
# views.reagent
[Reagent][1] plugin for the [views][2] library, providing real-time component updates to server-side changes to data.
[Reagent][1] plugin for the [views][2] library, providing real-time component updates to
server-side changes to data.
[1]: https://github.com/reagent-project/reagent
[2]: https://github.com/gered/views
@ -11,40 +12,48 @@
This library is made up of two core parts:
* The actual library, views.reagent, providing core functionality.
* A client/server communications plugin library which provides the glue code between whatever underlying client/server library you're using (e.g. [Sente][3] or [clj-browserchannel][4]) and views.reagent.
* A client/server communications plugin library which provides the glue code between whatever
underlying client/server library you're using (most likely [Sente][3]) and views.reagent.
[3]: https://github.com/ptaoussanis/sente
[4]: https://github.com/gered/clj-browserchannel
To use views.reagent in your application, you need to add both the main library and one client/server communications plugin library as dependencies. See their respective pages linked to below for more information on doing this.
To use views.reagent in your application, you need to add both the main library and one
client/server communications plugin library as dependencies. See their respective pages linked
to below for more information on doing this.
### Main Library Documentation
[See here for full documentation.][5]
[See here for full documentation.][4]
[5]: https://github.com/gered/views.reagent/tree/master/views.reagent
[4]: https://github.com/gered/views.reagent/tree/master/views.reagent
### Client/Server Plugin Documentation
* **[views.reagent.sente][6]** provides fairly low-level integration with Sente.
* **[views.reagent.browserchannel][7]** for using BrowserChannel for client/server communication.
* **[views.reagent.sente][5]** provides fairly low-level integration with Sente.
[6]: https://github.com/gered/views.reagent/tree/master/views.reagent.sente
[7]: https://github.com/gered/views.reagent/tree/master/views.reagent.browserchannel
[5]: https://github.com/gered/views.reagent/tree/master/views.reagent.sente
If you're intent on using something else, you'll need to write your own client/server plugin
library. Previously I provided a BrowserChannel plugin in addition to the Sente plugin library,
but BrowserChannel is now pretty ancient and unnecessary since modern browsers universally support
Websockets, so it was removed in favour of using Sente.
### Examples
There are two example applications for you to look at to see a fully working web application with working views system configured and working.
There are two example applications for you to look at to see a fully working web application with
working views system configured and working.
* Todo MVC. There are two versions of this that are both largely identical except that [one uses Sente][8] and the [other uses BrowserChannel][9].
* [Class Registry][10]. This is a somewhat more complex application with a busy UI showing a bunch of data at once, but it does serve to show how a UI can be built from multiple different views at once. This example app uses Sente.
* [Todo MVC][6]. This is a copy of the original Reagent "Todo MVC" example app, but re-worked to
use a SQL database and the views system.
* [Class Registry][7]. This is a somewhat more complex application with a busy UI showing a bunch
of data at once, but it does serve to show how a UI can be built from multiple different views
at once.
[8]: https://github.com/gered/views.reagent/tree/master/examples/todomvc
[9]: https://github.com/gered/views.reagent/tree/master/examples/todomvc-browserchannel
[10]: https://github.com/gered/views.reagent/tree/master/examples/class-registry
[6]: https://github.com/gered/views.reagent/tree/master/examples/todomvc
[7]: https://github.com/gered/views.reagent/tree/master/examples/class-registry
### Notes
@ -55,15 +64,12 @@ like to integrate client/server communications in their applications. I wanted t
(as much as possible) doing anything that would require any specific way of doing this
kind of integration.
As well, speaking for myself, I use my own custom helper library that wraps over Sente which
I like but did not want to force anyone else to use.
The client/server glue code provided by these libraries is incredibly light so if they
do not meet your needs for whatever reason you should find it easy to create one yourself.
## License
Copyright © 2016 Gered King
Copyright © 2022 Gered King
Distributed under the the MIT License. See LICENSE for more details.

View file

@ -1,21 +1,21 @@
(defproject gered/views.reagent.sente "0.2-SNAPSHOT"
(defproject net.gered/views.reagent.sente "0.2-SNAPSHOT"
:description "Sente client/server messaging adapter for views.reagent."
:url "https://github.com/gered/views.reagent"
:license {:name "MIT License"
:url "http://opensource.org/licenses/MIT"}
:dependencies [[org.clojure/clojure "1.8.0"]]
:dependencies []
:plugins [[lein-cljsbuild "1.1.3"]]
:plugins [[lein-cljsbuild "1.1.8"]]
:profiles {:provided
{:dependencies
[[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.8.51"]
[reagent "0.6.0-alpha"]
[gered/views "1.5"]
[gered/views.reagent "0.1"]
[com.taoensso/sente "1.8.1"]]}}
[[org.clojure/clojure "1.10.3"]
[org.clojure/clojurescript "1.10.773"]
[reagent "1.1.0"]
[net.gered/views "1.6-SNAPSHOT"]
[net.gered/views.reagent "0.2-SNAPSHOT"]
[com.taoensso/sente "1.16.2"]]}}
:cljsbuild {:builds
{:main

View file

@ -21,6 +21,15 @@
(send-fn sente-chsk-map data))
(reset! send-buffer []))
(defn chsk-open-event?
"returns true if the sente event is for a channel-socket state change to 'open' or 'connected'."
[{:keys [event] :as ev}]
; for :chsk/state events, sente sends the event data in the form [old-state new-state].
; we only care about the new state for the purposes of performing this check ...
(let [[ev-id ev-data] event]
(and (= :chsk/state ev-id)
(:open? (second ev-data)))))
(defn on-open!
"should be called when a new Sente connection is established. ev is the event
map provided by Sente where id = :chsk/state, and :open? = true. make sure
@ -44,14 +53,12 @@
application does not need to do any custom Sente event handling, then you can
opt to use this event handler."
[sente-chsk-map {:keys [event id client-id] :as ev}]
(let [[ev-id ev-data] event]
(cond
(and (= :chsk/state ev-id)
(:open? ev-data))
(on-open! sente-chsk-map ev)
(cond
(chsk-open-event? event)
(on-open! sente-chsk-map ev)
(= :chsk/recv id)
(on-receive! sente-chsk-map ev))))
(= :chsk/recv id)
(on-receive! sente-chsk-map ev)))
(defn init!
"performs initial configuration necessary to hook Sente into views.reagent as the

View file

@ -1,11 +1,10 @@
# views.reagent
This is the main library for the [views.reagent project][1] which
provides the core functionality that most of your application code will
make use of.
This is the main library for the [views.reagent project][1] which provides the core functionality
that most of your application code will make use of.
Familiarity with the [views][2] library is *absolutely crucial* to
understanding and usage of views.reagent.
Familiarity with the [views][2] library is *absolutely crucial* to understanding and usage of
views.reagent.
[1]: https://github.com/gered/views.reagent
[2]: https://github.com/gered/views
@ -18,22 +17,19 @@ understanding and usage of views.reagent.
## Usage
Much of this documentation will be referring to the
[Todo MVC example project][3] which uses
Much of this documentation will be referring to the [Todo MVC example project][3] which uses
[Sente client/server messaging][4] and [SQL views][5].
[3]: https://github.com/gered/views.reagent/tree/master/examples/todomvc
[4]: https://github.com/gered/views.reagent/tree/master/views.reagent.sente
[5]: https://github.com/gered/views.sql
Usage of this library is incredibly simple once you have a working
views system up and running.
Usage of this library is incredibly simple once you have a working views system up and running.
Initialization of the views system is typically either done directly
via `views.core/init!` with some special configuration for whatever
client/server messaging plugin you're using, or you may be required to
call a special "init" function provided by the plugin library to use
instead of `views.core/init!`.
Initialization of the views system is typically either done directly via `views.core/init!` with
some special configuration for whatever client/server messaging plugin you're using, or you may be
required to call a special "init" function provided by the plugin library to use instead of
`views.core/init!`.
The Todo MVC example uses the following (server-side) view system:
@ -49,31 +45,26 @@ The Todo MVC example uses the following (server-side) view system:
[(view :todos db #'todos-list)])
```
A single view named `:todos` which simply returns a list of all Todos
in the database. The view takes no parameters.
A single view named `:todos` which simply returns a list of all Todos in the database. The view
takes no parameters.
Over on the ClojureScript side of things, once a connection has been
established to the server by the client/server messaging library, you
are ready to start using **view cursors** in your Reagent components.
Over on the ClojureScript side of things, once a connection has been established to the server by
the client/server messaging library, you are ready to start using **view cursors** in your Reagent
components.
### View Cursors
A **view cursor** is simply a Reagent cursor that represents the
underlying view data received from the views library when the view is
subscribed to. We can create a subscription by simply creating a view
cursor for the desired view and giving it any appropriate parameters.
views.reagent will automatically determine if it's the first usage of
the view cursor and if a subscription request needs to be sent to the
server. When a view refresh is performed on the server for the view,
views.reagent sends it to the client and the data is put into a
location where it's available to the views cursor. Since view cursors
are Reagent cursors, updating the data like this instantly causes
components dereferencing the cursor to rerender themselves. When
components are unmounted views.reagent will automatically unsubscribe
from views as appropriate. As well, when parameters passed in to view
cursors change, view re-subscriptions are automatically handled to make
sure the view cursor is always up to date with the current client
state.
A **view cursor** is simply a Reagent cursor that represents the underlying view data received
from the views library when the view is subscribed to. We can create a subscription by simply
creating a view cursor for the desired view and giving it any appropriate parameters.
views.reagent will automatically determine if it's the first usage of the view cursor and if a
subscription request needs to be sent to the server. When a view refresh is performed on the
server for the view, views.reagent sends it to the client and the data is put into a location
where it's available to the views cursor. Since view cursors are Reagent cursors, updating the
data like this instantly causes components dereferencing the cursor to rerender themselves. When
components are unmounted views.reagent will automatically unsubscribe from views as appropriate.
As well, when parameters passed in to view cursors change, view re-subscriptions are automatically
handled to make sure the view cursor is always up to date with the current client state.
So, how do we create a view cursor?
@ -91,36 +82,29 @@ So, how do we create a view cursor?
@(view-cursor :todos))])
```
Given the previously set up view system on the server, this is all the
UI code necessary to subscribe to the `:todos` view and retrieve and
render the Todos list on the client. Whenever the `:todos` view is
refreshed on the server, the client will receive the data automatically
and the component will rerender since it is dereferencing the views
cursor. Finally, the client will automatically unsubscribe from the
`:todos` view when the `my-todos-list` component is unmounted.
Given the previously set up view system on the server, this is all the UI code necessary to
subscribe to the `:todos` view and retrieve and render the Todos list on the client. Whenever the
`:todos` view is refreshed on the server, the client will receive the data automatically and the
component will rerender since it is dereferencing the views cursor. Finally, the client will
automatically unsubscribe from the `:todos` view when the `my-todos-list` component is unmounted.
At first glance, `my-todos-list` looks just like any other normal
Reagent component. However a very important difference is the use of
`defvc` instead of `defn`.
At first glance, `my-todos-list` looks just like any other normal Reagent component. However a
very important difference is the use of `defvc` instead of `defn`.
`defvc` creates a Reagent **view component** which hooks into some
React component lifecycle events to automatically handle view
subscriptions/unsubscriptions for us based on how we use `view-cursor`
inside the component. You ***must*** use `defvc` for all Reagent
components within which you want to use `view-cursor`.
`defvc` creates a Reagent **view component** which hooks into some React component lifecycle
events to automatically handle view subscriptions/unsubscriptions for us based on how we use
`view-cursor` inside the component. You ***must*** use `defvc` for all Reagent components within
which you want to use `view-cursor`.
`view-cursor` returns a Reagent cursor containing the actual view data
(in this case, a simple list of Todos). It's important to note that at
first the view data returned will be `nil` since obviously the client
must first wait for the subscription to be processed by the server and
then for the server to send back the initial view data. Once this
happens the component will automatically rerender as you would expect
(showing the list of todos).
`view-cursor` returns a Reagent cursor containing the actual view data (in this case, a simple
list of Todos). It's important to note that at first the view data returned will be `nil` since
obviously the client must first wait for the subscription to be processed by the server and then
for the server to send back the initial view data. Once this happens the component will
automatically rerender as you would expect (showing the list of todos).
You can check if the view cursor is still waiting on the initial set of
data through the use of the `loading?` function. This can be used to
render some kind of "loading" message or something similar if you'd
prefer not to render empty data when components first load.
You can check if the view cursor is still waiting on the initial set of data through the use of
the `loading?` function. This can be used to render some kind of "loading" message or something
similar if you'd prefer not to render empty data when components first load.
```clj
(defvc my-todos-list []
@ -135,20 +119,18 @@ prefer not to render empty data when components first load.
@todos)])))
```
Note that `loading?` should be passed the actual Reagent cursor that
`view-cursor` returns, not the dereferenced result.
Note that `loading?` should be passed the actual Reagent cursor that `view-cursor` returns, not
the dereferenced result.
Also remember that `loading?` only checks if the view cursor is waiting
on the **initial** view data. Once that first set of data is received,
`loading?` will always return false. There is no current method in
views.reagent for determining if a view refresh is pending, although
this is typically somewhat of a less drastic UI change to the user so
in practice it may be less of a concern.
Also remember that `loading?` only checks if the view cursor is waiting on the **initial** view
data. Once that first set of data is received, `loading?` will always return false. There is no
current method in views.reagent for determining if a view refresh is pending, although this is
typically somewhat of a less drastic UI change to the user so in practice it may be less of a
concern.
#### View Parameters
Some of your views may take parameters. This is easily supported by
views.reagent.
Some of your views may take parameters. This is easily supported by views.reagent.
As an example, if our `:todos` view was updated to include a filter:
@ -182,26 +164,23 @@ Letting us do any of these on the client:
#### View Cursors Are Intended To Be Read-only
Even though a Reagent cursor allows you to update them as well as read
from them, updating a view cursor doesn't do anything. Nothing stops
you from doing this, but updating a view cursor does not propagate
changes to the server or anything like that. In addition, you will lose
any changes you make every time a view refresh is received as the data
gets blindly replaced.
Even though a Reagent cursor allows you to update them as well as read from them, updating a view
cursor doesn't do anything. Nothing stops you from doing this, but updating a view cursor does not
propagate changes to the server or anything like that. In addition, you will lose any changes you
make every time a view refresh is received as the data gets blindly replaced.
It is recommended that you do not write code that updates a view cursor.
## Advanced Topics
Most people just looking to use views.reagent in their applications
probably won't need to read anything in this section.
Most people just looking to use views.reagent in their applications probably won't need to read
anything in this section.
### Manually Managing View Subscriptions
For those applications with very specific/complex requirements, you can
manually subscribe and unsubscribe to views from your ClojureScript
code using views.reagent as well as make use of view cursors outside of
Reagent components. I do not recommend this though.
For those applications with very specific/complex requirements, you can manually subscribe and
unsubscribe to views from your ClojureScript code using views.reagent as well as make use of view
cursors outside of Reagent components. I do not recommend this though.
```clj
(use 'views.reagent.client.core)
@ -217,50 +196,41 @@ Reagent components. I do not recommend this though.
(unsubscribe! [{:view-id :todos :parameters []}])
```
When using these low-level functions, you need to specify view
signature maps (a.k.a. "view sigs") to refer to the views you want to
use. Also note that unlike the server-side view signatures that you may
be familiar with from the views library, these client-side view
signatures never have a `:namespace` in them. Even if you include one,
it is disregarded by the server.
When using these low-level functions, you need to specify view signature maps (a.k.a. "view sigs")
to refer to the views you want to use. Also note that unlike the server-side view signatures that
you may be familiar with from the views library, these client-side view signatures never have a
`:namespace` in them. Even if you include one, it is disregarded by the server.
Also note that `subscribe!` and `unsubscribe!` both take a list of view
signatures, so you can subscribe and unsubscribe from multiple views at
once.
Also note that `subscribe!` and `unsubscribe!` both take a list of view signatures, so you can
subscribe and unsubscribe from multiple views at once.
I do not recommend mixing use of these low-level functions and using
`defvc` components. You should probably pick one or the other and stick
to it unless you really know what you're doing.
I do not recommend mixing use of these low-level functions and using `defvc` components. You
should probably pick one or the other and stick to it unless you really know what you're doing.
### Integration Points for Writing a Client/Server Messaging Plugin
If you would like to write your own client/server messaging plugin
library to fit your own needs you can easily do so. There are a couple
integration points within the main views.reagent library, as well as
some special configuration you will need to provide to the views system
that you need to be aware of.
If you would like to write your own client/server messaging plugin library to fit your own needs
you can easily do so. There are a couple integration points within the main views.reagent library,
as well as some special configuration you will need to provide to the views system that you need
to be aware of.
#### View Subscriber Key
In the views library, a bunch of functions take a "subscriber key."
This is an arbitrary value that uniquely identifies a subscriber. There
can of course be multiple views for each subscriber key. Said another
way: someone (uniquely identified by the subscriber key) can be
subscribed to multiple different views at the same time.
In the views library, a bunch of functions take a "subscriber key." This is an arbitrary value
that uniquely identifies a subscriber. There can of course be multiple views for each subscriber
key. Said another way: someone (uniquely identified by the subscriber key) can be subscribed to
multiple different views at the same time.
You will typically want to use the underlying client/server library's
"client/user connection ID" as the subscriber key. For Sente this is
the "user id" or the "client id" (depending on your application), and
for clj-browserchannel this is the BrowserChannel "session id."
You will typically want to use the underlying client/server library's "client/user connection ID"
as the subscriber key. For Sente this is almost certainly going to be the "user id" or the
"client id" (depending on your application).
#### View System Configuration
You need to provide a `:send-fn` function that can be provided in the
options given to `views.core/init!`. This function is used by the views
library to send view refreshes to subscribers. For views.reagent, your
send-fn function should send a vector with 3 things in it: the keyword
`:views/refresh` followed by the `view-sig` and then `view-data`. For
example:
You need to provide a `:send-fn` function that can be provided in the options given to
`views.core/init!`. This function is used by the views library to send view refreshes to
subscribers. For views.reagent, your send-fn function should send a vector with 3 things in it:
the keyword `:views/refresh` followed by the `view-sig` and then `view-data`. For example:
```clj
(defn send-fn
@ -270,50 +240,43 @@ example:
#### Server-side Handling
`views.reagent.server.core` has two main event handling functions that
provide proper handling for client connection events:
`views.reagent.server.core` has two main event handling functions that provide proper handling
for client connection events:
* `on-close!` should be called when a client's connection is closed for
whatever reason. views.reagent will remove all of the client's
subscriptions.
* `on-receive!` should be called and passed in the raw data from every
message received from the client. It will return `true` if
views.reagent recognized and handled the message as a subscription or
unsubscription request. See `views.reagent.utils/relevant-event?` for
how it recognizes relevant messages.
* `on-close!` should be called when a client's connection is closed for whatever reason.
views.reagent will remove all of the client's subscriptions.
* `on-receive!` should be called and passed in the raw data from every message received from the
client. It will return `true` if views.reagent recognized and handled the message as a
subscription or unsubscription request. See `views.reagent.utils/relevant-event?` for how it
recognizes relevant messages.
For both of these functions, `client-id` should be the subscriber key
from the views library, and `context` can be anything you wish.
Typically you will want to use something like a Ring request map and/or
user profile data as the context. This context argument is what gets
passed to `views.core/subscribe!` and `views.core/unsubscribe!`.
For both of these functions, `client-id` should be the subscriber key from the views library, and
`context` can be anything you wish. Typically you will want to use something like a Ring request
map and/or user profile data as the context. This context argument is what gets passed to
`views.core/subscribe!` and `views.core/unsubscribe!`.
#### Client-side Handling
`views.reagent.client.core` also has two main event handling functions
that provide proper handling for server connection events:
`views.reagent.client.core` also has two main event handling functions that provide proper
handling for server connection events:
* `on-open!` should be called when a connection to the server is
established (and re-established, if applicable). views.reagent will
re-send subscription requests for any subscriptions that should exist
(e.g. if the connection was lost and the application reconnected,
resubscribe to the views that all current mounted components need).
* `on-receive!` should be called and passed in the raw data from every
message received from the server. It will return `true` if
views.reagent recognized and handled the message as a view refresh
event. See `views.reagent.utils/relevant-event?` for how it recognizes
relevant messages.
* `on-open!` should be called when a connection to the server is established (and re-established,
if applicable). views.reagent will re-send subscription requests for any subscriptions that
should exist (e.g. if the connection was lost and the application reconnected, resubscribe to
the views that all current mounted components need).
* `on-receive!` should be called and passed in the raw data from every message received from the
server. It will return `true` if views.reagent recognized and handled the message as a view
refresh event. See `views.reagent.utils/relevant-event?` for how it recognizes relevant messages.
You also need to provide a function to send messages to the server.
You can set this function by directly using `reset!` on the atom
`views.reagent.client/send-fn`. The function should take a single
argument which is the data to be sent.
You also need to provide a function to send messages to the server. You can set this function by
directly using `reset!` on the atom `views.reagent.client/send-fn`. The function should take a
single argument which is the data to be sent.
You should take care to hook up all of these integration points before
the first Reagent component is rendered at page load.
You should take care to hook up all of these integration points before the first Reagent component
is rendered at page load.
## License
Copyright © 2016 Gered King
Copyright © 2022 Gered King
Distributed under the the MIT License. See LICENSE for more details.

View file

@ -1,19 +1,19 @@
(defproject gered/views.reagent "0.2-SNAPSHOT"
(defproject net.gered/views.reagent "0.2-SNAPSHOT"
:description "Reagent plugin for the views library, providing real-time component updates to server-side changes to data."
:url "https://github.com/gered/views.reagent"
:license {:name "MIT License"
:url "http://opensource.org/licenses/MIT"}
:dependencies [[org.clojure/tools.logging "0.3.1"]]
:dependencies [[org.clojure/tools.logging "1.2.4"]]
:plugins [[lein-cljsbuild "1.1.3"]]
:plugins [[lein-cljsbuild "1.1.8"]]
:profiles {:provided
{:dependencies
[[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.8.51"]
[reagent "0.6.0-alpha"]
[gered/views "1.5"]]}}
[[org.clojure/clojure "1.10.3"]
[org.clojure/clojurescript "1.10.773"]
[reagent "1.1.0"]
[net.gered/views "1.6-SNAPSHOT"]]}}
:cljsbuild {:builds
{:main

View file

@ -28,8 +28,8 @@
[args & body] decl]
`(defn ~component-name ~attr-map []
(reagent.core/create-class
{:component-will-mount
(fn [this#]
{:constructor
(fn [this# props#]
(views.reagent.client.component/prepare-for-render! this#))
:component-did-mount
@ -43,14 +43,11 @@
(fn [this#]
(views.reagent.client.component/unsubscribe-all! this#))
:component-will-receive-props
(fn [this# new-argv#]
(views.reagent.client.component/prepare-for-render! this#))
:component-did-update
(fn [this# old-argv#]
(views.reagent.client.component/update-subscriptions! this#))
:component-function
:reagent-render
(fn ~args
(views.reagent.client.component/prepare-for-render! (reagent.core/current-component))
~@body)}))))