mirror of
https://github.com/status-im/reagent.git
synced 2025-01-12 21:05:20 +00:00
Merge branch 'master' into fix-array-ops
This commit is contained in:
commit
16d8021e89
32
CHANGELOG.md
32
CHANGELOG.md
@ -1,10 +1,34 @@
|
||||
# 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))
|
||||
- Create React Component without `create-react-class` ([#416](https://github.com/reagent-project/reagent/issues/416))
|
||||
- Allow any number of arguments for `reagent.core/merge-props` and
|
||||
ensure `:class` is merged correctly when it is defined as collection. ([#412](https://github.com/reagent-project/reagent/issues/412))
|
||||
- Add `reagent.core/class-names` utility functions which can be used
|
||||
to normalize and combine `:class` values (similar to `classnames` JS library)
|
||||
- Fix comparing Reagent `PartialFn` to `nil` ([#385](https://github.com/reagent-project/reagent/issues/385))
|
||||
|
||||
## 0.8.1 (2018-05-15)
|
||||
|
||||
**[compare](https://github.com/reagent-project/reagent/compare/v0.8.0...v0.8.1)**
|
||||
|
||||
- Fix problem which caused using e.g. `:class` property with custom HTML element to break normal elements
|
||||
- Fix problem using keyword or symbol as `:class` together with element tag class shorthand, e.g. `[:p.a {:class :b}]` ([#367](https://github.com/reagent-project/reagent/issues/367))
|
||||
- Added support for using keywords and symbols in `:class` collection
|
||||
- Removed component type assertion for `:>` ([#369](https://github.com/reagent-project/reagent/issues/369), [#372](https://github.com/reagent-project/reagent/pull/372))
|
||||
- This caused problems with React Context where component is Plain JS object with special properties
|
||||
- `React/createElement` will still provide error if `:>` is used with invalid values
|
||||
- Handle exceptions from `not=` in Reagent `component-did-update` method ([#350](https://github.com/reagent-project/reagent/pull/350), [#344](https://github.com/reagent-project/reagent/pull/344))
|
||||
|
||||
## 0.8.0 (2018-04-19)
|
||||
|
||||
**[compare](https://github.com/reagent-project/reagent/compare/v0.8.0-rc1...v0.8.0)**
|
||||
|
||||
- Reagent documentation is now maintained as part of the repository, in [docs](./docs) folder.
|
||||
- Reagent documentation is now maintained as part of the repository, in [doc](./doc) folder.
|
||||
- Default to React 16
|
||||
- Apply vector metadata to the outermost element when using nesting shorthand ([#262](https://github.com/reagent-project/reagent/issues/262))
|
||||
- Add `:<>` shorthand for [React Fragments](https://reactjs.org/docs/fragments.html) ([#352](https://github.com/reagent-project/reagent/pull/352)])
|
||||
@ -13,7 +37,7 @@
|
||||
- Added `IWithMeta` to `RAtom` ([#314](https://github.com/reagent-project/reagent/pull/314))
|
||||
- Support for using Reagent together with React from npm
|
||||
|
||||
#### Read [0.8 upgrade guide](./docs/0.8-upgrade.md) for more information.
|
||||
#### Read [0.8 upgrade guide](./doc/0.8-upgrade.md) for more information.
|
||||
|
||||
## 0.8.0-rc1 (2018-04-11)
|
||||
|
||||
@ -21,7 +45,7 @@
|
||||
|
||||
Unless defaulting to React 16 causes problems, final release should follow soon.
|
||||
|
||||
- Reagent documentation is now maintained as part of the repository, in [docs](./docs) folder.
|
||||
- Reagent documentation is now maintained as part of the repository, in [doc](./doc) folder.
|
||||
- Default to React 16
|
||||
- Apply vector metadata to the outermost element when using nesting shorthand ([#262](https://github.com/reagent-project/reagent/issues/262))
|
||||
- Add `:<>` shorthand for [React Fragments](https://reactjs.org/docs/fragments.html) ([#352](https://github.com/reagent-project/reagent/pull/352)])
|
||||
@ -106,7 +130,7 @@ React-with-addons bundle [has been deprecated](https://facebook.github.io/react/
|
||||
of that package. The latest React-with-addons version won't work with Reagent 0.8.
|
||||
For animation utils use [react-transition-group](https://github.com/cljsjs/packages/tree/master/react-transition-group) package instead. [React-dom/test-utils](https://facebook.github.io/react/docs/test-utils.html) and [react-addons-perf](https://facebook.github.io/react/docs/perf.html) are not currently packaged as browserified files, so their use would require Webpack, or they might work with Closure module processing (TODO: Provide example).
|
||||
|
||||
#### Read [0.8 upgrade guide](./docs/0.8-upgrade.md) for more information.
|
||||
#### Read [0.8 upgrade guide](./doc/0.8-upgrade.md) for more information.
|
||||
|
||||
#### Which libraries work together with Reagent 0.8:
|
||||
|
||||
|
21
README.md
21
README.md
@ -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.
|
||||
@ -8,10 +10,15 @@ Reagent provides a way to write efficient React components using (almost) nothin
|
||||
* **[Detailed intro with live examples](http://reagent-project.github.io/)**
|
||||
* **[News](http://reagent-project.github.io/news/index.html)**
|
||||
* **[API Documentation](http://reagent-project.github.io/docs/master/)**
|
||||
* **[Tutorials and FAQ](https://github.com/reagent-project/reagent/tree/master/docs)**
|
||||
* **[Tutorials and FAQ](https://github.com/reagent-project/reagent/tree/master/doc)**
|
||||
* **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"]}]
|
||||
> ```
|
||||
|
@ -28,6 +28,7 @@ test ! -e tmp/js/out
|
||||
cd tmp
|
||||
|
||||
# Restore files not created by this script
|
||||
git add docs/master/
|
||||
git checkout -- README.md docs/
|
||||
git add .
|
||||
git commit -m "Built site from $SHA"
|
||||
|
@ -55,7 +55,10 @@ workflows:
|
||||
version: 2
|
||||
test-and-update-site:
|
||||
jobs:
|
||||
- test
|
||||
- test:
|
||||
filters:
|
||||
tags:
|
||||
only: /v.*/
|
||||
- update-site:
|
||||
requires:
|
||||
- test
|
||||
|
@ -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: "]
|
||||
|
@ -13,6 +13,9 @@
|
||||
|
||||
(defn main []
|
||||
[:div
|
||||
[:div.reagent-demo
|
||||
[:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#081-2018-05-15"} "Reagent 0.8.1"]]
|
||||
[:span "2018-05-15"]]
|
||||
[:div.reagent-demo
|
||||
[:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#080-2018-04-19"} "Reagent 0.8.0"]]
|
||||
[:span "2018-04-19"]]
|
||||
|
@ -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,33 +43,28 @@ 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
|
||||
|
||||
Available in 1.0.238
|
||||
Available in 1.10.238
|
||||
|
||||
Reagent should use Cljsjs libraries by default even when running on Node.
|
||||
|
||||
@ -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.
|
@ -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 Nolen’s [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 Nolen’s [excellent explanation here.](http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs)
|
||||
|
||||
## The bad news
|
||||
|
@ -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**.
|
@ -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)
|
||||
|
||||
***
|
||||
|
@ -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 it’s 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 it’s 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)
|
@ -1,6 +1,6 @@
|
||||
# Managing state: atoms, cursors, Reactions, and tracking
|
||||
|
||||
Although it is possible to update reagent componetns by remounting the entire component tree with `react.core/render`, Reagent comes with a sophisticated state management library based on `reagent.core/atom`, which allows components to track application state and update only when needed. Reagent also provides cursors, which are like ratoms but can be constructed from portions of one or more other ratoms to limit or expand which ratoms a component watches. Finally, Reagent provides a set of tracking primitives called reactions and a set of utility functions to build more customized state management.
|
||||
Although it is possible to update reagent components by remounting the entire component tree with `react.core/render`, Reagent comes with a sophisticated state management library based on `reagent.core/atom`, which allows components to track application state and update only when needed. Reagent also provides cursors, which are like ratoms but can be constructed from portions of one or more other ratoms to limit or expand which ratoms a component watches. Finally, Reagent provides a set of tracking primitives called reactions and a set of utility functions to build more customized state management.
|
||||
|
||||
**TODO is this right?**
|
||||
|
||||
@ -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))
|
@ -1,12 +1,12 @@
|
||||
## Tutorials
|
||||
|
||||
1. [Using Hiccup to Describe HTML](/docs/UsingHiccupToDescribeHTML.md)
|
||||
2. [Creating Reagent Components](/docs/CreatingReagentComponents.md)
|
||||
3. [Using [square brackets] instead of (parentheses)](/docs/UsingSquareBracketsInsteadOfParens.md)
|
||||
4. [When do components update?](/docs/WhenDoComponentsUpdate.md)
|
||||
5. [[WIP] Managing State: atoms, cursors, Reactions, and tracking](/docs/ManagingState.md)
|
||||
6. [Batching and Timing: How Reagent Renders Changes to Application State](/docs/BatchingAndTiming.md)
|
||||
7. [Interop with React](/docs/InteropWithReact.md)
|
||||
1. [Using Hiccup to Describe HTML](UsingHiccupToDescribeHTML.md)
|
||||
2. [Creating Reagent Components](CreatingReagentComponents.md)
|
||||
3. [Using [square brackets] instead of (parentheses)](UsingSquareBracketsInsteadOfParens.md)
|
||||
4. [When do components update?](WhenDoComponentsUpdate.md)
|
||||
5. [[WIP] Managing State: atoms, cursors, Reactions, and tracking](ManagingState.md)
|
||||
6. [Batching and Timing: How Reagent Renders Changes to Application State](BatchingAndTiming.md)
|
||||
7. [Interop with React](InteropWithReact.md)
|
||||
|
||||
Also:
|
||||
* [purelyfunctional.tv ](https://purelyfunctional.tv/guide/reagent/) - an excellent, written tutorial
|
||||
@ -17,9 +17,11 @@ Also:
|
||||
|
||||
## 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?
|
||||
|
||||
@ -40,5 +45,5 @@ Many Thanks!! We'd like that:
|
||||
|
||||
#### Misc Docs
|
||||
|
||||
- [0.8-upgrade](/docs/0.8-upgrade.md)
|
||||
- [development](/docs/development.md)
|
||||
- [0.8-upgrade](0.8-upgrade.md)
|
||||
- [development](development.md)
|
@ -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.
|
@ -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?
|
||||
@ -186,7 +187,7 @@ The `=` version is more accurate, more intuitive, but potentially more expensive
|
||||
|
||||
### Efficient Re-renders
|
||||
|
||||
Its only via rerenders that a UI will change. So re-rendering is pretty essential.
|
||||
It's only via rerenders that a UI will change. So re-rendering is pretty essential.
|
||||
|
||||
On the other hand, unnecessary re-rendering should be avoided. In the worst case, it could lead to performance problems. By unnecessary rendering, I mean rerenders which result in unchanged HTML. That's a whole lot of work for no reason.
|
||||
|
||||
@ -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.
|
||||
|
22
doc/cljdoc.edn
Normal file
22
doc/cljdoc.edn
Normal file
@ -0,0 +1,22 @@
|
||||
{:cljdoc.api/platforms #{"cljs"}
|
||||
:cljdoc.doc/tree [["Documentation index" {:file "doc/README.md"}]
|
||||
["Tutorials" {}
|
||||
["Using Hiccup to Describe HTML" {:file "doc/UsingHiccupToDescribeHTML.md"}]
|
||||
["Creating Reagent Components" {:file "doc/CreatingReagentComponents.md"}]
|
||||
["Using [square brackets] instead of (parentheses)" {:file "doc/UsingSquareBracketsInsteadOfParens.md"}]
|
||||
["When do components update?" {:file "doc/WhenDoComponentsUpdate.md"}]
|
||||
["[WIP] Managing State: atoms, cursors, Reactions, and tracking" {:file "doc/ManagingState.md"}]
|
||||
["Batching and Timing: How Reagent Renders Changes to Application State" {:file "doc/BatchingAndTiming.md"}]
|
||||
["Interop with React" {:file "doc/InteropWithReact.md"}]]
|
||||
["Frequently Asked Questions" {}
|
||||
["Why isn't my Component re-rendering?" {:file "doc/FAQ/ComponentNotRerendering.md"}]
|
||||
["How do I use React's \"refs\"" {:file "doc/FAQ/UsingRefs.md"}]
|
||||
["How can I use an entity like \"nbsp\"?" {:file "doc/FAQ/UsingAnEntity.md"}]
|
||||
["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"}]]]}
|
65
doc/examples/material-ui.md
Normal file
65
doc/examples/material-ui.md
Normal 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.
|
47
examples/material-ui/project.clj
Normal file
47
examples/material-ui/project.clj
Normal file
@ -0,0 +1,47 @@
|
||||
(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"
|
||||
"@material-ui/core" "3.1.1"
|
||||
"@material-ui/icons" "3.0.1"}
|
||||
:process-shim true}}}})
|
13
examples/material-ui/resources/public/index.html
Normal file
13
examples/material-ui/resources/public/index.html
Normal 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>
|
171
examples/material-ui/src/example/core.cljs
Normal file
171
examples/material-ui/src/example/core.cljs
Normal 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)
|
28
examples/react-sortable-hoc/project.clj
Normal file
28
examples/react-sortable-hoc/project.clj
Normal 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}}}})
|
13
examples/react-sortable-hoc/resources/public/index.html
Normal file
13
examples/react-sortable-hoc/resources/public/index.html
Normal 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>
|
75
examples/react-sortable-hoc/src/example/core.cljs
Normal file
75
examples/react-sortable-hoc/src/example/core.cljs
Normal 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)
|
8
logo.svg
Normal file
8
logo.svg
Normal 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 |
5157
package-lock.json
generated
5157
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -2,17 +2,16 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@cljs-oss/module-deps": "1.1.1",
|
||||
"create-react-class": "15.6.2",
|
||||
"prop-types": "15.6.0",
|
||||
"react": "16.3.0",
|
||||
"react-dom": "16.3.0"
|
||||
"prop-types": "15.6.2",
|
||||
"react": "16.6.0",
|
||||
"react-dom": "16.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gzip-size-cli": "^2.1.0",
|
||||
"karma": "2.0.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
76
project.clj
76
project.clj
@ -1,4 +1,4 @@
|
||||
(defproject reagent "0.8.0"
|
||||
(defproject reagent "0.8.2-SNAPSHOT"
|
||||
:url "http://github.com/reagent-project/reagent"
|
||||
:license {:name "MIT"}
|
||||
:description "A simple ClojureScript interface to React"
|
||||
@ -7,15 +7,14 @@
|
||||
;; 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.0-1"]
|
||||
[cljsjs/react-dom "16.3.0-1"]
|
||||
[cljsjs/react-dom-server "16.3.0-1"]
|
||||
[cljsjs/create-react-class "15.6.2-0"]]
|
||||
[cljsjs/react "16.6.0-0"]
|
||||
[cljsjs/react-dom "16.6.0-0"]
|
||||
[cljsjs/react-dom-server "16.6.0-0"]]
|
||||
|
||||
:plugins [[lein-cljsbuild "1.1.7"]
|
||||
[lein-doo "0.1.10"]
|
||||
[lein-codox "0.10.3"]
|
||||
[lein-figwheel "0.5.15"]]
|
||||
[lein-figwheel "0.5.18"]]
|
||||
|
||||
:source-paths ["src"]
|
||||
|
||||
@ -23,11 +22,10 @@
|
||||
:exclude clojure.string
|
||||
:source-paths ["src"]}
|
||||
|
||||
:profiles {:dev {:dependencies [[org.clojure/clojurescript "1.10.238"]
|
||||
[figwheel "0.5.15"]
|
||||
:profiles {:dev {:dependencies [[org.clojure/clojurescript "1.10.439"]
|
||||
[figwheel "0.5.18"]
|
||||
[doo "0.1.10"]
|
||||
[com.google.javascript/closure-compiler-unshaded "v20180319"]
|
||||
[cljsjs/prop-types "15.6.0-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"]}}
|
||||
|
||||
@ -35,9 +33,7 @@
|
||||
|
||||
:figwheel {:http-server-root "public" ;; assumes "resources"
|
||||
:css-dirs ["site/public/css"]
|
||||
:repl false
|
||||
;; :npm-deps and :stable-names
|
||||
:validate-config false}
|
||||
:repl false}
|
||||
|
||||
;; No profiles and merging - just manual configuration for each build type.
|
||||
;; For :optimization :none ClojureScript compiler will compile all
|
||||
@ -47,8 +43,8 @@
|
||||
;; In future :main alone should be enough to find entry file.
|
||||
:cljsbuild
|
||||
{:builds
|
||||
{:client
|
||||
{:source-paths ["demo"]
|
||||
[{:id "client"
|
||||
:source-paths ["demo"]
|
||||
:watch-paths ["src" "demo" "test"]
|
||||
:figwheel true
|
||||
:compiler {:parallel-build true
|
||||
@ -56,12 +52,12 @@
|
||||
: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
|
||||
:asset-path "js/out"
|
||||
:checked-arrays :warn}}
|
||||
|
||||
:client-npm
|
||||
{:source-paths ["demo"]
|
||||
{:id "client-npm"
|
||||
:source-paths ["demo"]
|
||||
:watch-paths ["src" "demo" "test"]
|
||||
:figwheel true
|
||||
:compiler {:parallel-build true
|
||||
@ -69,11 +65,12 @@
|
||||
: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"
|
||||
:checked-arrays :warn}}
|
||||
|
||||
:test
|
||||
{:source-paths ["test"]
|
||||
{:id "test"
|
||||
:source-paths ["test"]
|
||||
:compiler {:parallel-build true
|
||||
:optimizations :none
|
||||
:main "reagenttest.runtests"
|
||||
@ -84,29 +81,31 @@
|
||||
:aot-cache true
|
||||
:checked-arrays :warn}}
|
||||
|
||||
:test-npm
|
||||
{:source-paths ["test"]
|
||||
{:id "test-npm"
|
||||
:source-paths ["test"]
|
||||
:compiler {:parallel-build true
|
||||
:optimizations :none
|
||||
:main "reagenttest.runtests"
|
||||
: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
|
||||
:checked-arrays :warn}}
|
||||
|
||||
;; Separate source-path as this namespace uses Node built-in modules which
|
||||
;; aren't available for other targets, and would break other builds.
|
||||
:prerender
|
||||
{:source-paths ["prerender"]
|
||||
{:id "prerender"
|
||||
:source-paths ["prerender"]
|
||||
:compiler {:main "sitetools.prerender"
|
||||
:target :nodejs
|
||||
:output-dir "target/cljsbuild/prerender/out"
|
||||
:output-to "target/cljsbuild/prerender/main.js"
|
||||
:npm-deps true
|
||||
:aot-cache true}}
|
||||
|
||||
:node-test
|
||||
{:source-paths ["test/reagenttest/runtests.cljs"]
|
||||
{:id "node-test"
|
||||
:source-paths ["test/reagenttest/runtests.cljs"]
|
||||
:watch-paths ["src" "test"]
|
||||
:compiler {:main "reagenttest.runtests"
|
||||
:target :nodejs
|
||||
@ -118,8 +117,8 @@
|
||||
:aot-cache true
|
||||
:checked-arrays :warn}}
|
||||
|
||||
:node-test-npm
|
||||
{:source-paths ["test/reagenttest/runtests.cljs"]
|
||||
{:id "node-test-npm"
|
||||
:source-paths ["test/reagenttest/runtests.cljs"]
|
||||
:watch-paths ["src" "test"]
|
||||
:compiler {:main "reagenttest.runtests"
|
||||
:target :nodejs
|
||||
@ -127,13 +126,14 @@
|
||||
: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
|
||||
:checked-arrays :warn}}
|
||||
|
||||
;; With :advanched source-paths doesn't matter that much as
|
||||
;; Cljs compiler will only read :main file.
|
||||
:prod
|
||||
{:source-paths ["demo"]
|
||||
{:id "prod"
|
||||
:source-paths ["demo"]
|
||||
:compiler {:main "reagentdemo.prod"
|
||||
:optimizations :advanced
|
||||
:elide-asserts true
|
||||
@ -145,8 +145,8 @@
|
||||
:npm-deps false
|
||||
:aot-cache true}}
|
||||
|
||||
:prod-npm
|
||||
{:source-paths ["demo"]
|
||||
{:id "prod-npm"
|
||||
:source-paths ["demo"]
|
||||
:compiler {:main "reagentdemo.prod"
|
||||
:optimizations :advanced
|
||||
:elide-asserts true
|
||||
@ -155,10 +155,11 @@
|
||||
: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}}
|
||||
|
||||
:prod-test
|
||||
{:source-paths ["test"]
|
||||
{:id "prod-test"
|
||||
:source-paths ["test"]
|
||||
:compiler {:main "reagenttest.runtests"
|
||||
:optimizations :advanced
|
||||
:elide-asserts true
|
||||
@ -170,8 +171,8 @@
|
||||
:aot-cache true
|
||||
:checked-arrays :warn}}
|
||||
|
||||
:prod-test-npm
|
||||
{:source-paths ["test"]
|
||||
{:id "prod-test-npm"
|
||||
:source-paths ["test"]
|
||||
:compiler {:main "reagenttest.runtests"
|
||||
:optimizations :advanced
|
||||
:elide-asserts true
|
||||
@ -180,5 +181,6 @@
|
||||
: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
|
||||
:checked-arrays :warn}}}})
|
||||
:checked-arrays :warn}}]})
|
||||
|
@ -1,9 +1,10 @@
|
||||
(ns reagent.core
|
||||
(:require [reagent.ratom :as ra]))
|
||||
|
||||
(defmacro with-let [bindings & body]
|
||||
(defmacro with-let
|
||||
"Bind variables as with let, except that when used in a component
|
||||
the bindings are only evaluated once. Also takes an optional finally
|
||||
clause at the end, that is executed when the component is
|
||||
destroyed."
|
||||
[bindings & body]
|
||||
`(ra/with-let ~bindings ~@body))
|
||||
|
@ -22,12 +22,16 @@
|
||||
that any Reagent hiccup forms must be processed with as-element. For example
|
||||
like this:
|
||||
|
||||
(r/create-element \"div\" #js{:className \"foo\"}
|
||||
\"Hi \" (r/as-element [:strong \"world!\"])
|
||||
```cljs
|
||||
(r/create-element \"div\" #js{:className \"foo\"}
|
||||
\"Hi \" (r/as-element [:strong \"world!\"])
|
||||
```
|
||||
|
||||
which is equivalent to
|
||||
|
||||
[:div.foo \"Hi\" [:strong \"world!\"]]"
|
||||
```cljs
|
||||
[:div.foo \"Hi\" [:strong \"world!\"]]
|
||||
```"
|
||||
([type]
|
||||
(create-element type nil))
|
||||
([type props]
|
||||
@ -102,15 +106,17 @@
|
||||
"Create a component, React style. Should be called with a map,
|
||||
looking like this:
|
||||
|
||||
{:get-initial-state (fn [this])
|
||||
:component-will-receive-props (fn [this new-argv])
|
||||
:should-component-update (fn [this old-argv new-argv])
|
||||
:component-will-mount (fn [this])
|
||||
:component-did-mount (fn [this])
|
||||
:component-will-update (fn [this new-argv])
|
||||
:component-did-update (fn [this old-argv])
|
||||
:component-will-unmount (fn [this])
|
||||
:reagent-render (fn [args....])} ;; or :render (fn [this])
|
||||
```cljs
|
||||
{:get-initial-state (fn [this])
|
||||
:component-will-receive-props (fn [this new-argv])
|
||||
:should-component-update (fn [this old-argv new-argv])
|
||||
:component-will-mount (fn [this])
|
||||
:component-did-mount (fn [this])
|
||||
:component-will-update (fn [this new-argv])
|
||||
:component-did-update (fn [this old-argv])
|
||||
:component-will-unmount (fn [this])
|
||||
:reagent-render (fn [args....])} ;; or :render (fn [this])
|
||||
```
|
||||
|
||||
Everything is optional, except either :reagent-render or :render."
|
||||
[spec]
|
||||
@ -118,7 +124,7 @@
|
||||
|
||||
|
||||
(defn current-component
|
||||
"Returns the current React component (a.k.a this) in a component
|
||||
"Returns the current React component (a.k.a `this`) in a component
|
||||
function."
|
||||
[]
|
||||
comp/*current-component*)
|
||||
@ -131,14 +137,14 @@
|
||||
|
||||
(defn state
|
||||
"Returns the state of a component, as set with replace-state or set-state.
|
||||
Equivalent to (deref (r/state-atom this))"
|
||||
Equivalent to `(deref (r/state-atom this))`"
|
||||
[this]
|
||||
(assert-component this)
|
||||
(deref (state-atom this)))
|
||||
|
||||
(defn replace-state
|
||||
"Set state of a component.
|
||||
Equivalent to (reset! (state-atom this) new-state)"
|
||||
Equivalent to `(reset! (state-atom this) new-state)`"
|
||||
[this new-state]
|
||||
(assert-component this)
|
||||
(assert-new-state new-state)
|
||||
@ -146,7 +152,7 @@
|
||||
|
||||
(defn set-state
|
||||
"Merge component state with new-state.
|
||||
Equivalent to (swap! (state-atom this) merge new-state)"
|
||||
Equivalent to `(swap! (state-atom this) merge new-state)`"
|
||||
[this new-state]
|
||||
(assert-component this)
|
||||
(assert-new-state new-state)
|
||||
@ -187,11 +193,27 @@
|
||||
[this]
|
||||
(dom/dom-node this))
|
||||
|
||||
(defn class-names
|
||||
"Function which normalizes and combines class values to a string
|
||||
|
||||
Reagent allows classes to be defined as:
|
||||
- Strings
|
||||
- Named objects (Symbols or Keywords)
|
||||
- Collections of previous types"
|
||||
([])
|
||||
([class] (util/class-names class))
|
||||
([class1 class2] (util/class-names class1 class2))
|
||||
([class1 class2 & others] (apply util/class-names class1 class2 others)))
|
||||
|
||||
(defn merge-props
|
||||
"Utility function that merges two maps, handling :class and :style
|
||||
specially, like React's transferPropsTo."
|
||||
[defaults props]
|
||||
(util/merge-props defaults props))
|
||||
"Utility function that merges some maps, handling `:class` and `:style`.
|
||||
|
||||
The :class value is always normalized (using `class-names`) even if no
|
||||
merging is done."
|
||||
([] (util/merge-props))
|
||||
([defaults] (util/merge-props defaults))
|
||||
([defaults props] (util/merge-props defaults props))
|
||||
([defaults props & others] (apply util/merge-props defaults props others)))
|
||||
|
||||
(defn flush
|
||||
"Render dirty components immediately to the DOM.
|
||||
@ -201,8 +223,6 @@
|
||||
[]
|
||||
(batch/flush))
|
||||
|
||||
|
||||
|
||||
;; Ratom
|
||||
|
||||
(defn atom
|
||||
@ -218,8 +238,8 @@
|
||||
Reagent atoms (or track, etc), the value will be updated whenever
|
||||
the atom changes.
|
||||
|
||||
In other words, @(track foo bar) will produce the same result
|
||||
as (foo bar), but foo will only be called again when the atoms it
|
||||
In other words, `@(track foo bar)` will produce the same result
|
||||
as `(foo bar)`, but foo will only be called again when the atoms it
|
||||
depends on changes, and will only trigger updates of components when
|
||||
its result changes.
|
||||
|
||||
@ -253,8 +273,10 @@
|
||||
|
||||
Use for example like this:
|
||||
|
||||
```cljs
|
||||
(wrap (:foo @state)
|
||||
swap! state assoc :foo)
|
||||
```
|
||||
|
||||
Probably useful only for passing to child components."
|
||||
[value reset-fn & args]
|
||||
@ -269,18 +291,23 @@
|
||||
|
||||
Behaves like a Reagent atom but focuses updates and derefs to
|
||||
the specified path within the wrapped Reagent atom. e.g.,
|
||||
(let [c (cursor ra [:nested :content])]
|
||||
... @c ;; equivalent to (get-in @ra [:nested :content])
|
||||
... (reset! c 42) ;; equivalent to (swap! ra assoc-in [:nested :content] 42)
|
||||
... (swap! c inc) ;; equivalence to (swap! ra update-in [:nested :content] inc)
|
||||
)
|
||||
|
||||
```cljs
|
||||
(let [c (cursor ra [:nested :content])]
|
||||
... @c ;; equivalent to (get-in @ra [:nested :content])
|
||||
... (reset! c 42) ;; equivalent to (swap! ra assoc-in [:nested :content] 42)
|
||||
... (swap! c inc) ;; equivalence to (swap! ra update-in [:nested :content] inc)
|
||||
)
|
||||
```
|
||||
|
||||
The first parameter can also be a function, that should look
|
||||
something like this:
|
||||
|
||||
(defn set-get
|
||||
([k] (get-in @state k))
|
||||
([k v] (swap! state assoc-in k v)))
|
||||
```cljs
|
||||
(defn set-get
|
||||
([k] (get-in @state k))
|
||||
([k v] (swap! state assoc-in k v)))
|
||||
```
|
||||
|
||||
The function will be called with one argument – the path passed to
|
||||
cursor – when the cursor is deref'ed, and two arguments (path and
|
||||
@ -288,7 +315,7 @@
|
||||
|
||||
Given that set-get function, (and that state is a Reagent atom, or
|
||||
another cursor) these cursors are equivalent:
|
||||
(cursor state [:foo]) and (cursor set-get [:foo]).
|
||||
`(cursor state [:foo])` and `(cursor set-get [:foo])`.
|
||||
|
||||
Note that a cursor is lazy: its value will not change until it is
|
||||
used. This may be noticed with add-watch."
|
||||
@ -299,7 +326,7 @@
|
||||
;; Utilities
|
||||
|
||||
(defn rswap!
|
||||
"Swaps the value of a to be (apply f current-value-of-atom args).
|
||||
"Swaps the value of a to be `(apply f current-value-of-atom args)`.
|
||||
|
||||
rswap! works like swap!, except that recursive calls to rswap! on
|
||||
the same atom are allowed – and it always returns nil."
|
||||
|
@ -1,5 +1,5 @@
|
||||
(ns reagent.impl.component
|
||||
(:require [create-react-class :as create-react-class]
|
||||
(:require [goog.object :as gobj]
|
||||
[react :as react]
|
||||
[reagent.impl.util :as util]
|
||||
[reagent.impl.batching :as batch]
|
||||
@ -88,7 +88,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 (.-reagentRender c)
|
||||
_ (assert-callable f)
|
||||
res (if (true? (.-cljsLegacyRender c))
|
||||
@ -152,9 +159,10 @@
|
||||
:getDefaultProps
|
||||
(throw (js/Error. "getDefaultProps not supported"))
|
||||
|
||||
;; In ES6 React, this is now part of the constructor
|
||||
:getInitialState
|
||||
(fn getInitialState []
|
||||
(this-as c (reset! (state-atom c) (.call f c c))))
|
||||
(fn getInitialState [c]
|
||||
(reset! (state-atom c) (.call f c c)))
|
||||
|
||||
:componentWillReceiveProps
|
||||
(fn componentWillReceiveProps [nextprops]
|
||||
@ -170,7 +178,10 @@
|
||||
new-argv (.-argv nextprops)
|
||||
noargv (or (nil? old-argv) (nil? new-argv))]
|
||||
(cond
|
||||
(nil? f) (or noargv (not= old-argv new-argv))
|
||||
(nil? f) (or noargv (try (not= old-argv new-argv)
|
||||
(catch :default e
|
||||
(warn "Exception thrown while comparing argv's in shouldComponentUpdate: " old-argv " " new-argv " " e)
|
||||
false)))
|
||||
noargv (.call f c c (get-argv c) (props-argv c nextprops))
|
||||
:else (.call f c c old-argv new-argv))))))
|
||||
|
||||
@ -261,17 +272,60 @@
|
||||
(set! (.-render obj) (:render static-fns))
|
||||
obj))
|
||||
|
||||
(defn map-to-js [m]
|
||||
(reduce-kv (fn [o k v]
|
||||
(doto o
|
||||
(aset (name k) v)))
|
||||
#js{} m))
|
||||
|
||||
(defn cljsify [body]
|
||||
(-> body
|
||||
camelify-map-keys
|
||||
add-obligatory
|
||||
wrap-funs))
|
||||
|
||||
(defn create-class [body]
|
||||
;; Idea from:
|
||||
;; https://gist.github.com/pesterhazy/2a25c82db0519a28e415b40481f84554
|
||||
;; https://gist.github.com/thheller/7f530b34de1c44589f4e0671e1ef7533#file-es6-class-cljs-L18
|
||||
|
||||
(def built-in-static-method-names
|
||||
[:childContextTypes :contextTypes
|
||||
:getDerivedStateFromProps :getDerivedStateFromError])
|
||||
|
||||
(defn create-class
|
||||
"Creates JS class based on provided Clojure map.
|
||||
|
||||
Map keys should use `React.Component` method names (https://reactjs.org/docs/react-component.html).
|
||||
Constructor function is defined using key `:getInitialState`.
|
||||
|
||||
React built-in static methods or properties are automatically defined as statics."
|
||||
[body]
|
||||
{:pre [(map? body)]}
|
||||
(->> body
|
||||
cljsify
|
||||
create-react-class))
|
||||
(let [body (cljsify body)
|
||||
methods (map-to-js (apply dissoc body :displayName :getInitialState built-in-static-method-names))
|
||||
static-methods (map-to-js (select-keys body built-in-static-method-names))
|
||||
display-name (:displayName body)
|
||||
construct (:getInitialState body)
|
||||
cmp (fn [props context updater]
|
||||
(this-as this
|
||||
(.call react/Component this props context updater)
|
||||
(when construct
|
||||
(construct this))
|
||||
this))]
|
||||
(gobj/extend (.-prototype cmp) (.-prototype react/Component) methods)
|
||||
(gobj/extend cmp react/Component static-methods)
|
||||
|
||||
(when display-name
|
||||
(set! (.-displayName cmp) display-name)
|
||||
(set! (.-cljs$lang$ctorStr cmp) display-name)
|
||||
(set! (.-cljs$lang$ctorPrWriter cmp)
|
||||
(fn [this writer opt]
|
||||
(cljs.core/-write writer display-name))))
|
||||
|
||||
(set! (.-cljs$lang$type cmp) true)
|
||||
(set! (.. cmp -prototype -constructor) cmp)
|
||||
|
||||
cmp))
|
||||
|
||||
(defn fiber-component-path [fiber]
|
||||
(let [name (some-> fiber
|
||||
|
@ -2,7 +2,7 @@
|
||||
(:require [react :as react]
|
||||
[clojure.string :as string]
|
||||
[clojure.walk :refer [prewalk]]
|
||||
[reagent.impl.util :as util :refer [is-client]]
|
||||
[reagent.impl.util :as util :refer [is-client named?]]
|
||||
[reagent.impl.component :as comp]
|
||||
[reagent.impl.batching :as batch]
|
||||
[reagent.ratom :as ratom]
|
||||
@ -22,10 +22,6 @@
|
||||
|
||||
;;; Common utilities
|
||||
|
||||
(defn ^boolean named? [x]
|
||||
(or (keyword? x)
|
||||
(symbol? x)))
|
||||
|
||||
(defn ^boolean hiccup-tag? [x]
|
||||
(or (named? x)
|
||||
(string? x)))
|
||||
@ -83,7 +79,7 @@
|
||||
(if-some [k' (cache-get custom-prop-name-cache (name k))]
|
||||
k'
|
||||
(let [v (util/dash-to-camel k)]
|
||||
(gobj/set prop-name-cache (name k) v)
|
||||
(gobj/set custom-prop-name-cache (name k) v)
|
||||
v))
|
||||
k))
|
||||
|
||||
@ -114,20 +110,12 @@
|
||||
|
||||
;; Merge classes
|
||||
class
|
||||
(assoc :class (let [old-class (:class props)]
|
||||
(if (nil? old-class) class (str class " " old-class)))))))
|
||||
|
||||
(defn stringify-class [{:keys [class] :as props}]
|
||||
(if (coll? class)
|
||||
(->> class
|
||||
(filter identity)
|
||||
(string/join " ")
|
||||
(assoc props :class))
|
||||
props))
|
||||
(assoc :class (util/class-names class (:class props))))))
|
||||
|
||||
(defn convert-props [props id-class]
|
||||
(let [props (-> props
|
||||
stringify-class
|
||||
(let [class (:class props)
|
||||
props (-> props
|
||||
(cond-> class (assoc :class (util/class-names class)))
|
||||
(set-id-class id-class))]
|
||||
(if (.-custom id-class)
|
||||
(convert-custom-prop-value props)
|
||||
@ -374,8 +362,6 @@
|
||||
0 (let [component (nth v 1 nil)]
|
||||
;; Support [:> component ...]
|
||||
(assert (= ">" n) (hiccup-err v "Invalid Hiccup tag"))
|
||||
(assert (or (string? component) (fn? component))
|
||||
(hiccup-err v "Expected React component in"))
|
||||
(native-element (->HiccupTag component nil nil nil) v 2))
|
||||
;; Support extended hiccup syntax, i.e :div.bar>a.foo
|
||||
;; Apply metadata (e.g. :key) to the outermost element.
|
||||
|
@ -99,20 +99,47 @@
|
||||
(apply pfn a b c d e f g h i j k l m n o p q r s t rest))
|
||||
IEquiv
|
||||
(-equiv [_ other]
|
||||
(and (= f (.-f other)) (= args (.-args other))))
|
||||
(and (instance? PartialFn other)
|
||||
(= f (.-f other))
|
||||
(= args (.-args other))))
|
||||
IHash
|
||||
(-hash [_] (hash [f args])))
|
||||
|
||||
(defn make-partial-fn [f args]
|
||||
(->PartialFn (apply partial f args) f args))
|
||||
|
||||
(defn ^boolean named? [x]
|
||||
(or (keyword? x)
|
||||
(symbol? x)))
|
||||
|
||||
(defn class-names
|
||||
([])
|
||||
([class]
|
||||
(if (coll? class)
|
||||
(let [classes (keep (fn [c]
|
||||
(if c
|
||||
(if (named? c)
|
||||
(name c)
|
||||
c)))
|
||||
class)]
|
||||
(if (seq classes)
|
||||
(string/join " " classes)))
|
||||
(if (named? class)
|
||||
(name class)
|
||||
class)))
|
||||
([a b]
|
||||
(if a
|
||||
(if b
|
||||
(str (class-names a) " " (class-names b))
|
||||
(class-names a))
|
||||
(class-names b)))
|
||||
([a b & rst]
|
||||
(reduce class-names
|
||||
(class-names a b)
|
||||
rst)))
|
||||
|
||||
(defn- merge-class [p1 p2]
|
||||
(let [class (when-let [c1 (:class p1)]
|
||||
(when-let [c2 (:class p2)]
|
||||
(str c1 " " c2)))]
|
||||
(if (nil? class)
|
||||
p2
|
||||
(assoc p2 :class class))))
|
||||
(assoc p2 :class (class-names (:class p1) (:class p2))))
|
||||
|
||||
(defn- merge-style [p1 p2]
|
||||
(let [style (when-let [s1 (:style p1)]
|
||||
@ -122,14 +149,24 @@
|
||||
p2
|
||||
(assoc p2 :style style))))
|
||||
|
||||
(defn merge-props [p1 p2]
|
||||
(if (nil? p1)
|
||||
p2
|
||||
(do
|
||||
(assert (map? p1)
|
||||
(str "Property must be a map, not " (pr-str p1)))
|
||||
(merge-style p1 (merge-class p1 (merge p1 p2))))))
|
||||
|
||||
(defn merge-props
|
||||
([] nil)
|
||||
;; Normalize :class even if there are no merging
|
||||
([p]
|
||||
(if-let [c (:class p)]
|
||||
(assoc p :class (class-names c))
|
||||
p))
|
||||
([p1 p2]
|
||||
(if (nil? p1)
|
||||
(if-let [c (:class p2)]
|
||||
(assoc p2 :class (class-names c))
|
||||
p2)
|
||||
(do
|
||||
(assert (map? p1)
|
||||
(str "Property must be a map, not " (pr-str p1)))
|
||||
(merge p1 (merge-style p1 (merge-class p1 p2))))))
|
||||
([p1 p2 & ps]
|
||||
(reduce merge-props (merge-props p1 p2) ps)))
|
||||
|
||||
(def ^:dynamic *always-update* false)
|
||||
|
||||
|
@ -34,10 +34,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))))
|
||||
@ -49,7 +59,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)
|
||||
@ -336,7 +350,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]
|
||||
@ -498,7 +520,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))
|
||||
|
20
test/reagent/impl/template_test.cljs
Normal file
20
test/reagent/impl/template_test.cljs
Normal file
@ -0,0 +1,20 @@
|
||||
(ns reagent.impl.template-test
|
||||
(:require [clojure.test :as t :refer [deftest is testing]]
|
||||
[reagent.impl.template :as tmpl]
|
||||
[goog.object :as gobj]))
|
||||
|
||||
(deftest cached-prop-name
|
||||
(is (= "className"
|
||||
(tmpl/cached-prop-name :class))))
|
||||
|
||||
(deftest cached-custom-prop-name
|
||||
(is (= "class"
|
||||
(tmpl/cached-custom-prop-name :class))))
|
||||
|
||||
(deftest convert-props-test
|
||||
(is (gobj/equals #js {:className "a"}
|
||||
(tmpl/convert-props {:class "a"} #js {:id nil :custom false})))
|
||||
(is (gobj/equals #js {:class "a"}
|
||||
(tmpl/convert-props {:class "a"} #js {:id nil :custom true})))
|
||||
(is (gobj/equals #js {:className "a b" :id "a"}
|
||||
(tmpl/convert-props {:class "b"} #js {:id "a" :class "a" :custom false}))))
|
78
test/reagent/impl/util_test.cljs
Normal file
78
test/reagent/impl/util_test.cljs
Normal file
@ -0,0 +1,78 @@
|
||||
(ns reagent.impl.util-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[reagent.impl.util :as util]))
|
||||
|
||||
(deftest class-names-test
|
||||
(is (= nil
|
||||
(util/class-names)
|
||||
(util/class-names nil)
|
||||
(util/class-names [])
|
||||
(util/class-names nil [])))
|
||||
(is (= "a b"
|
||||
(util/class-names ["a" "b"])
|
||||
(util/class-names "a" "b")))
|
||||
(is (= "a"
|
||||
(util/class-names :a)
|
||||
(util/class-names [:a])
|
||||
(util/class-names nil "a")
|
||||
(util/class-names [] nil "a")))
|
||||
(is (= "a b c d"
|
||||
(util/class-names "a" "b" nil ["c" "d"]))))
|
||||
|
||||
; (simple-benchmark []
|
||||
; (do (util/class-names "a" "b")
|
||||
; (util/class-names nil "a")
|
||||
; (util/class-names "a" nil))
|
||||
; 10000)
|
||||
|
||||
; (simple-benchmark []
|
||||
; (util/class-names "a" "b" nil "c" "d")
|
||||
; 10000)
|
||||
|
||||
(deftest merge-props-test
|
||||
(testing "no arguments"
|
||||
(is (nil? (util/merge-props))))
|
||||
|
||||
(testing "one argument"
|
||||
(is (nil? (util/merge-props nil)))
|
||||
(is (= {:foo :bar} (util/merge-props {:foo :bar}))))
|
||||
|
||||
(testing "two arguments"
|
||||
(is (= {:disabled false :style {:flex 1 :flex-direction "row"} :class "foo bar"}
|
||||
(util/merge-props {:disabled true :style {:flex 1} :class "foo"}
|
||||
{:disabled false :style {:flex-direction "row"} :class "bar"}))))
|
||||
|
||||
(testing "n arguments"
|
||||
(is (= {:disabled false
|
||||
:checked true
|
||||
:style {:align-items "flex-end"
|
||||
:justify-content "center"}
|
||||
:class "foo bar baz quux"}
|
||||
(util/merge-props {:disabled false
|
||||
:checked false
|
||||
:style {:align-items "flex-end"}
|
||||
:class "foo"}
|
||||
{:disabled false
|
||||
:checked false
|
||||
:class "bar"}
|
||||
{:disabled true
|
||||
:style {:justify-content "center"}
|
||||
:class "baz"}
|
||||
{:disabled false
|
||||
:checked true
|
||||
:class "quux"}
|
||||
nil))))
|
||||
|
||||
(testing ":class"
|
||||
(is (= {:class "foo bar baz quux"}
|
||||
(util/merge-props {:class "foo bar"}
|
||||
{:class ["baz" "quux"]})
|
||||
(util/merge-props nil {:class ["foo" "bar" "baz" "quux"]})
|
||||
(util/merge-props {:class ["foo" "bar" "baz" "quux"]})))))
|
||||
|
||||
(deftest partial-fn-test
|
||||
(is (= (util/make-partial-fn println ["a"])
|
||||
(util/make-partial-fn println ["a"])))
|
||||
|
||||
(is (not (= (util/make-partial-fn println ["a"])
|
||||
nil))))
|
@ -6,6 +6,8 @@
|
||||
[reagenttest.testtrack]
|
||||
[reagenttest.testwithlet]
|
||||
[reagenttest.testwrap]
|
||||
[reagent.impl.template-test]
|
||||
[reagent.impl.util-test]
|
||||
[cljs.test :as test]
|
||||
[doo.runner :as doo :include-macros true]
|
||||
[reagent.core :as r]
|
||||
@ -20,8 +22,7 @@
|
||||
:color :#aaa})
|
||||
|
||||
(defn all-tests []
|
||||
#_(test/run-tests 'reagenttest.testratomasync)
|
||||
(test/run-all-tests #"reagenttest.test.*"))
|
||||
(test/run-all-tests #"(reagenttest\.test.*|reagent\..*-test)"))
|
||||
|
||||
(defmethod test/report [::test/default :summary] [m]
|
||||
;; ClojureScript 2814 doesn't return anything from run-tests
|
||||
@ -55,4 +56,4 @@
|
||||
(run-tests)
|
||||
[#'test-output-mini]))
|
||||
|
||||
(doo/doo-all-tests #"reagenttest.test.*")
|
||||
(doo/doo-all-tests #"(reagenttest\.test.*|reagent\..*-test)")
|
||||
|
@ -1,7 +1,6 @@
|
||||
(ns reagenttest.testreagent
|
||||
(:require [cljs.test :as t :refer-macros [is deftest testing]]
|
||||
[react :as react]
|
||||
[create-react-class :as create-react-class]
|
||||
[reagent.ratom :as rv :refer-macros [reaction]]
|
||||
[reagent.debug :as debug :refer-macros [dbg println log dev?]]
|
||||
[reagent.core :as r]
|
||||
@ -440,15 +439,18 @@
|
||||
(is (= (rstr (ae [:div [:div "foo"]]))
|
||||
(rstr (ae [:div (ce "div" nil "foo")]))))))
|
||||
|
||||
(def ndiv (create-react-class
|
||||
#js {:displayName "ndiv"
|
||||
:render
|
||||
(fn []
|
||||
(this-as
|
||||
this
|
||||
(r/create-element
|
||||
"div" #js{:className (.. this -props -className)}
|
||||
(.. this -props -children))))}))
|
||||
(def ndiv (let [cmp (fn [])]
|
||||
(gobj/extend
|
||||
(.-prototype cmp)
|
||||
(.-prototype react/Component)
|
||||
#js {:render (fn []
|
||||
(this-as
|
||||
this
|
||||
(r/create-element
|
||||
"div" #js {:className ($ this :props.className)}
|
||||
($ this :props.children))))})
|
||||
(gobj/extend cmp react/Component)
|
||||
cmp))
|
||||
|
||||
(deftest test-adapt-class
|
||||
(let [d1 (r/adapt-react-class ndiv)
|
||||
@ -573,6 +575,27 @@
|
||||
(is (= (rstr [:p {:class #{"a" "b" "c"}}])
|
||||
(rstr [:p {:class "a b c"}]))))
|
||||
|
||||
(deftest class-different-types
|
||||
(testing "named values are supported"
|
||||
(is (= (rstr [:p {:class :a}])
|
||||
(rstr [:p {:class "a"}])))
|
||||
(is (= (rstr [:p.a {:class :b}])
|
||||
(rstr [:p {:class "a b"}])))
|
||||
(is (= (rstr [:p.a {:class 'b}])
|
||||
(rstr [:p {:class "a b"}])))
|
||||
(is (= (rstr [:p {:class [:a :b]}])
|
||||
(rstr [:p {:class "a b"}])))
|
||||
(is (= (rstr [:p {:class ['a :b]}])
|
||||
(rstr [:p {:class "a b"}]))))
|
||||
|
||||
(testing "non-named values like numbers"
|
||||
(is (= (rstr [:p {:class [1 :b]}])
|
||||
(rstr [:p {:class "1 b"}]))))
|
||||
|
||||
(testing "falsey values are filtered from collections"
|
||||
(is (= (rstr [:p {:class [:a :b false nil]}])
|
||||
(rstr [:p {:class "a b"}])))) )
|
||||
|
||||
(deftest test-force-update
|
||||
(let [v (atom {:v1 0
|
||||
:v2 0})
|
||||
@ -940,9 +963,19 @@
|
||||
(is (thrown-with-msg?
|
||||
:default #"Invalid Hiccup form: \[23]"
|
||||
(rstr [23])))
|
||||
(is (thrown-with-msg?
|
||||
:default #"Expected React component in: \[:> \[:div]]"
|
||||
(rstr [:> [:div]])))
|
||||
;; This used to be asserted by Reagent, but because it is hard to validate
|
||||
;; components, now we just trust React will validate elements.
|
||||
; (is (thrown-with-msg?
|
||||
; :default #"Expected React component in: \[:> \[:div]]"
|
||||
; (rstr [:> [:div]])))
|
||||
;; This is from React.createElement
|
||||
;; NOTE: browser-npm uses production cjs bundle for now which only shows
|
||||
;; the minified error
|
||||
(debug/track-warnings
|
||||
(wrap-capture-console-error
|
||||
#(is (thrown-with-msg?
|
||||
:default #"(Element type is invalid:|Minified React error)"
|
||||
(rstr [:> [:div]])))))
|
||||
(is (thrown-with-msg?
|
||||
:default #"Invalid tag: 'p.'"
|
||||
(rstr [:p.])))
|
||||
@ -958,7 +991,13 @@
|
||||
comp4 (fn comp4 []
|
||||
(for [i (range 0 1)]
|
||||
[:p "foo"]))
|
||||
nat (create-react-class #js {:render (fn [])})
|
||||
nat (let [cmp (fn [])]
|
||||
(gobj/extend
|
||||
(.-prototype cmp)
|
||||
(.-prototype react/Component)
|
||||
#js {:render (fn [])})
|
||||
(gobj/extend cmp react/Component)
|
||||
cmp)
|
||||
pkg "reagenttest.testreagent."
|
||||
stack1 (str "in " pkg "comp1")
|
||||
stack2 (str "in " pkg "comp2 > " pkg "comp1")
|
||||
@ -1157,6 +1196,73 @@
|
||||
[: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"))
|
||||
|
||||
(def Provider (.-Provider my-context))
|
||||
(def Consumer (.-Consumer my-context))
|
||||
|
||||
(deftest new-context-test
|
||||
(is (= "<div>Context: foo</div>"
|
||||
(rstr (r/create-element
|
||||
Provider #js {:value "foo"}
|
||||
(r/create-element
|
||||
Consumer #js {}
|
||||
(fn [v]
|
||||
(r/as-element [:div "Context: " v])))))))
|
||||
|
||||
(testing "context default value works"
|
||||
(is (= "<div>Context: default</div>"
|
||||
(rstr (r/create-element
|
||||
Consumer #js {}
|
||||
(fn [v]
|
||||
(r/as-element [:div "Context: " v])))))))
|
||||
|
||||
(testing "context works with adapt-react-class"
|
||||
(let [provider (r/adapt-react-class Provider)
|
||||
consumer (r/adapt-react-class Consumer)]
|
||||
(is (= "<div>Context: bar</div>"
|
||||
(rstr [provider {:value "bar"}
|
||||
[consumer {}
|
||||
(fn [v]
|
||||
(r/as-element [:div "Context: " v]))]])))))
|
||||
|
||||
(testing "context works with :>"
|
||||
(is (= "<div>Context: bar</div>"
|
||||
(rstr [:> Provider {:value "bar"}
|
||||
[:> Consumer {}
|
||||
(fn [v]
|
||||
(r/as-element [:div "Context: " v]))]])))))
|
||||
|
||||
(deftest on-failed-prop-comparison-in-should-update-swallow-exception-and-do-not-update-component
|
||||
(let [prop (r/atom {:todos 1})
|
||||
component-was-updated (atom false)
|
||||
error-thrown-after-updating-props (atom false)
|
||||
component-class (r/create-class {:reagent-render (fn [& args]
|
||||
[:div (str (first args))])
|
||||
:component-did-update (fn [& args]
|
||||
(reset! component-was-updated true))})
|
||||
component (fn []
|
||||
[component-class @prop])]
|
||||
|
||||
(when (and isClient (dev?))
|
||||
(let [e (debug/track-warnings
|
||||
#(with-mounted-component [component]
|
||||
(fn [c div]
|
||||
(reset! prop (sorted-map 1 2))
|
||||
(try
|
||||
(r/flush)
|
||||
(catch :default e
|
||||
(reset! error-thrown-after-updating-props true)))
|
||||
|
||||
(is (not @component-was-updated))
|
||||
(is (not @error-thrown-after-updating-props)))))]
|
||||
(is (re-find #"Warning: Exception thrown while comparing argv's in shouldComponentUpdate:"
|
||||
(first (:warn e))))))))
|
||||
|
Loading…
x
Reference in New Issue
Block a user