Docs for React DI

This commit is contained in:
Aaron Carlino 2017-05-23 16:19:47 +12:00
parent 9fe707d3b9
commit 75981989b0
4 changed files with 330 additions and 16 deletions

View File

@ -204,26 +204,340 @@ To make the actions more user-friendly you can also use alternating buttons as
detailed in the [CMS Alternating Button](cms_alternating_button)
how-to.
## ReactJS in SilverStripe
## React components
### SilverStripeComponent
Some admin modules render their UI with React, a popular Javascript library created by Facebook.
For these sections, rendering happens via client side scripts that create and inject HTML
declaratively using data structures. These UI elements are known as "components" and
represent the fundamental building block of a React-rendered interface.
The base class for SilverStripe React components. If you're building React components for the CMS, this is the class you want to extend. `SilverStripeComponent` extends `React.Component` and adds some handy CMS specific behaviour.
### Creating a component
__my-component.js__
```javascript
import SilverStripeComponent from 'silverstripe-component';
class MyComponent extends SilverStripeComponent {
}
export default MyComponent;
For example, a component expressed like this:
```js
<PhotoItem size={200} caption={'Angkor Wat'} onSelect={openLightbox}>
<img src="path/to/image.jpg" />
</PhotoItem>
```
Might actually render HTML that looks like this:
```html
<div class="photo-item">
<div class="photo" style="width:200px;height:200px;">
<img src="path/to/image.jpg">
</div>
<div class="photo-caption">
<h3><a>Angkor Wat/a></h3>
</div>
</div>
```
This syntax is known as JSX. It is transpiled at build time into native Javascript calls
to the React API. While optional, it is recommended to express components this way.
This documentation will stop short of explaining React in-depth, as there is much better
documentation available all over the web. We recommend:
* [The Official React Tutorial](https://facebook.github.io/react/tutorial/tutorial.html)
* [Build With React](http://buildwithreact.com/tutorial)
#### A few words about ES6
The remainder of this tutorial is written in [ECMAScript 6](http://es6-features.org/#Constants), or _ES6_
for short. This is the new spec for Javascript (currently ES5) that is as of this writing
only partially implmented in modern browsers. Because it doesn't yet enjoy vast native support,
it has to be [transpiled](https://www.stevefenton.co.uk/2012/11/compiling-vs-transpiling/) in order to work
in a browser. This transpiling can be done using a variety of toolchains, but the basic
principle is that a browser-ready, ES5 version of your code is generated in your dev
environment as part of your workflow. Often called a "bundle," you should rarely have
to see this file. It is effectively an invisible layer that translates modern ES6
code into something a browser can parse. As browsers evolve, this step will become less
necessary. (Although, it is worth noting that because transpiling comes at such a low cost,
and browsers are relatively slow to catch up, we'll probably be using it for the
foreseeable future in order to adopt new features beyond ES6).
As stated above, there are many ways to solve the problem of transpiling. The toolchain
we use in core SilverStripe modules includes:
* [Babel](http://babeljs.io) (ES6 transpiler)
* [Webpack](http://webpack.js.org) (Module bundler)
### Customising React components
React components can be customised in a similar way to PHP classes, using a dependency
injection API. The key difference is that components are not overriden the way backend
services are. Rather, new components are composed using [higher order components](https://facebook.github.io/react/docs/higher-order-components.html).
This has the inherent advantage of allowing all thidparty code to have an influence
over the behaviour, state, and UI of a component.
#### A simple higher order component
Using our example above, let's create a customised `PhotoItem` that allows a badge,
perhaps indicating that it is new to the gallery.
```js
const enhancePhoto = (PhotoItem) => (props) {
const badge = props.isNew ?
<div className="badge">New!</div> :
null;
return (
<div>
{badge}
<PhotoItem {...props} />
</div>
);
}
const EnhancedPhotoItem = enhancedPhoto(PhotoItem);
<EnhancedPhotoItem isNew={true} size={300} />
```
Alternatively, this component could be expressed with an ES6 class, rather than a simple
function.
```js
const enhancePhoto = (PhotoItem) => {
return class EnhancedPhotoItem extends React.Component {
render() {
const badge = this.props.isNew ?
<div className="badge">New!</div> :
null;
return (
<div>
{badge}
<PhotoItem {...this.props} />
</div>
);
}
}
}
```
When components are stateless, using a simple function in lieu of a class is recommended.
#### Using the injector to customise a core component
Let's make a more awesome text field. Because the `TextField` component is fetched
through the injector, we can override it and augment it with our own functionality.
In this example, we'll add a simple character count below the text field.
First, let's create our higher order component.
__my-module/js/components/CharacterCounter.js__
```js
import React from 'react';
const CharacterCounter = (TextField) => (props) => {
return (
<div>
<TextField {...props} />
<small>Character count: {props.value.length}</small>
</div>
);
}
export default CharacterCounter;
```
Now let's add this higher order component to the injector.
__my-module/js/main.js__
```js
import Injector from 'lib/Injector';
import CharacterCounter from './components/CharacterCounter';
Injector.update(
{
name: 'my-module',
},
wrap => {
wrap('TextField', CharacterCounter);
}
);
```
Much like the configuration layer, we need to specify a name for this mutation. This
will help other modules negotiate their priority over the injector in relation to yours.
The second parameter of the `update` argument is a callback which receives a `wrap()` function
that allows you to mutate the DI container with a wrapper for the component. Remember, this function does not _replace_
the component -- it enhances it with new functionality.
The last thing we'll have to do is make sure this script gets loaded into the admin
page.
__my-module/\_config/config.yml__
```yaml
---
Name: my-module
---
SilverStripe\Admin\LeftAndMain:
extra_requirements_javascript:
# The name of this file will depend on how you've configured your build process
- 'my-module/js/dist/main.bundle.js'
```
Now that the customisation is applied, our text fields look like this:
![](../../../_images/react-di-1.png)
Let's add another customisation to TextField. If the text goes beyond a specified
length, let's throw a warning in the UI.
__my-module/js/components/TextLengthChecker.js__
```js
const TextLengthCheker = (TextField) => (props) => {
const {limit, value } = props;
const invalid = limit !== undefined && value.length > limit;
return (
<div>
<TextField {...props} />
{invalid &&
<span style={{color: 'red'}}>
{`Text is too long! Must be ${limit} characters`}
</span>
}
</div>
);
}
export default TextLengthChecker;
```
We'll apply this one to the injector as well, but let's do it under a different name.
For the purposes of demonstration, let's imagine this customisation comes from another
module.
__my-module/js/main.js__
```js
import Injector from 'lib/Injector';
import TextLengthChecker from './components/TextLengthChecker';
Injector.update(
{
name: 'my-other-module',
},
wrap => {
wrap('TextField', TextLengthChecker);
}
);
```
Now, both components have applied themselves to the textfield.
![](../../../_images/react-di-2.png)
##### Getting multiple customisations to work together
Both these enhancements are nice, but what would be even better is if they could
work together collaboratively so that the character count only appeared when the user
input got within a certain range of the limit. In order to do that, we'll need to be
sure that the `TextLengthChecker` customisation is loaded ahead of the `CharacterCounter`
customisation.
First let's update the character counter to show characters _remaining_, which is
much more useful. We'll also update the API to allow a `warningBuffer` prop. This is
the amount of characters the input can be within the `limit` before the warning shows.
__my-module/js/components/CharacterCounter.js__
```js
import React from 'react';
const CharacterCounter = (TextField) => (props) => {
const { warningBuffer, limit, value: { length } } = props;
const remainingChars = limit - length;
const showWarning = length + warningBuffer >= limit;
return (
<div>
<TextField {...props} />
{showWarning &&
<small>Characters remaining: {remainingChars}</small>
}
</div>
);
}
export default CharacterCounter;
```
Now, when we apply this customisation, we need to be sure it loads _after_ the length
checker in the middleware chain, as it relies on the prop `limit`. We can do that by specifying priority using `before` and `after`
metadata to the customisation.
__my-module/js/main.js__
```js
import Injector from 'lib/Injector';
import CharacterCounter from './components/CharacterCounter';
import TextLengthChecker from './components/TextLengthChecker';
Injector.update(
{
name: 'my-module',
after: 'my-other-module',
},
wrap => {
wrap('TextField', CharacterCounter);
}
);
Injector.update(
{
name: 'my-other-module',
before: 'my-module',
},
wrap => {
wrap('TextField', TextLengthChecker);
}
);
```
Now, both components play together nicely.
![](../../../_images/react-di-3.png)
### Registering new React components
If you've created a module using React, it's a good idea to afford other developers an
API to enhance those components. To do that, simply register them with `Injector`.
__my-public-module/js/main.js__
```js
import Injector from 'lib/Injector';
Injector.register('MyComponent', MyComponent);
```
Now other developers can customise your components with `Injector.update()`.
Note: Overwriting components by calling `register()` multiple times for the same
service name is discouraged, and will throw an error. Should you really need to do this,
you can pass `{ force: true }` as the third argument to the `register()` function.
### Using the injector within your component
If your component has dependencies, you can add the injector via context using the `withInjector`
higher order component.
__my-module/js/components/Gallery.js__
```js
import React from 'react';
import { withInjector } from 'lib/Injector';
class Gallery extends React.Component {
render() {
const GalleryItem = this.context.injector.get('GalleryItem');
return (
<div>
{this.props.items.map(item => (
<GalleryItem title={item.title} image={item.image} />
))}
</div>
);
}
}
export default withInjector(Gallery);
```
That's how you create a SilverStripe React component!
### Interfacing with legacy CMS JavaScript

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB