In the previous tutorial we ported to Reagent the first steps of the official React Tutorial.
In this Part II of the tutorial on Reagent we're going to complete the porting of the official React Tutorial to Reagent, by introducing component state management.
To start working, assuming you've git
installed, do as follows to
restart from the end of the
previous tutorial
for both React Tutorial and its porting on Reagent.
If you did not save the state of the React Tutorial at the end of the previous tutorial, open a terminal and submit the following commands
git clone https://github.com/magomimmo/react-tutorial.git
cd react-tutorial
git checkout reagent-tutorial
Then from the same terminal install the required npm
modules and run
the React Tutorial server:
npm install
PORT=3001 node server.js
Finally visit the URL localhost:3001.
Open a new terminal and enter the following commands
git clone https://github.com/magomimmo/modern-cljs.git
cd modern-cljs
git checkout se-tutorial-21
git checkout -b reagent-tutorial-2
Then launch the development environment as usual
boot dev
Next visit the URL
localhost:3000/reagent.html.
Even if you don't see anything, because we still have to attach the
comment-box
root component to the "content"
div
of the
reagent.html
page, a websocket connection will be established
between your development environment and your browser's JS engine.
Now open a new terminal and launch the nREPL client followed by the bREPL client on top of it as usual:
cd /path/to/modern-cljs
boot repl -c
...
boot.user=>
boot.user> (start-repl)
...
cljs.user>
We're almost done. To complete the setup for continuing the porting of the React Tutorial to Reagent, we need to require few namespaces from the bREPL:
cljs.user> (require '[reagent.core :as r :refer [render]])
cljs.user> (require '[domina.core :refer [by-id]])
cljs.user> (require '[modern-cljs.reagent :refer [comment-box data]])
NOTE 1: the latest requirement is needed to make the
comment-box
anddata
symbols visible in the bREPL, because they are now defined in themodern-cljs.reagent
namespace. In the previous bREPL session they were defined at the bREPL in thecljs.user
namespace.
Finally, to attach and render the components we need to call the
Reagent render
function:
cljs.user> (render [comment-box data] (by-id "content"))
#object[Constructor [object Object]]
You should now see the content of the reagent.html
page.
The next steps in the React Tutorial are Fetching from the server and Reactive State. In this port of the React Tutorial to Reagent we're going to restrict our scope to the state management only, because it is where Reagent stands out the most from React.
Let's start by reading the paragraph of the React Tutorial on state management:
So far, based on its props, each component has rendered itself once. props are immutable: they are passed from the parent and are "owned" by the parent. To implement interactions, we introduce mutable state to the component. this.state is private to the component and can be changed by calling this.setState(). When the state updates, the component re-renders itself.
In the subsequent couple of steps the React Tutorial substitutes the
hard-coded data
with some dynamic from the server. Considering I'm not
going to port to Reagent this part of the React Tutorial, I'm just
copying the corresponding code to facilitate its pasting into the
example.js
source file. I'll also limit my code comments to a few
things.
// delete the `data` variable definition.
// substitute the previous CommentBox component definition with
// the following one
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});
// substitute the previous ReactDOM.render call with the following one
ReactDOM.render(
<CommentBox url="/api/comments" pollInterval={2000} />,
document.getElementById('content')
);
The most important variation from the previous version of CommentBox
React component is the substitution of the this.props.data
property with the this.state.data
state in its render
function, initially set to an empty array of comments, to make the
component stateful.
Apparently, the only way to change the state of the component is via
the setState()
function.
The setState()
function is indirectly called the very first time,
via the loadCommentsFromServer()
function, in the body of
componentDidMount()
. The componentDidMount()
function is called only
once, immediately after the component has been mounted in the DOM by
the ReactDOM.render
function. Then, setState()
is indirectly
called every 2 seconds (i.e. pollInterval), by passing to it the array
of comments obtained by polling the server via ajax.
If you heard that React implements one-way data flow communication model between data and User Interfaces, this is what they mean:
data -> CommentBox -> CommentList -> Comment
The value of this.state.data
obtained from the server is passed down
to the CommentList
component. Each comment contained in
this.state.data
is then passed to the Comment
component of the
list. Only new or updated Comment
components will be redrawn.
This last observation is very important, because it illustrates that a
React component gets updated not only when its private state changes,
but even when they receive new props
from its owner
(e.g. CommentBox -> CommentList -> Comment
).
As you are going to see in a moment, Reagent exploits the above React behavior to not use the component state management offered by React and, instead, take care of it by itself.
Let's now verify if the above code refactoring works as
expected. First reload the localhost:3001
page. Considering that we still have to implement the CommentForm
component to create new comments, to see the React state management at
work you need to manually change the comment.json
array of comments
which is read every two second by the the CommentBox
component.
Open the comments.json
file, which is located in the
react-tutorial
main folder, and add a new comment to it:
[
{
"id": 1388534400000,
"author": "Pete Hunt",
"text": "Hey there!"
},
{
"id": 1420070400000,
"author": "Paul O’Shannessy",
"text": "React is *great*!"
},
{
"id": 3,
"author": "Dan Holmsand",
"text": "Reagent 0.6.0-alpha is here. Check it out! http://reagent-project.github.io/news/news060-alpha.html"
}
]
When you save the file, after a moment you should see that the page has been updated with the new comment by the author of Reagent.
You could even modify one of the comment in the comments.json
file
to see a reaction in the page as you save the file:
[
// ...,
,
{
"id": 3,
"author": "Dan Holmsand",
"text": "Reagent 0.4.3 is now, finally, a thing."
}
]
Before porting the component state management from React to Reagent,
we have to digress a little bit about Clojure(Script)
atom
.
Immutability is divine, but sometime you still need to modify the
world around you. ClojureScript has immutability as the default
behavior of its data structures, but it also offers you a more mundane
atom
function when you really want to manipulate reality.
Let's get some help from the bREPL to learn about atom
usage. Say
you want to maintain the number of times a button gets clicked
cljs.user> (def clicks (atom 0))
#'cljs.user/clicks
By evaluating clicks
, you get the Atom
object itself.
cljs.user> clicks
#object [cljs.core.Atom {:val 0}]
To get the protected value living inside the clicks
symbol, you need
to dereference it by using the deref
function:
cljs.user> (deref clicks)
0
The deref
function is used so frequently that it deserves a
reader macro
to shorten its call:
cljs.user> @clicks
0
Every time the button gets clicked, you want to increment its internal
value by using a function (i.e. inc
):
cljs.user> (swap! clicks inc)
1
cljs.user> @clicks
1
Sometimes you want to reset the number of clicks to a specific value:
cljs.user> (reset! clicks 0)
0
cljs.user> @clicks
0
Even if changes to atoms are always non blocking and free of race conditions, this is not the main reason we are talking about them in the Reagent context.
Say you want to observe the state of clicks
. You can easily add a
watcher to log its state in time at the js/console
.
To add a watcher
you use add-watch
:
cljs.user> (doc add-watch)
-------------------------
cljs.core/add-watch
([iref key f])
Adds a watch function to an atom reference. The watch fn must be a
fn of 4 args: a key, the reference, its old-state, its
new-state. Whenever the reference's state might have been changed,
any registered watches will have their functions called. The watch
fn will be called synchronously. Note that an atom's state
may have changed again prior to the fn call, so use old/new-state
rather than derefing the reference. Keys must be unique per
reference, and can be used to remove the watch with remove-watch,
but are otherwise considered opaque by the watch mechanism. Bear in
mind that regardless of the result or action of the watch fns the
atom's value will change. Example:
(def a (atom 0))
(add-watch a :inc (fn [k r o n] (assert (== 0 n))))
(swap! a inc)
;; Assertion Error
(deref a)
;=> 1
nil
In this context, we're only interested in logging the new value (i.e. the fourth argument passed to the watcher function).
(add-watch clicks :log #(-> %4 clj->js js/console.log))
Be sure to have your browser console open on the
localhost:3000/reagent.html page
and observe it while updating the state of the clicks
atom:
cljs.user> (reset! clicks 0)
0
cljs.user> (swap! clicks + 5)
5
cljs.user> (swap! clicks dec)
4
cljs.user> (swap! clicks dec)
3
You should see the state of the clicks
atom printed at the
browser console every time you change its internal value at the
bREPL. Here, the fundamental word is time.
You can think about the data
containing comments as an atom
and
the comment-box
component as an observer that executes a reaction,
redrawing itself, anytime it observes a change in the state of the
comments.
NOTE 2: in reality, as we'll see in a moment, this statement is not true.
The Reagent tutorials on the re-frame wiki do a terrific job explaining Reagent and I strongly reccomend you read it several times, until you really grasp the entire content.
That said, Reagent offers its own version of the standard CLJS atom
definition, AKA ratom
, supporting the same atom
protocol
(e.g. swap!
, reset!
, etc.) plus something new, as you can read
from its documentation at the REPL:
cljs.user> (doc r/atom)
-------------------------
reagent.core/atom
([x] [x & rest])
Like clojure.core/atom, except that it keeps track of derefs.
Reagent components that derefs one of these are automatically
re-rendered.
nil
This is very interesting: whenever a component derefs a ratom
, it
automagically re-renders itself. This statement is not totally true,
because a Reagent component re-renders itself when it derefs a ratom
only if the internal value of the ratom
is not
identical to
the previous one.
We previously saw in the React Tutorial that by modifying the array of
comments in the comments.json
file, periodically read by ajax, we're
creating the condition for a change in the CommentBox
this.state.data
state and, consequently, in the CommentList
and
Comment
components as well via the one-way data flow communication
model.
Even without porting the ajax
stuff from the React Tutorial to
Reagent, by now knowing that a Reagent component could eventually
re-render itself by derefing a ratom
, we should be able to obtain
the same behavior.
First, open the reagent.cljs
file and modify the data
symbol by
wrapping its definition in a ratom
:
(def data (r/atom [{:id 1
:author "Pete Hunt"
:text "This is one comment"}
{:id 2
:author "Jordan Walke"
:text "This is *another* comment"}]))
Then modify consequently the comment-box
component definition by
simply adding the ratomized comments
parameter as follows:
(defn comment-box [comments]
[:div
[:h1 "Comments"]
[comment-list comments]
[comment-form]])
By taking into account that comments
has been ratomized, we now
need to deref
it in the comment-list
component definition:
(defn comment-list [comments]
[:div
(for [{:keys [id author text]} @comments]
^{:key id} [comment-component author text])])
Next, re-render the comment-box
in the "content"
div as usual:
cljs.user> (render [comment-box data] (by-id "content"))
#object[Constructor [object Object]]
Finally, swap the content of the ratom
by adding a new comment to
it:
cljs.user> (swap! data conj {:id 3 :author "Mimmo Cosenza" :text "Reagent is even better"})
[{:id 1, :author "Pete Hunt", :text "This is one comment"} {:id 2, :author "Jordan Walke", :text "This is *another* comment"} {:id 3, :author "Mimmo Cosenza", :text "Reagent is even better"}]
The comment-box
view component should have immediately been updated
with the new comment just added to the ratom
.
Let's see if the comment-box
component reacts to an updated comment as well:
cljs.user> (swap! data assoc-in [(rand-int (count @data)) :text] "This is a **randomly** assigned comment")
[{:id 1, :author "Pete Hunt", :text "This is one comment"} {:id 2, :author "Jordan Walke", :text "This is *another* comment"} {:id 3, :author "Mimmo Cosenza", :text "This is a **randomly** assigned comment"}]
NOTE 3: the text of the comment to be updated is randomly chosen by the
(rand-int (count @data))
expression.
Again, a soon as the swap!
form gets evaluated, you should see the
corresponding components view update.
We just experimented with how Reagent abstracted away, in the ratom
structure, the React need to use this.state.data
instead of
this.props.data
to manage state management in React components.
Hopefully, you should have also appreciated how the Reagent way of managing components state made the Reagent/CLJS code much more concise than the corresponding React/JS code.
The next and last step in porting the React Tutorial to Reagent has to
do with the CommentForm
component.
In the lastest paragraps we were able to programmatically add new
comments to the React Tutorial by just adding them to the
comments.json
file. Mutatis mutandis,
while porting this step from the React Tutorial to Reagent, we did a
similar thing by just adding new comments to the data
ratom
.
Let's now afford the problem of adding new comments by using a form component. The final solution implemented by the React Tutorial follows:
var CommentForm = React.createClass({
getInitialState: function() {
return {author: '', text: ''};
},
handleAuthorChange: function(e) {
this.setState({author: e.target.value});
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var author = this.state.author.trim();
var text = this.state.text.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
this.setState({author: '', text: ''});
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange}
/>
<input
type="text"
placeholder="Say something..."
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Post" />
</form>
);
}
});
The easiest way to understand the code of a React component is to
start reading its render
function.
The CommentForm
component is composed of a form
with three
input
: two input
of text
type and one input
of submit
type.
The text
input
types get their values from a corresponding state
of the main component: this.state.author
and
this.state.text
. Their values are initially set to the void string
''
by the getInitialState()
function.
Whenever the user type into one of these input
components, the
corresponding onChange
handler gets called
(i.e. handleAuthorChange
and handleTextChange
). Those handlers
just set the value of the corresponding state to the one typed in by
the user. So far, so good.
The handleSubmit
function is called whenever the user click the
Post
submit button of the form
. The handleSubmit
function does
few things:
- it stops the default propagation of the submit event to the server;
- it trims any blank from
this.state.author
andthis.state.text
strings; - it calls the
onCommentSubmit
function passing to it theauthor
andtext
trimmed strings when they are not blanks and it finally clears theauthor
and thetext
states by setting again their values to the void''
string.
But where is the onCommentSubmit
function defined? In the
CommentBox
component which is the owner
of the CommentForm
one:
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
handleCommentSubmit: function(comment) {
var comments = this.state.data;
// Optimistically set an id on the new comment. It will be replaced by an
// id generated by the server. In a production application you would likely
// not use Date.now() for this and would have a more robust system in place.
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
this.setState({data: comments});
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
Again, if you start by reading the render
function of the
CommentBox
component, you'll see that it passes the
handleCommentSubmit
callback function as the value of the
onCommentSubmit
props of the CommentForm
sub-component.
As previously said, we are not going to port to Reagent the ajax and
the server side parts of the React Tutorial and we leave to the
careful reader the understanding of the handleCommentSubmit
callback.
To see the updated and final version of the React Tutorial at work,
just reload the localhost:3001 URL. Add any
number of comments you want by using the newly added CommentForm
component.
NOTE 4: Thanks to the provided custom backend, you can open more tabs in your browser and appreciate the way they get updated anytime you add a new comment in one of the tab. As previously said, I'm not going to port this feature to Reagent in this tutorial.
Let's start the porting of the CommentForm
to Reagent by first
defining the structure of the comment-form
component as simple as
possible. Considering that the code already ported to Reagent is now
living in the modern-cljs.reagent
namespace, let's first set this
namespace as the current one in the bREPL.
cljs.user> (in-ns 'modern-cljs.reagent)
nil
Now define the comment-form
as follows:
modern-cljs.reagent> (defn comment-form []
[:form
[:input {:type "text"
:placeholder "Your name"}]
[:input {:type "text"
:placeholder "Say something"}]
[:input {:type "button"
:value "Post"}]])
#'modern-cljs.reagent/comment-form
NOTE 5: the third
input
of theform
is of typebutton
because we're not going toPOST
new comments to the server.
Before rendering the comment-box
component, if we want to use the
by-id
function from the domina
library as we did before, we have
to require its domina.core
namespace in the modern-cljs.reagent
current bREPL namespace as well:
modern-cljs.reagent> (require '[domina.core :as dom :refer [by-id]])
Let's now render the comment-box
as usual:
modern-cljs.reagent> (r/render [comment-box data] (by-id "content"))
#object[Constructor [object Object]]
You should immediately see the comment-form
component in your
browser. The next step to replicate the same behavior of the React
Tutorial is to manage the state for the CommentForm
component.
We already used a ratom
to manage the state of the data
vector of
maps recording comments. But this case is different. We need a local
state, not a global one as it is data
.
Before creating a local ratom
to manage the local state
of a Reagent component, we need to digress about a very important
Reagent topic: the three different ways to create a Reagent
component. In this tutorial we're not using the third way, known as
form-3
, of creating Reagent components.
Until now we only used the most simple way, known as form-1
, for
creating a Reagent component: a simple function definition returning a
hiccup vector
(defn comment-component [author comment]
[:div
[:h2 author]
[:span {:dangerouslySetInnerHTML
#js {:__html (js/marked comment #js {:sanitize true})}}]])
(defn comment-list [comments]
[:div
(for [{:keys [id author text]} @comments]
^{:key id} [comment-component author text])])
(defn comment-form []
[:form
[:input {:type "text"
:placeholder "Your name"}]
[:input {:type "text"
:placeholder "Say something"}]
[:input {:type "button"
:value "Post"}]])
(defn comment-box [comments]
[:div
[:h1 "Comments"]
[comment-list comments]
[comment-form]])
The second way of creating a Reagent component, known as form-2
, is
to define a function returning a function which, in turn, returns an
hiccup vector.
The form-2
way of creating a Reagent component is used whenever you
need some initial setup for the component which has to be executed
only once, at its creation.
This is exactly what we need to setup a local state for the
comment-form
component which initially set both the author
and the
text
props to the void string ""
. Something like the following:
modern-cljs.reagent> (defn comment-form []
(let [comment (r/atom {:author "" :text ""})]
(fn []
[:form
[:input {:type "text"
:placeholder "Your name"
:value (:author @comment)}]
[:input {:type "text"
:placeholder "Say something"
:value (:text @comment)}]
[:input {:type "button"
:value "Post"}]])))
#'modern-cljs.reagent/comment-form
As you see, we first create a local ratom
comment, internally
represented as a map with its :author
and :text
keys initialized
to the void string ""
. Then we return an anonymous function that, in
turn, returns the usual hiccup vector to be subsequently rendered.
Also note as we set the input
attributes using a map, as we already
did within comment-component
to set the dangerouslySetInnerHTML
attribute for its included span
component.
By using the map in the hiccup vector we were able to set the value
attribute of both author
and text
inputs to the corresponding
protected value of the local ratom
by derefing it. This way, anytime
the mutable comment
ratom gets swapped or reset, the corresponding
input
component gets the opportunity to be re-rendered.
So far, so good. But if you try to re-render the comment-box
root
component at the bREPL
modern-cljs.reagent> (r/render [comment-box data] (by-id "content"))
#object[Constructor [object Object]]
you'll see that both the author
and the text
input components do
not take any of your typing.
This is because we have not defined any
handler for the :on-change
event.
NOTE 6: as previously said, React uses
CamelCase
names for component names. For events, it usescamelCase
names. Conversely, Reagent useskebab-case
names for components and keywordized:kebab-case
names for both events and component attributes.
Let's add the :on-change
event handler to our input components:
modern-cljs.reagent> (defn comment-form []
(let [comment (r/atom {:author "" :text ""})]
(fn []
[:form
[:input {:type "text"
:placeholder "Your name"
:value (:author @comment)
:on-change #(swap! comment assoc :author (-> %
.-target
.-value))}]
[:input {:type "text"
:placeholder "Say something"
:value (:text @comment)
:on-change #(swap! comment assoc :text (-> %
.-target
.-value))}]
[:input {:type "button"
:value "Post"}]])))
#'modern-cljs.reagent/comment-form
As you see, each :on-change
handler gets the value of the target of
the :on-change
event and sets it as the value of the corresponding
key of the ratomized comment
map by using the swap!
function.
If you re-render the comment-box
root component the input
component of the comment-form
component is now taking your typing
modern-cljs.reagent> (r/render [comment-box data] (by-id "content"))
#object[Object [object Object]]
The very last step in porting to Reagent the final version of the
React Tutorial concerns the :on-click
event associated with the
Post
button.
NOTE 7: as we decided not to port to Reagent the custom backend and the corresponding ajax call to set/get comments to/from it, we substituted the
submit
input type with thebutton
input type. Consequently, instead of having to manage the form:on-submit
event we need to manage its:on-click
event.
The :on-click
handler associated with the comment-form
has to do a few things:
- get the values of
:author
and:text
keys from the ratomized comment andtrim
them; - reset those keys' to the void string
""
; - when neither the
author
or thetext
value isblank?
, add the comment to the globaldata
ratom providing each of them with a kind of unique identifier as React Tutorial did.
The trim
function and the blank?
predicate are included in the
clojure.string
namespace. So we need to require it at the bREPL:
modern-cljs.reagent> (require '[clojure.string :as s :refer [trim blank?]])
modern-cljs.reagent> (trim " trim me ")
"trim me"
modern-cljs.reagent> (blank? nil)
true
modern-cljs.reagent> (blank? " ")
true
modern-cljs.reagent> (blank? "")
true
modern-cljs.reagent> (blank? "\t")
true
modern-cljs.reagent> (blank? "\n")
true
We can now define the handle-comment-on-click
handler as follows:
modern-cljs.reagent> (defn handle-comment-on-click [comment]
(let [author (trim (:author @comment))
text (trim (:text @comment))]
(reset! comment {:author "" :text ""})
(when-not (or (blank? author) (blank? text))
(swap! data conj {:id (.getTime (js/Date.)) :author author :text text}))))
#'modern-cljs.reagent/handle-comment-on-click
Note as we derefed the ratomized comment to get the values from
its :author
and :text
keys. Also note that we created an unique
identifier for each new comment by using the same Date
and getTime
JS constructor/function. Finally, when none of author
or text
value is blank, we conj
the newly created comment to the global
data
ratom.
The very last step is to redefine the comment-form
to include the
:on-click
newly defined handler:
modern-cljs.reagent> (defn comment-form []
(let [comment (r/atom {:author "" :text ""})]
(fn []
[:form
[:input {:type "text"
:placeholder "Your name"
:value (:author @comment)
:on-change #(swap! comment assoc :author (-> %
.-target
.-value))}]
[:input {:type "text"
:placeholder "Say something"
:value (:text @comment)
:on-change #(swap! comment assoc :text (-> %
.-target
.-value))}]
[:input {:type "button"
:value "Post"
:on-click #(handle-comment-on-click comment)}]])))
#'modern-cljs.reagent/comment-form
Re-render the comment-box
root component as usual:
modern-cljs.reagent> (r/render [comment-box data] (by-id "content"))
#object[Constructor [object Object]]
You should now be able to add new comments by using the comment-form
component as you previously did with the React Tutorial. Because we
did not port the custom backend and the corresponding ajax calls to
set/get the comments from the backend, you'll not be able to use more
tabs of your browser to add new comments.
For completing this tutorial we update the reagent.cljs
source file
to bring into it the code we evaluated at the bREPL with a small
improvement that I leave to you to discover.
We also add, as we did in previous tutorials with the login.cljs
and
the shopping.cljs
, an init
function to be exported and used in the
reagent.html
file to attach the component-box
root Reagent
component to the "content"
div.
Here is the complete reagent.cljs
source file
(ns modern-cljs.reagent
(:require [reagent.core :as r :refer [render]] ;; refer render to be used in the init function
[domina.core :as dom :refer [by-id]]
[clojure.string :as s :refer [trim blank?]]
[cljsjs.marked]))
(def data (r/atom [{:id 1
:author "Pete Hunt"
:text "This is one comment"}
{:id 2
:author "Jordan Walke"
:text "This is *another* comment"}]))
(defn comment-component [author comment]
[:div
[:h2 author]
[:span {:dangerouslySetInnerHTML
#js {:__html (js/marked comment #js {:sanitize true})}}]])
(defn comment-list [comments]
[:div
(for [{:keys [id author text]} @comments]
^{:key id} [comment-component author text])])
(defn handle-comment-on-click [comments comment]
(let [author (trim (:author @comment))
text (trim (:text @comment))]
(reset! comment {:author "" :text ""})
(when-not (or (blank? author) (blank? text))
(swap! comments conj {:id (.getTime (js/Date.)) :author author :text text}))))
(defn comment-form [comments]
(let [comment (r/atom {:author "" :text ""})]
(fn [comments]
[:form
[:input {:type "text"
:placeholder "Your name"
:value (:author @comment)
:on-change #(swap! comment assoc :author (-> %
.-target
.-value))}]
[:input {:type "text"
:placeholder "Say something"
:value (:text @comment)
:on-change #(swap! comment assoc :text (-> %
.-target
.-value))}]
[:input {:type "button"
:value "Post"
:on-click #(handle-comment-on-click comments comment)}]])))
(defn comment-box [comments]
[:div
[:h1 "Comments"]
[comment-list comments]
[comment-form comments]])
(defn ^:export init []
(render [comment-box data] (by-id "content")))
Now that we have defined and exported the init
function, we can call
it in the reagent.html
file as follows:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Reagent Tutorial</title>
<link rel="stylesheet" href="css/base.css" />
</head>
<body>
<div id="content"></div>
<script src="main.js"></script>
<script>modern_cljs.reagent.init();</script>
</body>
</html>
As soon as you save the reagent.html
file, it gets reloaded and you
can start adding new comments by using the comment-form
component.
Before leaving the tutorial, I suggest you to kill all the active processes and commit your work:
cd /path/to/react-tutorial
git commit -am "Part II"
cd /path/to/modern-cljs
git commit -am "Part II"
Stay tuned for the next tutorial, because the way we solved the local
state for the comment-form
can be improved by adopting a different
communication strategy.
Copyright © Mimmo Cosenza, 2012-16. Released under the Eclipse Public License, the same as Clojure.