From a467bf9dc3f84187238e26ab806b1ce4ebc7a0b2 Mon Sep 17 00:00:00 2001 From: gered Date: Sat, 4 Jun 2016 18:30:12 -0400 Subject: [PATCH] add main views.reagent library documentation --- views.reagent/README.md | 305 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 views.reagent/README.md diff --git a/views.reagent/README.md b/views.reagent/README.md new file mode 100644 index 0000000..c68787c --- /dev/null +++ b/views.reagent/README.md @@ -0,0 +1,305 @@ +# 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. + +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 + +## Leiningen + +```clj +[gered/views.reagent "0.2.0-SNAPSHOT"] +``` + +## Usage + +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. + +Initialization of the views system is typically either done directly +view `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: + +```clj +(require '[views.sql.view :refer [view]]) + +(defonce view-system ... ) + +(defn todos-list [] + ["SELECT id, title, done FROM todos ORDER BY title"]) + +(def views + [(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. + +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. + +So, how do we create a view cursor? + +```clj +(ns webapp.client + (:require + [views.reagent.client.component :as vc :refer [view-cursor] :refer-macros [defvc]])) + +(defvc my-todos-list [] + [:ul + (map + (fn [{:keys [id title done]}] + ^{:key id} + [:li {:class (if done "completed")} title]) + @(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. 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`. + +`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. + +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 [] + (let [todos (view-cursor :todos)] + (if (vc/loading? todos) + [:div "Please wait ..."] + [:ul + (map + (fn [{:keys [id title done]}] + ^{:key id} + [:li {:class (if done "completed")} title]) + @todos)]))) +``` + +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. + +As an example, if our `:todos` view was updated to include a filter: + +```clj +(defn todos-list [done?] + ["SELECT id, title, done FROM todos WHERE done = ? ORDER BY title" done?]) +``` + +We can include this parameter in our call to `view-cursor`: + +```clj +(view-cursor :todos true) ; only return completed todos +``` + +We could get a little bit more fancy and make it an optional parameter: + +```clj +(defn todos-list [& [done?]] + (if-not (nil? done?) + ["SELECT id, title, done FROM todos WHERE done = ? ORDER BY title" done?] + ["SELECT id, title, done FROM todos ORDER BY title"])) +``` + +Letting us do any of these on the client: + +```clj +(view-cursor :todos) ; return all todos +(view-cursor :todos true) ; only completed +(view-cursor :todos false) ; only uncompleted +``` + +#### 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. + +It is highly 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. + +### 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. + +```clj +(use 'views.reagent.client.core) + +; subscribe to the :todos view (no parameters) +(subscribe! [{:view-id :todos :parameters []}]) + +; returns a Reagent cursor to the view's data. this is a "dumb" function and if no +; subscription exists it will simply return a cursor pointing at nil data forever +(->view-sig-cursor {:view-id :todos :parameters []}) + +; unsubscribe from the :todos view +(unsubscribe! [{:view-id :todos :parameters []}]) +``` + +When using these low-level functions, you need to specify view +signature maps (aka "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 (aka. view sigs), 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. + +### 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. + +#### 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 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 BrowserChannel this is the BrowserChannel "session id." + +#### 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: + +```clj +(defn send-fn + [subscriber-key [view-sig view-data]] + (your-messaging-lib/send! subscriber-key [:views/refresh view-sig view-data])) +``` + +#### Server-side Handling + +`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. + +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: + +* `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. + +## License + +Copyright © 2016 Gered King + +Distributed under the the MIT License. See LICENSE for more details.