-
Notifications
You must be signed in to change notification settings - Fork 12
Step 12: consuming JavaScript components
Browse code - Diff - Live demo
To reset your workspace for this step, run git checkout step-12
.
You will probably need to restart you Figwheel process and install the dependencies. Type ^C in your terminal to stop your Figwheel build process, then run :
lein do deps, clean, figwheel
In this step, we'll add some animations over our components. It will be the occasion to call native JavaScript libraries from ClojureScript:
- We'll use React's CSSTransitionGroup to apply animations.
- We'll use BootStrap's Carousel, a jQuery plugin.
You need a basic understanding of React to read this section.
React provides an addon for animations, which exposes a component named CSSTransitionGroup
. We'll use it to animate our phones list and page transitions.
First, we'll add React with Addons to our dependencies:
:dependencies [;; ...
[cljsjs/react-with-addons "0.13.3-0"]
[reagent "0.5.0" :exclusions [cljsjs/react]]
;; ...
]
(Notice that we have to add :exclusions [cljsjs/react]
to reagent, this prevents some collisions when building our project for production. You don't need to worry about that for now).
Now let's see how we can use this to add some movement animation to our list of phones
(ns reagent-phonecat.core
;; ...
(:require ;; ...
[cljsjs.react :as react])
)
;; ...
;; here we define a Reagent component which adapts the native React component
(def CSSTransitionGroup (rg/adapt-react-class (.. js/React -addons -CSSTransitionGroup)))
;; ...
(defn <phones-list> "An unordered list of phones"
[phones-list search order-prop]
[:ul.phones
[CSSTransitionGroup {:transition-name "phone-listing"} ;; we wrap our phones list in the CSSTransitionGroup component
(for [phone (->> phones-list
(filter #(matches-search? search %))
(sort-by order-prop))]
^{:key (:id phone)} [<phone-item> phone]
)]])
Thanks to reagent.core/adapt-react-class
, we can put the CSSTransitionGroup
in our Reagent rendering function; note that the first argument to CSSTransitionGroup
must be a map that corresponds to the React Props passed to the component.
How does CSSTransitionGroup
work? It will watch it's children nodes and add .phone-listing-enter
, .phone-listing-enter-active
, .phone-listing-move
, .phone-listing-move
, etc. on the children nodes when the list changes. All we have to do is add transitions to our CSS based on these classes:
// transitions
// phone list transition
.phone-listing-enter,
.phone-listing-leave,
.phone-listing-move {
-webkit-transition: 0.5s linear all;
-moz-transition: 0.5s linear all;
-o-transition: 0.5s linear all;
transition: 0.5s linear all;
}
.phone-listing-enter,
.phone-listing-move {
opacity: 0;
height: 0;
overflow: hidden;
}
.phone-listing-move.phone-listing-active,
.phone-listing-enter.phone-listing-enter-active {
opacity: 1;
height: 120px;
}
.phone-listing-leave {
opacity: 1;
overflow: hidden;
}
.phone-listing-leave.phone-listing-leave-active {
opacity: 0;
height: 0;
padding-top: 0;
padding-bottom: 0;
}
Let's do this again, this time we'll add a fade-out/fade-in transition when we change pages:
(defn <top-cpnt> []
(let [{:keys [page params]} @navigational-state]
[:div.container-fluid
[:div.view-container
[CSSTransitionGroup {:transition-name "view-frame"}
(case page
:phones ^{:key :phones} [<phones-list-page>]
:phone (let [phone-id (:phone-id params)]
^{:key :phone} [<phone-page> phone-id])
^{:key :not-found} [:div "This page does not exist"]
)
]]
]))
Notice how we specify the :key
metadata to our child components so that CSSTransitionGroup
can track them.
And the corresponding CSS :
// fading page transitions
.view-container {
position: relative;
}
.view-frame-enter, .view-frame-leave {
background: white;
position: absolute;
top: 0;
left: 0;
right: 0;
}
.view-frame-enter {
-webkit-animation: 0.5s fade-in;
-moz-animation: 0.5s fade-in;
-o-animation: 0.5s fade-in;
animation: 0.5s fade-in;
z-index: 100;
}
.view-frame-leave {
-webkit-animation: 0.5s fade-out;
-moz-animation: 0.5s fade-out;
-o-animation: 0.5s fade-out;
animation: 0.5s fade-out;
z-index:99;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@-moz-keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@-moz-keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@-webkit-keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
We'll go even further in interop by using a jQuery plugin in our views. We'll replace the pictures in our phone details page with a Bootstrap Carousel.
Wrapping a jQuery plugin requires us to get closer the the React/DOM metal than what we've been so far. To do this, we must declare a React component in a way more similar to what JavaScript React developers usually do: defining a React class with lifecycle methods, in our case using the lifecycle methods to access the native DOM.
Reagent provides us a reagent.core/create-class
function to do this:
(declare ;; here we declare our components to define their in an order that feels natural.
;; ...
<phone-detail-cpnt>
<phone-carousel>
)
;; ...
(def <phone-carousel>
(rg/create-class
;; we can still use our classic Reagent API for the rendering function.
{:reagent-render
(fn [images]
[:div {:id "phone-pictures-carousel" :class "carousel slide"}
[:ol {:class "carousel-indicators"}
(->> images
(map-indexed (fn [i _]
^{:key i} [:li {:data-target "#phone-pictures-carousel" :data-slide-to (str i) :class (when (= i 0) "active")}]))
doall)]
[:div {:class "carousel-inner" :role "listbox"}
(->> images
(map-indexed (fn [i img]
^{:key i} [:div {:class (str "item " (when (= i 0) "active"))}
[:img.phone-carousel-img {:src img}]]))
doall)]
[:a {:class "left carousel-control" :href "#phone-pictures-carousel" :role "button" :data-slide "prev"}
[:span {:class "glyphicon glyphicon-chevron-left" :aria-hidden "true"}]
[:span {:class "sr-only"} "Previous"]]
[:a {:class "right carousel-control" :href "#phone-pictures-carousel" :role "button" :data-slide "next"}
[:span {:class "glyphicon glyphicon-chevron-right" :aria-hidden "true"}]
[:span {:class "sr-only"} "Next"]]
])
;; once the component is mounted onto the DOM, we can use this lifecycle method to access the native DOM
:component-did-mount (fn [this]
(let [e (js/jQuery (rg/dom-node this))]
(-> e (aget "carousel") (.call e)) ;; this looks awkward, but is necessary for advanced compilation. We could not have written (.carousel d), it would have failed in advanced compilation.
))
}))
;; ...
(defn <phone-detail-cpnt> [phone]
(let [{:keys [images name description availability additionalFeatures]
;; ...
} phone]
[:div
[:span.phone-carousel-container [<phone-carousel> images]]
;; ...
]))
Note that our <phone-detail-cpnt>
no longer needs to be stateful (the jQuery carousel instance is the state holder now).
Try to put back the thumbnails in the phone details, so that when you click on a thumbnail it causes the Carousel to transition to the appropriate slide (using the .carousel(number)
method).
This is the end of this tutorial, hopefully you now have enough knowledge to start building rich client applications in ClojureScript and React.
If something is not clear, or you would like to suggest an improvement, do not hesitate to file an issue to this repository.
Have fun!