Merge branch 'master' of github.com:reagent-project/reagent into extends-component

This commit is contained in:
Hendrik Poernama 2018-12-16 12:02:30 +07:00
commit 7b4d5ff9c1
29 changed files with 2519 additions and 3833 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## Unreleased
**[compare](https://github.com/reagent-project/reagent/compare/v0.8.1...master)**
- Fix using metadata to set React key with Fragment shortcut (`:<>`) ([#401](https://github.com/reagent-project/reagent/issues/401))
## 0.8.1 (2018-05-15)
**[compare](https://github.com/reagent-project/reagent/compare/v0.8.0...v0.8.1)**

View File

@ -1,6 +1,8 @@
# Reagent
![Reagent-Project](logo.png)
A simple [ClojureScript](http://github.com/clojure/clojurescript) interface to [React](http://facebook.github.io/react/).
Reagent provides a way to write efficient React components using (almost) nothing but plain ClojureScript functions.
@ -12,6 +14,11 @@ Reagent provides a way to write efficient React components using (almost) nothin
* **Community discussion and support channels**
* **[#reagent](https://clojurians.slack.com/messages/reagent/)** channel in [Clojure Slack](http://clojurians.net/)
* **[Reagent Project Mailing List](https://groups.google.com/forum/#!forum/reagent-project)**
* **Commercial video material**
* [Learn Reagent Free](https://www.jacekschae.com/learn-reagent-free/tycit?coupon=REAGENT)
* [Learn Reagent Pro](https://www.jacekschae.com/learn-reagent-pro/tycit?coupon=REAGENT) (Affiliate link, $30 discount)
* [purelyfunctional.tv ](https://purelyfunctional.tv/guide/reagent/)
* [Lambda Island Videos](https://lambdaisland.com/collections/react-reagent-re-frame)
### Prerequisites
@ -28,7 +35,7 @@ If you wish to only create the assets for ClojureScript without a Clojure backen
lein new reagent-frontend myproject
This will setup a new Reagent project with some reasonable defaults, see here for more [details](https://github.com/reagent-project/reagent-template).
This will setup a new Reagent project with some reasonable defaults, see here for more [details](https://github.com/reagent-project/reagent-template).
To use Reagent in an existing project you add this to your dependencies in `project.clj`:
@ -51,7 +58,7 @@ Reagent uses [Hiccup-like](https://github.com/weavejester/hiccup) markup instead
(defn some-component []
[:div
[:h3 "I am a component!"]
[:p.someclass
[:p.someclass
"I have " [:strong "bold"]
[:span {:style {:color "red"}} " and red"]
" text."]])
@ -64,15 +71,15 @@ Reagent extends standard Hiccup in one way: it is possible to "squeeze" elements
[:p
[:b "Nested Element"]]]
```
can be written as:
```clj
[:div>p>b "Nested Element"]
```
```
> **Since version 0.8:** The `:class` attribute also supports collections of classes, and nil values are removed:
>
>
> ```clj
> [:div {:class ["a-class" (when active? "active") "b-class"]}]
> ```

View File

@ -128,7 +128,7 @@
[:p "The goal of Reagent is to make it possible to define
arbitrarily complex UIs using just a couple of basic concepts,
and to be fast enough by default that you rarely have to care
and to be fast enough by default that you rarely have to think
about performance."]
[:p "A very basic Reagent component may look something like this: "]

View File

@ -7,19 +7,8 @@ how you want to provide React.
|---|---|---|---|
| Cljsjs | `:none` | Supported | Requires Cljs 1.10.238+ |
| Cljsjs | `:advanced` | Supported | Requires Cljs 1.10.238+ |
| `node modules` | `:none` | Known problems (1) | Supported |
| `node modules` | `:advanced` | Known problems (2) | Supported |
While Reagent 0.8 supports use with React from npm, there are known problems:
1. Closure can't properly handle React 16 CommonJS module pattern: https://github.com/google/closure-compiler/issues/2841
This causes the production React code being loaded even for development builds.
Using Chrome React Developer Tools with this setup will break Reagent.
2. Closure optimization currently breaks certain statically created objects which are
accessed dynamically in `ReactDOM/server`: https://github.com/facebook/react/issues/12368
Fixed by using `[com.google.javascript/closure-compiler-unshaded "v20180319"]` ([fix commit](https://github.com/google/closure-compiler/commit/c13cf48b98477e44409dba6359246bffa95b1c7b)), will be
the default in next ClojureScript release.
| `node modules` | `:none` | Requires Cljs 1.10.312 | Supported |
| `node modules` | `:advanced` | Requires Cljs 1.10.312 | Supported |
## Browser - Cljsjs
@ -54,28 +43,23 @@ will in these cases rename the statically object properties, which will break
dynamically accessing the objects. Externs fix this by defining which properties
must not be renamed.
## Browser - loading React from CDNJS or custom Webpack bundle
## Browser - Webpack
**TODO: Not tested properly**
https://clojurescript.org/guides/webpack
If you want to load React.js yourself from external JS file (CDN) or from custom bundle,
it should be possible to override the Cljsjs foreign-libs, while still using externs from Cljsjs packages. To override the foreign-libs, you can provide following compiler option:
```clj
:foreign-libs
[{:file "empty.js",
[{:file "bundje.js",
:provides ["react" "react-dom" "create-react-class" "react-dom/server"],
:requires [],
:global-exports {react React
react-dom ReactDOM
create-react-class createReactClass
react-dom/server ReactDOMServer}}]
```
You'll also need to create the mentioned `empty.js` file (FIXME: relative to `project.clj`?).
If your bundle provides other libraries, you could extern `:provides` and `:global-exports` (e.g. `prop-types`).
## NodeJS - Cljsjs
Requires https://github.com/clojure/clojurescript/commit/f7d611d87f6ea8a605eae7c0339f30b79a840b49
@ -106,3 +90,17 @@ If you have one npm package installed, e.g. `react`, you also need
to provide others (`react-dom` and `create-react-class`), else
Cljsjs packages would be used for these, and packages from different sources
don't work together.
### Previous problems
Before ClojureScript 1.10.312 there were couple of problems with npm support:
1. Closure can't properly handle React 16 CommonJS module pattern: https://github.com/google/closure-compiler/issues/2841
This causes the production React code being loaded even for development builds.
Using Chrome React Developer Tools with this setup will break Reagent. Fixed by `[com.google.javascript/closure-compiler-unshaded "v20180610"]` ([PR](https://github.com/google/closure-compiler/pull/2963)), will be
the included in next the ClojureScript release.
2. Closure optimization currently breaks certain statically created objects which are
accessed dynamically in `ReactDOM/server`: https://github.com/facebook/react/issues/12368
Fixed by using `[com.google.javascript/closure-compiler-unshaded "v20180319"]` ([fix commit](https://github.com/google/closure-compiler/commit/c13cf48b98477e44409dba6359246bffa95b1c7b)), will be
the default in next ClojureScript release.

View File

@ -9,7 +9,7 @@ This is good for all sorts of reasons:
* The new code does proper batching of renderings even when changes to atoms are done outside of event handlers (which is great for e.g core.async users).
* Repaints can be synced by the browser with for example CSS transitions, since Reagent uses requestAnimationFrame to do the batching. That makes for example animations smoother.
In short, Reagent renders less often, but at the right times. For a much better description of why async rendering is good, see David Nolens [excellent explanation here.](http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs/)
In short, Reagent renders less often, but at the right times. For a much better description of why async rendering is good, see David Nolens [excellent explanation here.](http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs)
## The bad news

View File

@ -87,6 +87,25 @@ That isn't valid Hiccup and you'll get a slightly baffling error. You'll have to
[:div name]]) ;; [:div] containing two nested [:divs]
```
Alternatively, you could return a [React Fragment](https://reactjs.org/docs/fragments.html). In reagent, a React Fragment is created using the `:<>` Hiccup form.
```cljs
(defn right-component
[name]
[:<>
[:div "Hello"]
[:div name]])
```
Referring to the example in [React's documentation](https://reactjs.org/docs/fragments.html), the `Columns` component could be defined in reagent as:
```cljs
(defn columns
[:<>
[:td "Hello"]
[:td "World"]]
```
## Form-2: A Function Returning A Function
Now, let's take one step up in complexity. Sometimes, a component requires:
@ -143,7 +162,9 @@ In my experience, you'll probably use `Form-3` `components` less than 1% of the
While the critical part of a component is its render function, sometimes we need to perform actions at various critical moments in a component's lifetime, like when it is first created, or when its about to be destroyed (removed from the DOM), or when its about to be updated, etc.
With `Form-3` components, you can nominate `lifecycle methods`. reagent provides a very thin layer over React's own `lifecycle methods`. So, before going on, [read all about React's lifecycle methods.](http://facebook.github.io/react/docs/component-specs.html#lifecycle-methods)
With `Form-3` components, you can nominate `lifecycle methods`. reagent provides a very thin layer over React's own `lifecycle methods`. So, before going on, [read all about React's lifecycle methods.](http://facebook.github.io/react/docs/component-specs.html#lifecycle-methods).
Because React's lifecycle methods are object-oriented, they presume the ability to access `this` to obtain the current state of the component. Accordingly, the signatures of the corresponding Reagent lifecycle methods all take a reference to the reagent component as the first argument. This reference can be used with `r/props`, `r/children`, and `r/argv` to obtain the current props/arguments. There are some unexpected details with these functions described below. You may also find `r/dom-node` helpful, as a common use of form-3 components is to draw into a `canvas` element, and you will need access to the underlying DOM element to do so.
A `Form-3` component definition looks like this:
```cljs
@ -152,15 +173,19 @@ A `Form-3` component definition looks like this:
(let [some (local but shared state) ;; <-- closed over by lifecycle fns
can (go here)]
(reagent/create-class ;; <-- expects a map of functions
{:component-did-mount ;; the name of a lifecycle function
#(println "component-did-mount") ;; your implementation
{:display-name "my-component" ;; for more helpful warnings & errors
:component-did-mount ;; the name of a lifecycle function
(fn [this]
(println "component-did-mount")) ;; your implementation
:component-will-mount ;; the name of a lifecycle function
#(println "component-will-mount") ;; your implementation
:component-did-update ;; the name of a lifecycle function
(fn [this old-argv] ;; reagent provides you the entire "argv", not just the "props"
(let [new-argv (rest (reagent/argv this))]
(do-something new-argv old-argv)))
;; other lifecycle funcs can go in here
:display-name "my-component" ;; for more helpful warnings & errors
:reagent-render ;; Note: is not :render
(fn [x y z] ;; remember to repeat parameters
@ -178,7 +203,13 @@ A `Form-3` component definition looks like this:
[my-component 1 2 3]]) ;; Be sure to put the Reagent class in square brackets to force it to render!
```
At the time of writing, the official reagent tutorial doesn't show how to do `Form-3` `components` in the way shown above, and instead suggests that you use `with-meta`, which is clumsy and inferior. So I won't show that method here, but be aware that an alternative way exists to achieve the same outcome.
Note the `old-argv` above in the signature for `component-did-mount`. Many of these Reagent lifecycle method analogs take `prev-argv` or `old-argv` (see the docstring for `reagent/create-class` for a full listing). These `argv` arguments include the component constructor as the first argument, which should generally be ignored. This is the same format returned by `(reagent/argv this)`.
Alternately, you can use `(reagent/props this)` and `(reagent/props children)`, but, conceptually, these don't map as clearly to the `argv` concept. Specifically, the arguments to your render function are actually passed as children (not props) to the underlying React component, **unless the first argument is a map.** If the first argument is a map, then that map is passed as props, and the rest of the arguments are passed as children. Using `props` and `children` may read a bit cleaner, but you do need to pay attention to whether you're passing a props map or not.
Finally, note that some React lifecycle methods take `prevState` and `nextState`. Because Reagent provides its own state management system, there is no access to these parameters in the lifecycle methods.
It is possible to create `Form-3` `components` using `with-meta`. However, `with-meta` is a bit clumsy and has no advantages over the above method, but be aware that an alternative way exists to achieve the same outcome.
**Rookie mistake**
@ -196,6 +227,8 @@ While you can override `component-should-update` to achieve some performance imp
Leaving out the `:display-name` entry. If you leave it out, Reagent and React have no way of knowing the name of the component causing a problem. As a result, the warnings and errors they generate won't be as informative.
*****************
## Final Note
Above I used the terms `Form-1`, `Form-2` and `Form-3`, but there's actually only one kind of component. It is just that there's **3 different ways to create a component**.

View File

@ -30,11 +30,11 @@ We'll start with a code fragment, because it is worth a 1000 words:
Notes:
1. This example uses a Form-2 component, which allows us to retain state outside of the renderer `fn`. The same technique would work with a Form-3 component.
2. We capture state in `!video`. In this example, the state we capture is a reference to a [video HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video).
2. `!video` is a `clojure.core/atom` and not a `reaagent.core/atom`. We don't use a normal atom because refs never change during the lifecycle of a component and if we used a reagent atom, it would cause an unnecessary re-render when the ref callback mutates the atom.
3. `!video` is a `clojure.core/atom` and not a `reaagent.core/atom`. We use a normal Clojure `atom` because refs never change during the lifecycle of a component and if we used a reagent atom, it would cause an unnecessary re-render when the ref callback mutates the atom.
4. On the `:video` component there's a `:ref` callback function which establishes the state in `!video`. You can attach a ref callback to any of the Hiccup elements.
5. Thereafter, `@!video` is used with the `:button's` `:on-click` to manipulate the `video` DOM methods.
5. For full notes [read Paulus' blog post](https://presumably.de/reagent-mysteries-part-3-manipulating-the-dom.html)
6. For more background on callback refs, see [React's documentation](https://facebook.github.io/react/docs/more-about-refs.html)
6. For full notes [read Paulus' blog post](https://presumably.de/reagent-mysteries-part-3-manipulating-the-dom.html)
7. For more background on callback refs, see [React's documentation](https://facebook.github.io/react/docs/more-about-refs.html)
***

View File

@ -94,11 +94,10 @@ Note:
Some React libraries use the decorator pattern: a React component which takes a component as an argument and returns a new component as its result. One example is the React DnD library. We will need to use both `adapt-react-class` and `reactify-component` to move back and forth between React and reagent:
```clojure
(defn react-dnd-component
[]
(def react-dnd-component
(let [decorator (DragDropContext HTML5Backend)]
[(reagent/adapt-react-class
(decorator (reagent/reactify-component top-level-component)))]))
(reagent/adapt-react-class
(decorator (reagent/reactify-component top-level-component)))))
```
This is the equivalent javascript:
@ -128,13 +127,13 @@ Some React components expect a function as their only child. React autosizer is
## Getting props and children of current component
Because you just pass argument to reagent functions, you typically don't need to think about "props" and "children" as distinct things. But reagent does make a distinction and it is helpful to understand this particularly when interoperating with native elements and React libraries.
Because you just pass arguments to reagent functions, you typically don't need to think about "props" and "children" as distinct things. But Reagent does make a distinction and it is helpful to understand this, particularly when interoperating with native elements and React libraries.
Specifically, if the first argument to your reagent function is a map, that is assigned to `this.props` of the underlying reagent component. All other arguments are assigned as children to `this.props.children`.
Specifically, if the first argument to your Reagent function is a map, that is assigned to `this.props` of the underlying Reagent component. All other arguments are assigned as children to `this.props.children`.
When interacting with native React components, it may be helpful to access props and children, which you can do with `reagent.core/current-component`. This function returns an object that allows you retrieve the props and children passed to the current component.
Beware that `current-component` is only valid in component functions, and must be called outside of e.g event handlers and for expressions, so its safest to always put the call at the top, as in `my-div` here:
Beware that `current-component` is only valid in component functions, and must be called outside of e.g event handlers and `for` expressions, so its safest to always put the call at the top, as in `my-div` here:
```clojure
(ns example
@ -154,6 +153,8 @@ Beware that `current-component` is only valid in component functions, and must b
## React Interop Macros
**Please do not use these macros. They will be removed at some point. Either use extern inference, externs or proper `goog.object/get`.**
Reagent provides two utility macros `$` and `$!` for getting and setting javascript properties in a way that is safe for advanced compilation.
`($ o :foo)` is equivalent to `(.-foo o)`
@ -164,3 +165,8 @@ Similarly,
`($! o :foo 1)` is equivalent to `(set! (.-foo o) 1)`
Note, these are not necessary if your JavaScript library has an externs file or if externs inference is on and working.
## Examples
- [Material-UI](../examples/material-ui/src/example/core.cljs)
- [React-sortable-hoc](../examples/react-sortable-hoc/src/example/core.cljs)

View File

@ -119,8 +119,8 @@ Cursors are created with `reagent/cursor`, which takes a ratom and a keypath (li
```clojure
;; First create a ratom
(def state (reagent/atom {:foo {:bar "BAR"}
:baz "BAZ"
:quux "QUUX"}))
:baz "BAZ"
:quux "QUUX"}))
;; Now create a cursor
(def bar-cursor (reagent/cursor state [:foo :bar]))
@ -167,7 +167,7 @@ When reactions produce a new result (as determined by `=`), they cause other dep
The function `make-reaction`, and its macro `reaction` are used to create a `Reaction`, which is a type that belongs to a number of protocols such as `IWatchable`, `IAtom`, `IReactiveAtom`, `IDeref`, `IReset`, `ISwap`, `IRunnable`, etc. which make it atom-like: ie it can be watched, derefed, reset, swapped on, and additionally, tracks its derefs, behave reactively, and so on.
Reactions are what give `r/atom`, `r/cursor`, and function `r/cursor` and `r/wrap` their power.
Reactions are what give `r/atom`, `r/cursor`, and `r/wrap` their power.
`make-reaction` takes one argument, `f`, and an optional options map. The options map specifies what happens to `f`:
@ -175,8 +175,39 @@ Reactions are what give `r/atom`, `r/cursor`, and function `r/cursor` and `r/wra
* `on-set` and `on-dispose` are run when the reaction is set and unset from the DOM
* `derefed` **TODO unclear**
**TODO EXAMPLE**
Reactions are very useful when
* You need a way in which a component only updates based on part of the ratom state. (reagent/cursor can also be used for this scenario)
* When you want to combine two `ratoms` and produce a result
* You want the component to use some transformed value of `ratom`
Here's an example:
```
(def app-state (reagent/atom {:state-var-1 {:var-a 2
:var-b 3}
:state-var-2 {:var-a 7
:var-b 9}}))
(def app-var2a-reaction (reagent.ratom/make-reaction
#(get-in @app-state [:state-var-2 :var-a])))
(defn component-using-make-reaction []
[:div
[:div "component-using-make-reaction"]
[:div "state-var-2 - var-a : " @app-var2a-reaction]])
```
The below example uses `reagent.ratom/reaction` macro, which provides syntactic sugar compared to
using plain `make-reaction`:
```
(let [username (reagent/atom "")
password (reagent/atom "")
fields-populated? (reagent.ratom/reaction (every? not-empty [@username @password]))]
[:div "Is username and password populated ?" @fields-populated?])
```
Reactions are executed asynchronously, so be sure to call `flush` if you depend on reaction side effects.
## The track function
@ -191,8 +222,8 @@ Here's an example:
(ns example.core
(:require [reagent.core :as r]))
(defonce app-state (r/atom {:people
{1 {:name "John Smith"}
2 {:name "Maggie Johnson"}}}))
{1 {:name "John Smith"}
2 {:name "Maggie Johnson"}}}))
(defn people []
(:people @app-state))

View File

@ -13,13 +13,15 @@ Also:
* [Reagent Deep Dive Series by Timothy Pratley](http://timothypratley.blogspot.com.au/p/p.html) - a four part series
* [Reagent Mysteries series by Paulus Esterhazy](https://presumably.de/) - a four part series
* [Props, Children & Component Lifecycle](https://www.martinklepsch.org/posts/props-children-and-component-lifecycle-in-reagent.html) by Martin Klepsch
* [Using Stateful JS Components - like D3](https://github.com/Day8/re-frame/blob/masterUsing-Stateful-JS-Components.md) (external link)
* [Using Stateful JS Components - like D3](https://github.com/Day8/re-frame/blob/master/docs/Using-Stateful-JS-Components.md) (external link)
## Commercial Videos Series
* [Learn Reagent Free](https://www.jacekschae.com/learn-reagent-free/tycit?coupon=REAGENT)
* [Learn Reagent Pro](https://www.jacekschae.com/learn-reagent-pro/tycit?coupon=REAGENT) (Affiliate link, $30 discount)
* [purelyfunctional.tv ](https://purelyfunctional.tv/guide/reagent/)
* [Lambda Island Videos](https://lambdaisland.com/collections/react-reagent-re-frame)
## Frequently Asked Questions
1. [Why isn't my Component re-rendering?](FAQ/ComponentNotRerendering.md)
@ -31,6 +33,9 @@ Also:
5. [How do I force Component re-creation?](https://groups.google.com/forum/#!topic/reagent-project/tNY4gzk7TUY) (external link)
6. [How do I access "props" in lifecycle methods?](http://nils-blum-oeste.net/clojurescripts-reagent-using-props-in-lifecycle-hooks/) (external link)
## Examples
- [MaterialUI v1 with working TextField](examples/material-ui.md)
### Want To Add An FAQ?

View File

@ -52,6 +52,46 @@ As of reagent 0.8.0, the `class` attribute accepts a collection of classes and w
[:div {:class ["a-class" (when active? "active") "b-class"]}]
```
## Special notation for id and class
The id of an element can be indicated with a hash (`#`) after the name of the element.
This:
```clojure
[:div#my-id]
```
is the same as this:
```clojure
[:div {:id "my-id"}]
```
One or more classes can indicated for an element with a `.` and the class-name like this:
```clojure
[:div.my-class.my-other-class.etc]
```
which is the same as:
```clojure
[:div {:class ["my-class" "my-other-class" "etc"]}]
```
Special notations for id and classes can be used together. The id must be listed first:
```clojure
[:div#my-id.my-class.my-other-class]
```
which is the same as:
```clojure
[:div {:id "my-id" :class ["my-class" "my-other-class"]}]
```
## Special notation for nested elements
Reagent extends standard Hiccup in one way: it is possible to "squeeze" elements together by using a `>` character.

View File

@ -123,8 +123,9 @@ And, finally, a Form-2 parent Component which uses these two child components:
(fn parent-renderer
[]
[:div
[more-button counter] ;; no @ on counter
[greet-number @counter]]))) ;; notice the @. The prop is an int
[greet-number @counter] ;; notice the @. The prop is an int
[more-button counter]]))) ;; no @ on counter
```
With this setup, answer this question: what rerendering happens each time the `more-button` gets clicked and `counter` gets incremented?
@ -194,9 +195,9 @@ So this notion of "changed" is pretty important. It controls if we are doing un
### Lifecycle Functions
When `props` change, the entire underlying React machinery is engaged. React Components can have lifecycle methods like `component-did-update` and these functions will get called, just as they would if you were dealing with a React Component.
When `props` change, the entire underlying React machinery is engaged. Reagent Components can have lifecycle methods like `component-did-update` and these functions will get called, just as they would if you were dealing with a React Component.
But ... when the rerender is re-run because an input ratom changed, **Lifecycle functions are not run**. So, for example, `component-did-update` will not be called on the Component.
But ... when the re-render occurs because an input ratom changed, **Lifecycle functions are not run**. So, for example, `component-did-update` will not be called on the Component.
Careful of this one. It trips people up.

View File

@ -15,6 +15,8 @@
["Why is my attribute (like autoFocus) missing?" {:file "doc/FAQ/MyAttributesAreMissing.md"}]
["How can I use React's dangerouslySetInnerHTML?" {:file "doc/FAQ/dangerouslySetInnerHTML.md"}]
["Reagent doesn't work after updating dependencies" {:file "doc/FAQ/CljsjsReactProblems.md"}]]
["Examples" {}
["Material-UI v1" {:file "doc/examples/material-ui.md"}]]
["Other" {}
["0.8 Upgrade guide" {:file "doc/0.8-upgrade.md"}]
["Development guide" {:file "doc/development.md"}]]]}

View File

@ -0,0 +1,65 @@
# Material-UI
[Example project](../../examples/material-ui/)
Material-UI [TextField](https://material-ui.com/api/text-field/) has for long
time caused problems for Reagent users. The problem is that `TextField` wraps the
`input` element inside a component so that Reagent is not able to enable
input cursor fixes, which are required due to [async rendering](http://reagent-project.github.io/news/reagent-is-async.html).
Good news is that Material-UI v1 has a property that can be used to provide
the input component to `TextField`:
```cljs
(ns example.material-ui
(:require ["material-ui" :as mui]
[reagent.core :as r]))
(def text-field (r/adapt-react-class mui/TextField))
(def value (r/atom ""))
(def input-component
(r/reactify-component
(fn [props]
[:input (-> props
(assoc :ref (:inputRef props))
(dissoc :inputRef))])))
(def example []
[text-field
{:value @value
:on-change #(reset! value (.. e -target -value))
:InputProps {:inputComponent input-component}}])
```
`reactify-component` can be used to convert Reagent component into React component,
which can then be passed into Material-UI. The component should be created once
(i.e. on top level) to ensure it is not unnecessarily redefined, causing the
component to be re-mounted.
For some reason Material-UI uses different name for `ref`, so the `inputRef` property
should be renamed by the input component.
## Wrapping for easy use
Instead of providing `:InputProps :inputComponent` option to every `TextField`,
it is useful to wrap the `TextField` component in a way that the option is added always:
```cljs
(defn text-field [props & children]
(let [props (-> props
(assoc-in [:InputProps :inputComponent] input-component)
rtpl/convert-prop-value)]
(apply r/create-element mui/TextField props (map r/as-element children))))
```
Here `r/create-element` and `reagent.impl.template/convert-prop-value` achieve
the same as what `adapt-react-class` does, but allows modifying the props.
**Check the example project for complete code.** Some additional logic is
required to ensure option like `:multiline` and `:select` work correctly,
as they affect how the `inputComponent` should work.
TODO: `:multiline` `TextField` without `:rows` (i.e. automatic height) doesn't
work, because that requires Material-UI `Input/Textarea`, which doesn't work
with Reagent cursor fix.

View File

@ -0,0 +1,48 @@
(defproject material-ui-reagent "0.6.0"
:dependencies [[org.clojure/clojure "1.9.0"]
[org.clojure/clojurescript "1.10.439"]
[reagent "0.8.1"]
[figwheel "0.5.17"]
[cljsjs/material-ui "3.2.0-0"]
[cljsjs/material-ui-icons "3.0.1-0"]]
:plugins [[lein-cljsbuild "1.1.7"]
[lein-figwheel "0.5.17"]]
:figwheel {:repl false
:http-server-root "public"}
:profiles {:dev {:resource-paths ["target/cljsbuild/client" "target/cljsbuild/client-npm"]}}
:cljsbuild
{:builds
{:client
{:source-paths ["src"]
:figwheel true
:compiler {:parallel-build true
:source-map true
:optimizations :none
:main "example.core"
:output-dir "target/cljsbuild/client/public/js/out"
:output-to "target/cljsbuild/client/public/js/main.js"
:asset-path "js/out"
:npm-deps false}}
;; FIXME: Doesn't work due to Closure bug with scoped npm packages
:client-npm
{:source-paths ["src"]
:figwheel true
:compiler {:parallel-build true
:source-map true
:optimizations :none
:main "example.core"
:output-dir "target/cljsbuild/client-npm/public/js/out"
:output-to "target/cljsbuild/client-npm/public/js/main.js"
:asset-path "js/out"
:install-deps true
:npm-deps {react "16.6.0"
react-dom "16.6.0"
create-react-class "15.6.3"
"@material-ui/core" "3.1.1"
"@material-ui/icons" "3.0.1"}
:process-shim true}}}})

View File

@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Example</title>
</head>
<body>
<div id="app">
<h1>Reagent example app see README.md</h1>
</div>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,171 @@
(ns example.core
(:require [reagent.core :as r]
;; Scoped names require Cljs 1.10.439
["@material-ui/core" :as mui]
["@material-ui/core/styles" :refer [createMuiTheme withStyles]]
["@material-ui/core/colors" :as mui-colors]
["@material-ui/icons" :as mui-icons]
[goog.object :as gobj]
[reagent.impl.template :as rtpl]))
;; TextField cursor fix:
(def ^:private input-component
(r/reactify-component
(fn [props]
[:input (-> props
(assoc :ref (:inputRef props))
(dissoc :inputRef))])))
(def ^:private textarea-component
(r/reactify-component
(fn [props]
[:textarea (-> props
(assoc :ref (:inputRef props))
(dissoc :inputRef))])))
;; To fix cursor jumping when controlled input value is changed,
;; use wrapper input element created by Reagent instead of
;; letting Material-UI to create input element directly using React.
;; Create-element + convert-props-value is the same as what adapt-react-class does.
(defn text-field [props & children]
(let [props (-> props
(assoc-in [:InputProps :inputComponent] (cond
(and (:multiline props) (:rows props) (not (:maxRows props)))
textarea-component
;; FIXME: Autosize multiline field is broken.
(:multiline props)
nil
;; Select doesn't require cursor fix so default can be used.
(:select props)
nil
:else
input-component))
rtpl/convert-prop-value)]
(apply r/create-element mui/TextField props (map r/as-element children))))
;; Example
(def custom-theme
(createMuiTheme
#js {:palette #js {:primary #js {:main (gobj/get (.-red mui-colors) 100)}}}))
(defn custom-styles [theme]
#js {:button #js {:margin (.. theme -spacing -unit)}
:textField #js {:width 200
:marginLeft (.. theme -spacing -unit)
:marginRight (.. theme -spacing -unit)}})
(def with-custom-styles (withStyles custom-styles))
(defonce text-state (r/atom "foobar"))
;; Props in cljs but classes in JS object
(defn form [{:keys [classes] :as props}]
[:> mui/Grid
{:container true
:direction "column"
:spacing 16}
[:> mui/Grid {:item true}
[:> mui/Toolbar
{:disable-gutters true}
[:> mui/Button
{:variant "contained"
:color "primary"
:class (.-button classes)
:on-click #(swap! text-state str " foo")}
"Update value property"
[:> mui-icons/AddBox]]
[:> mui/Button
{:variant "outlined"
:color "secondary"
:class (.-button classes)
:on-click #(reset! text-state "")}
"Reset"
[:> mui-icons/Clear]]]]
[:> mui/Grid {:item true}
[text-field
{:value @text-state
:label "Text input"
:placeholder "Placeholder"
:helper-text "Helper text"
:class (.-textField classes)
:on-change (fn [e]
(reset! text-state (.. e -target -value)))
:inputRef #(js/console.log "input-ref" %)}]]
[:> mui/Grid {:item true}
[text-field
{:value @text-state
:label "Textarea"
:placeholder "Placeholder"
:helper-text "Helper text"
:class (.-textField classes)
:on-change (fn [e]
(reset! text-state (.. e -target -value)))
:multiline true
;; TODO: Autosize textarea is broken.
:rows 10}]]
[:> mui/Grid {:item true}
[text-field
{:value @text-state
:label "Select"
:placeholder "Placeholder"
:helper-text "Helper text"
:class (.-textField classes)
:on-change (fn [e]
(reset! text-state (.. e -target -value)))
:select true}
[:> mui/MenuItem
{:value 1}
"Item 1"]
;; Same as previous, alternative to adapt-react-class
[:> mui/MenuItem
{:value 2}
"Item 2"]]]
[:> mui/Grid {:item true}
[:> mui/Grid
{:container true
:direction "row"
:spacing 8}
;; For properties that require React Node as parameter,
;; either use r/as-element to convert Reagent hiccup forms into React elements,
;; or use r/create-element to directly instantiate element from React class (i.e. non-adapted React component).
[:> mui/Grid {:item true}
[:> mui/Chip
{:icon (r/as-element [:> mui-icons/Face])
:label "Icon element example, r/as-element"}]]
[:> mui/Grid {:item true}
[:> mui/Chip
{:icon (r/create-element mui-icons/Face)
:label "Icon element example, r/create-element"}]]]]])
(defn main []
;; fragment
[:<>
[:> mui/CssBaseline]
[:> mui/MuiThemeProvider
{:theme custom-theme}
[:> mui/Grid
{:container true
:direction "row"
:justify "center"}
[:> mui/Grid
{:item true
:xs 6}
[:> (with-custom-styles (r/reactify-component form))]]]]])
(defn start []
(r/render [main] (js/document.getElementById "app")))
(start)

View File

@ -0,0 +1,28 @@
(defproject reagent/react-sortable-hoc-example "0.1.0"
:dependencies [[org.clojure/clojure "1.9.0"]
[org.clojure/clojurescript "1.10.339"]
[reagent "0.8.1"]
[figwheel "0.5.16"]
[cljsjs/react-sortable-hoc "0.8.2-0"]]
:plugins [[lein-cljsbuild "1.1.7"]
[lein-figwheel "0.5.16"]]
:figwheel {:repl false
:http-server-root "public"}
:profiles {:dev {:resource-paths ["target/cljsbuild/client" "target/cljsbuild/client-npm"]}}
:cljsbuild
{:builds
{:client
{:source-paths ["src"]
:figwheel true
:compiler {:parallel-build true
:source-map true
:optimizations :none
:main "example.core"
:output-dir "target/cljsbuild/client/public/js/out"
:output-to "target/cljsbuild/client/public/js/main.js"
:asset-path "js/out"
:npm-deps false}}}})

View File

@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Example</title>
</head>
<body>
<div id="app">
<h1>Reagent example app see README.md</h1>
</div>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,75 @@
(ns example.core
(:require [reagent.core :as r]
;; FIXME: add global-exports support
[cljsjs.react-sortable-hoc]
[goog.object :as gobj]))
;; Adapted from https://github.com/clauderic/react-sortable-hoc/blob/master/examples/drag-handle.js#L10
(def DragHandle
(js/SortableHOC.SortableHandle.
;; Alternative to r/reactify-component, which doens't convert props and hiccup,
;; is to just provide fn as component and use as-element or create-element
;; to return React elements from the component.
(fn []
(r/as-element [:span "::"]))))
(def SortableItem
(js/SortableHOC.SortableElement.
(r/reactify-component
(fn [{:keys [value]}]
[:li
[:> DragHandle]
value]))))
;; Alternative without reactify-component
;; props is JS object here
#_
(def SortableItem
(js/SortableHOC.SortableElement.
(fn [props]
(r/as-element
[:li
[:> DragHandle]
(.-value props)]))))
(def SortableList
(js/SortableHOC.SortableContainer.
(r/reactify-component
(fn [{:keys [items]}]
[:ul
(for [[value index] (map vector items (range))]
;; No :> or adapt-react-class here because that would convert value to JS
(r/create-element
SortableItem
#js {:key (str "item-" index)
:index index
:value value}))]))))
(defn vector-move [coll prev-index new-index]
(let [items (into (subvec coll 0 prev-index)
(subvec coll (inc prev-index)))]
(-> (subvec items 0 new-index)
(conj (get coll prev-index))
(into (subvec items new-index)))))
(comment
(= [0 2 3 4 1 5] (vector-move [0 1 2 3 4 5] 1 4)))
(defn sortable-component []
(let [items (r/atom (vec (map (fn [i] (str "Item " i)) (range 6))))]
(fn []
(r/create-element
SortableList
#js {:items @items
:onSortEnd (fn [event]
(swap! items vector-move (.-oldIndex event) (.-newIndex event)))
:useDragHandle true}))))
(defn main []
[sortable-component])
(defn start []
(r/render [main] (js/document.getElementById "app")))
(start)

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

8
logo.svg Normal file
View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
<path stroke="#000" stroke-width="10.751" stroke-linejoin="round" paint-order="markers fill stroke" d="M5.375 5.375h53.249v53.249H5.375z"/>
<path d="M16.734 59.233h30.532c1.857 0 3.134-.5 3.714-1.4.58-1 .465-2.1-.58-3.5l-13.35-18.1v-9.1h1.741c.465 0 .813-.1 1.161-.4.348-.3.464-.6.464-1s-.116-.7-.464-1c-.348-.3-.696-.4-1.16-.4H25.208c-.465 0-.813.1-1.161.4-.348.3-.464.6-.464 1s.116.7.464 1c.348.3.696.4 1.16.4h1.742v9.1l-13.35 18.1c-1.045 1.4-1.16 2.5-.58 3.5.696.9 1.973 1.4 3.714 1.4zm13.118-21.5l.58-.7v-9.9H33.8v9.9l.58.7 13.467 18.6H16.502z" fill="#303c3c"/>
<path fill="#00d8ff" d="M35.889 42.633h-7.778l-9.055 12.6H45.06z"/>
<path d="M180 439.416a5.09 5.09 0 1 1-10.179 0 5.09 5.09 0 1 1 10.179 0z" transform="matrix(.56482 0 0 .5518 -69.11 -222.593)" fill="#00d8ff" stroke="#00d8ff"/>
<path d="M37.217 12.657a2.325 2.325 0 1 1-4.65 0 2.325 2.325 0 1 1 4.65 0z" fill="#00d8ff" stroke="#00d8ff" stroke-width=".457"/>
<path d="M31.676 6.053a1.17 1.17 0 1 0-2.341 0 1.17 1.17 0 1 0 2.341 0z" fill="#00d8ff" stroke="#00d8ff" stroke-width=".23"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

5556
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,16 +3,16 @@
"dependencies": {
"@cljs-oss/module-deps": "1.1.1",
"create-react-class": "15.6.3",
"prop-types": "15.6.1",
"react": "16.3.2",
"react-dom": "16.3.2"
"prop-types": "15.6.2",
"react": "16.6.0",
"react-dom": "16.6.0"
},
"devDependencies": {
"gzip-size-cli": "^2.1.0",
"karma": "2.0.2",
"gzip-size-cli": "3.0.0",
"karma": "3.1.1",
"karma-chrome-launcher": "2.2.0",
"karma-cljs-test": "0.1.0",
"karma-junit-reporter": "1.2.0",
"md5-file": "^4.0.0"
"md5-file": "4.0.0"
}
}

View File

@ -7,15 +7,15 @@
;; If :npm-deps enabled, these are used only for externs.
;; Without direct react dependency, other packages,
;; like react-leaflet might have closer dependency to a other version.
[cljsjs/react "16.3.2-0"]
[cljsjs/react-dom "16.3.2-0"]
[cljsjs/react-dom-server "16.3.2-0"]
[cljsjs/create-react-class "15.6.3-0"]]
[cljsjs/react "16.6.0-0"]
[cljsjs/react-dom "16.6.0-0"]
[cljsjs/react-dom-server "16.6.0-0"]
[cljsjs/create-react-class "15.6.3-1"]]
:plugins [[lein-cljsbuild "1.1.7"]
[lein-doo "0.1.10"]
[lein-codox "0.10.3"]
[lein-figwheel "0.5.16"]]
[lein-figwheel "0.5.17"]]
:source-paths ["src"]
@ -23,11 +23,10 @@
:exclude clojure.string
:source-paths ["src"]}
:profiles {:dev {:dependencies [[org.clojure/clojurescript "1.10.238"]
[figwheel "0.5.16"]
:profiles {:dev {:dependencies [[org.clojure/clojurescript "1.10.439"]
[figwheel "0.5.17"]
[doo "0.1.10"]
[com.google.javascript/closure-compiler-unshaded "v20180319"]
[cljsjs/prop-types "15.6.1-0"]]
[cljsjs/prop-types "15.6.2-0"]]
:source-paths ["demo" "test" "examples/todomvc/src" "examples/simple/src" "examples/geometry/src"]
:resource-paths ["site" "target/cljsbuild/client" "target/cljsbuild/client-npm"]}}
@ -54,8 +53,8 @@
:main "reagentdemo.dev"
:output-dir "target/cljsbuild/client/public/js/out"
:output-to "target/cljsbuild/client/public/js/main.js"
:asset-path "js/out"
:npm-deps false}}
:npm-deps false
:asset-path "js/out"}}
{:id "client-npm"
:source-paths ["demo"]
@ -66,6 +65,7 @@
:main "reagentdemo.dev"
:output-dir "target/cljsbuild/client-npm/public/js/out"
:output-to "target/cljsbuild/client-npm/public/js/main.js"
:npm-deps true
:asset-path "js/out"}}
{:id "test"
@ -87,6 +87,7 @@
:asset-path "js/out"
:output-dir "target/cljsbuild/test-npm/out"
:output-to "target/cljsbuild/test-npm/main.js"
:npm-deps true
:aot-cache true}}
;; Separate source-path as this namespace uses Node built-in modules which
@ -97,6 +98,7 @@
:target :nodejs
:output-dir "target/cljsbuild/prerender/out"
:output-to "target/cljsbuild/prerender/main.js"
:npm-deps true
:aot-cache true}}
{:id "node-test"
@ -120,6 +122,7 @@
:optimizations :none
:output-dir "target/cljsbuild/node-test-npm/out"
:output-to "target/cljsbuild/node-test-npm/main.js"
:npm-deps true
:aot-cache true}}
;; With :advanched source-paths doesn't matter that much as
@ -147,6 +150,7 @@
:output-to "target/cljsbuild/prod-npm/public/js/main.js"
:output-dir "target/cljsbuild/prod-npm/out" ;; Outside of public, not published
:closure-warnings {:global-this :off}
:npm-deps true
:aot-cache true}}
{:id "prod-test"
@ -171,4 +175,5 @@
:output-to "target/cljsbuild/prod-test-npm/main.js"
:output-dir "target/cljsbuild/prod-test-npm/out"
:closure-warnings {:global-this :off}
:npm-deps true
:aot-cache true}}]})

View File

@ -86,7 +86,14 @@
;;; Rendering
(defn wrap-render [c]
(defn wrap-render
"Calls the render function of the component `c`. If result `res` evaluates to a:
1) Vector (form-1 component) - Treats the vector as hiccup and returns
a react element with a render function based on that hiccup
2) Function (form-2 component) - updates the render function to `res` i.e. the internal function
and calls wrap-render again (`recur`), until the render result doesn't evaluate to a function.
3) Anything else - Returns the result of evaluating `c`"
[c]
(let [f ($ c :reagentRender)
_ (assert-callable f)
res (if (true? ($ c :cljsLegacyRender))

View File

@ -323,9 +323,10 @@
(let [props (nth argv 1 nil)
hasprops (or (nil? props) (map? props))
jsprops (convert-prop-value (if hasprops props))
jsprops (if-some [key (key-from-vec argv)]
(oset jsprops "key" key)
jsprops)
first-child (+ 1 (if hasprops 1 0))]
(when-some [key (key-from-vec argv)]
(oset jsprops "key" key))
(make-element argv react/Fragment jsprops first-child)))
(defn adapt-react-class
@ -343,20 +344,20 @@
(aset tag-name-cache x (parse-tag x))))
(defn native-element [parsed argv first]
(let [comp ($ parsed :name)]
(let [props (nth argv first nil)
hasprops (or (nil? props) (map? props))
jsprops (convert-props (if hasprops props) parsed)
first-child (+ first (if hasprops 1 0))]
(if (input-component? comp)
(-> [(reagent-input) argv comp jsprops first-child]
(with-meta (meta argv))
as-element)
(let [key (-> (meta argv) get-key)
p (if (nil? key)
jsprops
(oset jsprops "key" key))]
(make-element argv comp p first-child))))))
(let [comp ($ parsed :name)
props (nth argv first nil)
hasprops (or (nil? props) (map? props))
jsprops (convert-props (if hasprops props) parsed)
first-child (+ first (if hasprops 1 0))]
(if (input-component? comp)
(-> [(reagent-input) argv comp jsprops first-child]
(with-meta (meta argv))
as-element)
(let [key (-> (meta argv) get-key)
p (if (nil? key)
jsprops
(oset jsprops "key" key))]
(make-element argv comp p first-child)))))
(defn str-coll [coll]
(if (dev?)

View File

@ -33,10 +33,20 @@
false))))))
(defn- in-context [obj f]
"When f is executed, if (f) derefs any ratoms, they are then added to 'obj.captured'(*ratom-context*).
See function notify-deref-watcher! to know how *ratom-context* is updated"
(binding [*ratom-context* obj]
(f)))
(defn- deref-capture [f r]
(defn- deref-capture
"Returns `(in-context f r)`. Calls `_update-watching` on r with any
`deref`ed atoms captured during `in-context`, if any differ from the
`watching` field of r. Clears the `dirty?` flag on r.
Inside '_update-watching' along with adding the ratoms in 'r.watching' of reaction,
the reaction is also added to the list of watches on each ratoms f derefs."
[f r]
(set! (.-captured r) nil)
(when (dev?)
(set! (.-ratomGeneration r) (set! generation (inc generation))))
@ -48,7 +58,11 @@
(._update-watching r c))
res))
(defn- notify-deref-watcher! [derefed]
(defn- notify-deref-watcher!
"Add `derefed` to the `captured` field of `*ratom-context*`.
See also `in-context`"
[derefed]
(when-some [r *ratom-context*]
(let [c (.-captured r)]
(if (nil? c)
@ -337,7 +351,15 @@
(defn- handle-reaction-change [this sender old new]
(._handle-change this sender old new))
;; Fields of a Reaction javascript object
;; - auto_run
;; - captured
;; - caught
;; - f
;; - ratomGeneration
;; - state
;; - watches
;; - watching
(deftype Reaction [f ^:mutable state ^:mutable ^boolean dirty? ^boolean nocache?
^:mutable watching ^:mutable watches ^:mutable auto-run
^:mutable caught]
@ -499,7 +521,16 @@
(def ^:private temp-reaction (make-reaction nil))
(defn run-in-reaction [f obj key run opts]
(defn run-in-reaction
"Evaluates `f` and returns the result. If `f` calls `deref` on any ratoms,
creates a new Reaction that watches those atoms and calls `run` whenever
any of those watched ratoms change. Also, the new reaction is added to
list of 'watches' of each of the ratoms. The `run` parameter is a function
that should expect one argument. It is passed `obj` when run. The `opts`
are any options accepted by a Reaction and will be set on the newly created
Reaction. Sets the newly created Reaction to the `key` on `obj`."
[f obj key run opts]
(let [r temp-reaction
res (deref-capture f r)]
(when-not (nil? (.-watching r))

View File

@ -1189,8 +1189,12 @@
[:div "hello"]
[:div "world"]]
^{:key 2}
[children])])]
(is (= "<div><div>hello</div><div>world</div><div>foo</div></div>"
[children]
^{:key 3}
[:<>
[:div "1"]
[:div "2"]])])]
(is (= "<div><div>hello</div><div>world</div><div>foo</div><div>1</div><div>2</div></div>"
(as-string [comp]))))))
(defonce my-context (react/createContext "default"))