diff --git a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md index 84fbad81e..d84c287eb 100644 --- a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md +++ b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md @@ -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 + + + +``` + +Might actually render HTML that looks like this: +```html +
+
+ +
+
+

Angkor Wat/a>

+
+
+``` + +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 ? +
New!
: + null; + + return ( +
+ {badge} + +
+ ); +} + +const EnhancedPhotoItem = enhancedPhoto(PhotoItem); + + +``` + +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 ? +
New!
: + null; + + return ( +
+ {badge} + +
+ ); + + } + } +} +``` + +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 ( +
+ + Character count: {props.value.length} +
+ ); +} + +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 ( +
+ + {invalid && + + {`Text is too long! Must be ${limit} characters`} + + } +
+ ); +} + +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 ( +
+ + {showWarning && + Characters remaining: {remainingChars} + } +
+ ); +} + +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 ( +
+ {this.props.items.map(item => ( + + ))} +
+ ); + } +} + +export default withInjector(Gallery); ``` -That's how you create a SilverStripe React component! ### Interfacing with legacy CMS JavaScript diff --git a/docs/en/_images/react-di-1.png b/docs/en/_images/react-di-1.png new file mode 100644 index 000000000..6b49649d9 Binary files /dev/null and b/docs/en/_images/react-di-1.png differ diff --git a/docs/en/_images/react-di-2.png b/docs/en/_images/react-di-2.png new file mode 100644 index 000000000..1f0075605 Binary files /dev/null and b/docs/en/_images/react-di-2.png differ diff --git a/docs/en/_images/react-di-3.png b/docs/en/_images/react-di-3.png new file mode 100644 index 000000000..ee975b0bf Binary files /dev/null and b/docs/en/_images/react-di-3.png differ