mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Custom redux state in injector, updated docs (#7042)
* Update docs for new Injector js structure and description for injector reducer API * Injector documentation updates * updates per ingo * Update examples and some notes based on feedback * Updates per ingo * documentation updates to reflect new injector.component * Minor doc updates
This commit is contained in:
parent
977c62a513
commit
9a98655124
@ -6,10 +6,24 @@ summary: Advanced documentation about writing and customizing javascript within
|
|||||||
The following document is an advanced guide on building rich javascript interactions within the SilverStripe CMS and
|
The following document is an advanced guide on building rich javascript interactions within the SilverStripe CMS and
|
||||||
a list of our best practices for contributing and modifying the core javascript framework.
|
a list of our best practices for contributing and modifying the core javascript framework.
|
||||||
|
|
||||||
|
## ES6 and build tools
|
||||||
|
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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
__Deprecated:__
|
__Deprecated:__
|
||||||
The following documentation regarding jQuery, jQueryUI and Entwine applies to legacy code only.
|
The following documentation regarding jQuery, jQueryUI and Entwine applies to legacy code only.
|
||||||
If you're developing new functionality in React powered sections please refer to
|
If you're developing new functionality in React powered sections please refer to
|
||||||
[ReactJS in SilverStripe](./How_Tos/Extend_CMS_Interface.md#reactjs-in-silverstripe).
|
[ReactJS, Redux, and GraphQL](./ReactJS_Redux_and_GraphQL).
|
||||||
|
|
||||||
## jQuery, jQuery UI and jQuery.entwine: Our libraries of choice
|
## jQuery, jQuery UI and jQuery.entwine: Our libraries of choice
|
||||||
|
|
||||||
|
@ -0,0 +1,725 @@
|
|||||||
|
title: React, Redux, and GraphQL
|
||||||
|
summary: Learn how to extend and customise the technologies we use for application state and client-rendered UI.
|
||||||
|
|
||||||
|
# Introduction to the "React" layer
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
There are some several members of this ecosystem that all work together to provide a dyanamic UI. They include:
|
||||||
|
* [ReactJS](https://facebook.github.io/react/) - A Javascript UI library
|
||||||
|
* [Redux](http://redux.js.org/) - A state manager for Javascript
|
||||||
|
* [GraphQL](http://graphql.org/) - A query language for your API
|
||||||
|
* [Apollo](https://www.apollodata.com/) - A framework for using GraphQL in your application
|
||||||
|
|
||||||
|
All of these pillars of the frontend application can be customised, giving you more control over how the admin interface looks, feels, and behaves.
|
||||||
|
|
||||||
|
First, a brief summary of what each of these are:
|
||||||
|
|
||||||
|
## React
|
||||||
|
|
||||||
|
React's job is to render UI. Its UI elements are known as "components" and represent the fundamental building block of a React-rendered interface. A React 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.
|
||||||
|
|
||||||
|
### Recommended: React Dev Tools
|
||||||
|
|
||||||
|
The [React Dev Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) extension available for Chrome and Firefox is critical to debugging a React UI. It will let you browse the React UI much like the DOM, showing the tree of rendered components and their current props and state in real time.
|
||||||
|
|
||||||
|
## Redux
|
||||||
|
|
||||||
|
Redux is a state management tool with a tiny API that affords the developer highly predictable behaviour. All of the application state is stored in a single object, and the only way to mutate that object is by calling an action, which is just a simple object that describes what happened. A function known as a _reducer_ mutates the state based on that action and returns a new reference with the updated state.
|
||||||
|
|
||||||
|
The following example is taken from the [Redux Github page](https://github.com/reactjs/redux):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// reducer
|
||||||
|
function counter(state = 0, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'INCREMENT':
|
||||||
|
return state + 1
|
||||||
|
case 'DECREMENT':
|
||||||
|
return state - 1
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = createStore(counter)
|
||||||
|
store.subscribe(() =>
|
||||||
|
console.log(store.getState())
|
||||||
|
)
|
||||||
|
// Call an action
|
||||||
|
store.dispatch({ type: 'INCREMENT' })
|
||||||
|
// 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended: Redux Devtools
|
||||||
|
|
||||||
|
It's important to be able to view the state of the React application when you're debugging and
|
||||||
|
building the interface.
|
||||||
|
|
||||||
|
To be able to view the state, you'll need to be in a dev environment
|
||||||
|
and have the [Redux Devtools](https://github.com/zalmoxisus/redux-devtools-extension)
|
||||||
|
installed on Google Chrome or Firefox, which can be found by searching with your favourite search
|
||||||
|
engine.
|
||||||
|
|
||||||
|
|
||||||
|
## GraphQL and Apollo
|
||||||
|
|
||||||
|
[GraphQL](http://graphql.org/learn/) is a strictly-typed query language that allows you to describe what data you want to fetch from your API. Because it is based on types, it is self-documenting and predictable. Further, it's structure lends itself nicely to fetching nested objects. Here is an example of a simple GraphQL query:
|
||||||
|
|
||||||
|
```
|
||||||
|
query GetUser($ID: Int!) {
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
email
|
||||||
|
blogPosts {
|
||||||
|
title
|
||||||
|
comments(Limit: 5) {
|
||||||
|
author
|
||||||
|
comment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above query is almost self-descriptive. It gets a user by ID, returns his or her name and email address, along with the title of any blog posts he or she has written, and the first five comments for each of those. The result of that query is, very predictably, JSON that takes on the same structure.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"name": "Test user",
|
||||||
|
"email": "me@example.com",
|
||||||
|
"blogPosts": [
|
||||||
|
{
|
||||||
|
"title": "How to be awesome at GraphQL",
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"author": "Uncle Cheese",
|
||||||
|
"comment": "Nice stuff, bro"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On its own, GraphQL offers nothing functional, as it's just a query language. You still need a service that will invoke queries and map their results to UI. For that, SilverStripe uses an implementation of [Apollo](http://dev.apollodata.com/) that works with React.
|
||||||
|
|
||||||
|
## For more information
|
||||||
|
|
||||||
|
This documentation will stop short of explaining React, Redux, and GraphQL/Apollo 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)
|
||||||
|
* [Getting Started with Redux](https://egghead.io/courses/getting-started-with-redux)
|
||||||
|
* [The React Apollo docs](http://dev.apollodata.com/react/)
|
||||||
|
|
||||||
|
# The Injector API
|
||||||
|
|
||||||
|
Much like SilverStripe's [Injector API](../../extending/injector) in PHP,
|
||||||
|
the client-side framework has its own implementation of dependency injection
|
||||||
|
known as `Injector`. Using Injector, you can register new services, and
|
||||||
|
transform existing services.
|
||||||
|
|
||||||
|
Injector is broken up into three sub-APIs:
|
||||||
|
* `Injector.component` for React UI components
|
||||||
|
* `Injector.reducer` for Redux state management
|
||||||
|
* `Injector.form` for forms rendered via `FormSchema`.
|
||||||
|
|
||||||
|
The frontend Injector works a bit differently than its backend counterpart. Instead of _overriding_ a service with your own implementation, you _enhance_ an existing service with your own concerns. This pattern is known as [middleware](https://en.wikipedia.org/wiki/Middleware).
|
||||||
|
|
||||||
|
Middleware works a lot like a decorator. It doesn't alter the original API of the service,
|
||||||
|
but it can augment it with new features and concerns. 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 middleware example
|
||||||
|
|
||||||
|
Let's say you have an application that features error logging. By default, the error logging service simply outputs to `console.error`. But you want to customise it to send errors to a thirdparty service. For this, you could use middleware to augment the default functionality of the logger.
|
||||||
|
|
||||||
|
_LoggingService.js_
|
||||||
|
```js
|
||||||
|
const LoggingService = (error) => console.error(error);
|
||||||
|
|
||||||
|
export default LoggingService;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, let's add some middleware to that service. The signature of middleware is:
|
||||||
|
```js
|
||||||
|
(next) => (args) => next(args)
|
||||||
|
```
|
||||||
|
Where `next()` is the next customisation in the "chain" of middleware. Before invoking the next implementation, you can add whatever customisations you need. Here's how we would use middleware to enhance `LoggingService`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import thirdPartyLogger from 'third-party-logger';
|
||||||
|
const addLoggingMiddleware = (next) => (error) => {
|
||||||
|
if (error.type === LoggingService.CRITICAL) {
|
||||||
|
thirdpartyLogger.send(error.message);
|
||||||
|
}
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we would create a new logging service that merges both implementations.
|
||||||
|
```js
|
||||||
|
import LoggingService from './LoggingService';
|
||||||
|
import addLoggingMiddleware from './addLoggingMiddleware';
|
||||||
|
|
||||||
|
const MyNewLogger = addLoggingMiddleware(LoggingService);
|
||||||
|
```
|
||||||
|
|
||||||
|
We haven't overriden any functionality. `LoggingService(error)` will still invoke `console.error`, once all the middleware has run. But what if we did want to kill the original functionality?
|
||||||
|
|
||||||
|
```js
|
||||||
|
import LoggingService from './LoggingService';
|
||||||
|
import thirdPartyLogger from 'third-party-logger';
|
||||||
|
|
||||||
|
const addLoggingMiddleware = (next) => (error) => {
|
||||||
|
// Critical errors go to a thirdparty service
|
||||||
|
if (error.type === LoggingService.CRITICAL) {
|
||||||
|
thirdPartyLogger.send(error.message);
|
||||||
|
}
|
||||||
|
// Other errors get logged, but not to our thirdparty
|
||||||
|
else if (error.type === LoggingService.ERROR) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
// Minor errors are ignored
|
||||||
|
else {
|
||||||
|
// Do nothing!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Registering new services to the Injector
|
||||||
|
|
||||||
|
If you've created a module using React, it's a good idea to afford other developers an
|
||||||
|
API to enhance those components, forms, and state. To do that, simply register them with `Injector`.
|
||||||
|
|
||||||
|
__my-public-module/js/main.js__
|
||||||
|
```js
|
||||||
|
import Injector from 'lib/Injector';
|
||||||
|
|
||||||
|
Injector.component.register('MyComponent', MyComponent);
|
||||||
|
Injector.reducer.register('myCustom', MyReducer);
|
||||||
|
```
|
||||||
|
|
||||||
|
Services can then be fetched using their respective `.get()` methods.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const MyComponent = Injector.component.get('MyComponent');
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="notice" markdown="1">
|
||||||
|
Because of the unique structure of the `form` middleware, you cannot register new services to `Injector.form`.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="alert" markdown="1">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## Transforming services using middleware
|
||||||
|
|
||||||
|
Now that the services are registered, other developers can customise your services with `Injector.transform()`.
|
||||||
|
|
||||||
|
__someone-elses-module/js/main.js__
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-transformation',
|
||||||
|
(updater) => {
|
||||||
|
updater.component('MyComponent', MyCustomComponent);
|
||||||
|
updater.reducer('myCustom', MyCustomReducer);
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Much like the configuration layer, we need to specify a name for this transformation. This will help other modules negotiate their priority over the injector in relation to yours.
|
||||||
|
|
||||||
|
The second parameter of the `transform` argument is a callback which receives an `updater`object. It contains four functions: `component()`, `reducer()`, `form.alterSchema()` and `form.addValidation()`. We'll cover all of these in detail functions in detail further into the document, but briefly, these update functions allow you to mutate the DI container with a wrapper for the service. Remember, this function does not _replace_
|
||||||
|
the service -- it enhances it with new functionality.
|
||||||
|
|
||||||
|
### Helpful tip: Name your component middleware
|
||||||
|
|
||||||
|
Since multiple enhancements can be applied to the same component, it will be really
|
||||||
|
useful for debugging purposes to reveal the names of each enhancement on the `displayName` of
|
||||||
|
the component. This will really help you when viewing the rendered component tree in
|
||||||
|
[React Dev Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en).
|
||||||
|
|
||||||
|
For this, you can use the third parameter of the `updater.component` function. It takes an arbitrary
|
||||||
|
name for the enhancement you're applying.
|
||||||
|
|
||||||
|
__module-a/js/main.js__
|
||||||
|
```js
|
||||||
|
(updater) => updater.component('TextField', CharacterCounter, 'CharacterCounter')
|
||||||
|
```
|
||||||
|
__module-b/js/main.js__
|
||||||
|
```js
|
||||||
|
(updater) => updater.component('TextField', TextLengthChecker, 'TextLengthChecker')
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Controlling the order of transformations
|
||||||
|
|
||||||
|
Sometimes, it's critical to ensure that your customisation happens after another one has been executed. To afford you control over the ordering of transforms, Injector allows `before` and `after` attributes as metadata for the transformation.
|
||||||
|
|
||||||
|
__my-module/js/main.js__
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-transformation',
|
||||||
|
(updater) => {
|
||||||
|
updater.component('MyComponent', MyCustomComponent);
|
||||||
|
updater.reducer('myCustom', MyCustomReducer);
|
||||||
|
|
||||||
|
},
|
||||||
|
{ after: 'another-module' }
|
||||||
|
);
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
`before` and `after` also accept arrays of constraints.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-transformation',
|
||||||
|
(updater) => updater.component('MyComponent', MyCustomComponent);
|
||||||
|
{ before: ['my-transformation', 'some-other-transformation'] }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the * flag
|
||||||
|
|
||||||
|
If you really want to be sure your customisation gets loaded first or last, you can use
|
||||||
|
`*` as your `before` or `after` reference.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-transformation',
|
||||||
|
(updater) => updater.component('MyComponent', FinalTransform),
|
||||||
|
{ after: '*' }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
<div class="info" markdown="1">
|
||||||
|
This flag can only be used once per transformation.
|
||||||
|
The following are not allowed:
|
||||||
|
* `{ before: ['*', 'something-else'] }`
|
||||||
|
* `{ after: '*', before: 'something-else' }`
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Injector context
|
||||||
|
|
||||||
|
Because so much of UI design depends on context, dependency injection in the frontend is not necessarily universal. Instead, services are fetched with context.
|
||||||
|
|
||||||
|
_example_:
|
||||||
|
```js
|
||||||
|
const CalendarComponent = Injector.get('Calendar', 'AssetAdmin.FileEditForm.StartDate');
|
||||||
|
```
|
||||||
|
|
||||||
|
Likewise, services can be applied for specific contexts.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform('my-transform', (updater) => {
|
||||||
|
// Applies to all text fields in AssetAdmin
|
||||||
|
updater.component('TextField.AssetAdmin', MyTextField);
|
||||||
|
|
||||||
|
// Applies to all text fields in AssetAdmin editform
|
||||||
|
updater.component('TextField.AssetAdmin.FileEditForm', MyTextField);
|
||||||
|
|
||||||
|
// Applies to any textfield named "Title" in AssetAdmin
|
||||||
|
updater.component('TextField.AssetAdmin.*.Title', MyTextField);
|
||||||
|
|
||||||
|
// Applies to any textfield named "Title" in any admin
|
||||||
|
updater.component('TextField.*.*.Title', MyTextField);
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
To apply context-based transformations, you'll need to know the context of the component you want to customise. To learn this,
|
||||||
|
open your React Developer Tools (see above) window and inspect the component name. The
|
||||||
|
context of the component is displayed between two square brackets, appended to the original name, for example:
|
||||||
|
`TextField[TextField.AssetAdmin.FileEditForm.Title]`. The context description is hierarchical, starting
|
||||||
|
with the most general category (in this case, "Admin") and working its way down to the most specific
|
||||||
|
category (Name = 'Title'). You can use Injector to hook into the level of specificity that you want.
|
||||||
|
|
||||||
|
|
||||||
|
# Customising React components with Injector
|
||||||
|
|
||||||
|
When middleware is used to customise a React component, it is known as a [higher order component](https://facebook.github.io/react/docs/higher-order-components.html).
|
||||||
|
|
||||||
|
Using the `PhotoItem` example above, let's create a customised `PhotoItem` that allows a badge, perhaps indicating that it is new to the gallery.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const enhancedPhoto = (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 enhancedPhoto = (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 dependencies within your React component
|
||||||
|
|
||||||
|
If your component has dependencies, you can add them via the injector using the `inject()`
|
||||||
|
higher order component. The function accepts the following arguments:
|
||||||
|
|
||||||
|
```js
|
||||||
|
inject([dependencies], mapDependenciesToProps)(Component)
|
||||||
|
```
|
||||||
|
* **[dependencies]**: An array of dependencies (or a string, if just one)
|
||||||
|
* **mapDependenciesToProps**: (optional) All dependencies are passed into this function as params. The function
|
||||||
|
is expected to return a map of props to dependencies. If this parameter is not specified,
|
||||||
|
the prop names and the service names will mirror each other.
|
||||||
|
|
||||||
|
The result is a function that is ready to apply to a component.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const MyInjectedComponent = inject(
|
||||||
|
['Dependency1', 'Dependency2']
|
||||||
|
)(MyComponent);
|
||||||
|
// MyComponent now has access to props.Dependency1 and props.Dependency2
|
||||||
|
```
|
||||||
|
Here is its usage with a bit more context:
|
||||||
|
|
||||||
|
__my-module/js/components/Gallery.js__
|
||||||
|
```js
|
||||||
|
import React from 'react';
|
||||||
|
import { inject } from 'lib/Injector';
|
||||||
|
|
||||||
|
class Gallery extends React.Component {
|
||||||
|
render() {
|
||||||
|
const { SearchComponent, ItemComponent } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SearchComponent />
|
||||||
|
{this.props.items.map(item => (
|
||||||
|
<ItemComponent title={item.title} image={item.image} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default inject(
|
||||||
|
Gallery,
|
||||||
|
['GalleryItem', 'SearchBar'],
|
||||||
|
(GalleryItem, SearchBar) => ({
|
||||||
|
ItemComponent: GalleryItem,
|
||||||
|
SearchComponent: SearchBar
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using the injector directly within your component
|
||||||
|
|
||||||
|
On rare occasions, you may just want direct access to the injector in your component. If
|
||||||
|
your dependency requirements are dynamic, for example, you won't be able to explicitly
|
||||||
|
declare them in `inject()`. In cases like this, use `withInjector()`. This higher order
|
||||||
|
component puts the `Injector` instance in `context`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyGallery extends React.Component {
|
||||||
|
render () {
|
||||||
|
<div>
|
||||||
|
{this.props.items.map(item => {
|
||||||
|
const Component = this.context.injector.get(item.type);
|
||||||
|
return <Component title={item.title} image={item.image} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withInjector(MyGallery);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Injector to customise forms
|
||||||
|
|
||||||
|
Forms in the React layer are built declaratively, using the `FormSchema` API. A component called `FormBuilderLoader` is given a URL to a form schema definition, and it populates itself with fields (both structural and data-containing) and actions to create the UI for the form. Each form is required to have an `identifier` property, which is used to create context for Injector when field components are fetched. This affords developers the opportunity provide very surgical customisations.
|
||||||
|
|
||||||
|
### Updating the form schema
|
||||||
|
|
||||||
|
Most behavioural and aesthetic customisations will happen via a mutation of the form schema. For this, we'll use the `updater.form.alterSchema()` function.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-custom-form',
|
||||||
|
(updater) => {
|
||||||
|
updater.form.alterSchema(
|
||||||
|
'AssetAdmin.*',
|
||||||
|
(updateSchema) => (form, values) => (
|
||||||
|
updateSchema(
|
||||||
|
form.updateField('Title', {
|
||||||
|
myCustomProp: true
|
||||||
|
})
|
||||||
|
.getState()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `alterSchema()` function takes a callback, with an instance of `SchemaStateManager` (`form` in the above example), and a map of the current form values as parameters. `SchemaStateMangaer` allows you to declaratively update the form schema API using several helper methods, including:
|
||||||
|
|
||||||
|
* `updateField(fieldName:string, updates:object)`
|
||||||
|
* `updateFields({ myFieldName: updates:object })`
|
||||||
|
* `mutateField(fieldName:string, callback:function)`
|
||||||
|
* `setFieldComponent(fieldName:string, componentName:string)`
|
||||||
|
* `setFieldClass(fieldName:string, cssClassName:string, active:boolean)`
|
||||||
|
* `addFieldClass(fieldName:string, cssClassName:string)`
|
||||||
|
* `removeFieldClass(fieldName:string, cssClassName:string)`
|
||||||
|
|
||||||
|
<div class="info" markdown="1">
|
||||||
|
For a complete list of props that are available to update on a `Field` object,
|
||||||
|
see http://redux-form.com/6.8.0/docs/api/Field.md/#props-you-can-pass-to-field-
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notice" markdown="1">
|
||||||
|
It is critical that you end series of mutation calls with `getState()`.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Adding validation to a form
|
||||||
|
|
||||||
|
Validation for React-rendered forms is handled by the [redux-form](http://redux-form.com) package. You can inject your own middleware to add custom validation rules using the `updater.form.addValidation()` function.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-validation',
|
||||||
|
(updater) => {
|
||||||
|
updater.form.addValidation(
|
||||||
|
'AssetAdmin.*',
|
||||||
|
(validate) => (values, errors) => (
|
||||||
|
validate(
|
||||||
|
values,
|
||||||
|
{
|
||||||
|
PostalCode: values.PostalCode.length !== 5 ? 'Invalid postal code' : null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Using Injector to customise Redux state data
|
||||||
|
|
||||||
|
Before starting this tutorial, you should become familiar with the concepts of [Immutability](https://www.sitepoint.com/immutability-javascript/) and [Redux](http://reduxjs.org).
|
||||||
|
|
||||||
|
The examples use [Spread in object literals](http://redux.js.org/docs/recipes/UsingObjectSpreadOperator.html) which is at this moment in Stage 3 Proposal. If you're more comfortable with using
|
||||||
|
the `Object.assign()` API that shouldn't present any problems and should work the same.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```js
|
||||||
|
newProps = { ...oldProps, name: 'New name' };
|
||||||
|
```
|
||||||
|
is the same as
|
||||||
|
```js
|
||||||
|
newProps = Object.assign(
|
||||||
|
{},
|
||||||
|
oldProps,
|
||||||
|
{ name: 'New name' }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
To start customising, you'll need to transform an existing registered reducer, you can find what reducers are registered by importing Injector and running `Injector.reducer.getAll()`
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform('customisationName', (updater) => {
|
||||||
|
updater.reducer('assetAdmin', MyReducerTransformer);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we use the `reducer()` function on the `update` object to augment Redux state transformations.
|
||||||
|
|
||||||
|
### Using Redux dev tools
|
||||||
|
|
||||||
|
It is important to learn the basics of [Redux dev tools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en), so that you can find out what ACTIONS and payloads to intercept and modify in your Transformer should target.
|
||||||
|
|
||||||
|
Most importantly, it helps to understand the "Action" sub-tab on the right panel (bottom if your dev tools is small), as this will be the data your Transformer will most likely receive, pending other transformers that may run before/after your one.
|
||||||
|
|
||||||
|
### Structuring a transformer
|
||||||
|
|
||||||
|
We use currying to supply utilities which your transformer may require to handle the transformation.
|
||||||
|
- `originalReducer` - reducer callback which the transformer is customising, this will need to be called in most cases. This will also callback other transformations down the chain of execution. Not calling this will break the chain.
|
||||||
|
- `globalState` - state of the global Redux store. There may be data outside the current scope in the reducer which you may need to help determine the transformation.
|
||||||
|
- `state` - current state of the current scope. This is what should be used to form the new state.
|
||||||
|
- `type` - the action to fire, like in any reducer in Redux. This helps determine if your transformer should do anything.
|
||||||
|
- `payload` - the new data sent with the action to mutate the Redux store.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const MyReducerTransformer = (originalReducer) => (globalState) => (state, { type, payload }) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'EXISTING_ACTION': {
|
||||||
|
// recommended to call and return the originalReducer with the payload changed by the transformer
|
||||||
|
/* return action to call here; */
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'OVERRIDE_EXISTING_ACTION': {
|
||||||
|
// could omit the originalReducer to enforce your change or cancel the originalREducer's change
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
// it is important to return the originalReducer with original redux parameters.
|
||||||
|
return originalReducer(state, { type, payload });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### A basic transformation
|
||||||
|
|
||||||
|
This example we will illustrate modifying the payload to get different data saved into the original reducer.
|
||||||
|
|
||||||
|
We will rename anything in the breadcrumbs that is displaying "Files" to display "Custom Files" instead.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const MyReducerTransformer = (originalReducer) => (globalState) => (state, { type, payload }) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'SET_BREADCRUMBS': {
|
||||||
|
return originalReducer(state, {
|
||||||
|
type,
|
||||||
|
payload: {
|
||||||
|
breadcrumbs: payload.breadcrumbs.map((crumb) => (
|
||||||
|
(crumb.text === 'Files')
|
||||||
|
? { ...crumb, text: 'Custom Files' }
|
||||||
|
: crumb
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the globalState
|
||||||
|
|
||||||
|
Accessing the globalState is easy, as it is passed in as part of the curried functions definition.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default (originalReducer) => (globalState) => (state, { type, payload }) => {
|
||||||
|
const baseUrl = globalState.config.baseUrl;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
/* ... cases here ... */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting a different initial state
|
||||||
|
|
||||||
|
We can easily define a new initial state by providing the `state` param with a default value.
|
||||||
|
It is recommended to keep the call for the original initialState for your initialState then override values, so that you do not lose any potentially critical data that would have originally been set.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const MyReducerTransformer = (originalReducer) => () => (state, { type, payload }) => {
|
||||||
|
if (typeof state === 'undefined') {
|
||||||
|
return {
|
||||||
|
...originalReducer(state, { type, payload }),
|
||||||
|
myCustom: 'initial state here',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cancelling an action
|
||||||
|
|
||||||
|
There are valid reasons to break the chain of reducer transformations, such as cancelling the Redux store update.
|
||||||
|
However, like an original reducer in redux, you will still need to return the original state.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default (originalReducer) => (globalState) => (state, { type, payload }) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'CANCEL_THIS_ACTION': {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Calling a different action
|
||||||
|
|
||||||
|
You could manipulate the action called by the originalReducer, there isn't an example available but this block of
|
||||||
|
code will present the theory of how it can be achieved.
|
||||||
|
|
||||||
|
```js
|
||||||
|
default (originalReducer) => (globalState) => (state, { type, payload }) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'REMOVE_ERROR': {
|
||||||
|
// we'd like to archive errors instead of removing them
|
||||||
|
return originalReducer(state, {
|
||||||
|
type: 'ARCHIVE_ERROR',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Injector to customise GraphQL queries
|
||||||
|
|
||||||
|
(coming soon)
|
@ -0,0 +1,229 @@
|
|||||||
|
# Customising React Components
|
||||||
|
|
||||||
|
In this tutorial, we'll customise some form elements rendered with React to have some new features.
|
||||||
|
|
||||||
|
## An enhanced TextField
|
||||||
|
|
||||||
|
Let's add a character count to the `TextField` component. `TextField` is a built-in component in the admin area. Because the `TextField` component is fetched
|
||||||
|
through Injector, we can override it and augment it with our own functionality.
|
||||||
|
|
||||||
|
First, let's create our [higher order component](../07_ReactJS_Redux_and_GraphQL.md#customising-react-components-with-injector).
|
||||||
|
|
||||||
|
__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 []Injector](../07_ReactJS_Redux_and_GraphQL.md#the-injector-api).
|
||||||
|
|
||||||
|
__my-module/js/main.js__
|
||||||
|
```js
|
||||||
|
import Injector from 'lib/Injector';
|
||||||
|
import CharacterCounter from './components/CharacterCounter';
|
||||||
|
|
||||||
|
Injector.transform('character-count-transform', (updater) => {
|
||||||
|
updater.component('TextField', CharacterCounter);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The last thing we'll have to do is [transpile our code](../06_Javascript_Development.md#es6-and-build-tools) and load the resulting bundle file
|
||||||
|
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)
|
||||||
|
|
||||||
|
### More enhancements
|
||||||
|
|
||||||
|
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.transform('text-length-transform', (updater) => {
|
||||||
|
updater.component('TextField', TextLengthChecker);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, both components have applied themselves to the textfield.
|
||||||
|
|
||||||
|
![](../../../_images/react-di-2.png)
|
||||||
|
|
||||||
|
|
||||||
|
### Getting the 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`.
|
||||||
|
|
||||||
|
For this example, we'll imagine these two enhancements come from different modules.
|
||||||
|
|
||||||
|
__module-a/js/main.js__
|
||||||
|
```js
|
||||||
|
import Injector from 'lib/Injector';
|
||||||
|
import CharacterCounter from './components/CharacterCounter';
|
||||||
|
Injector.transform(
|
||||||
|
'character-count-transform',
|
||||||
|
(update) => update.component('TextField', CharacterCounter),
|
||||||
|
{ after: 'text-length-transform' }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
__module-b/js/main.js__
|
||||||
|
```js
|
||||||
|
import Injector from 'lib/Injector';
|
||||||
|
import TextLengthChecker from './components/TextLengthChecker';
|
||||||
|
|
||||||
|
Injector.transform(
|
||||||
|
'text-length-transform',
|
||||||
|
(updater) => updater.component('TextField', TextLengthChecker),
|
||||||
|
{ before: 'character-count-transform' }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, both components, coming from different modules, play together nicely, in the correct order.
|
||||||
|
|
||||||
|
![](../../../_images/react-di-3.png)
|
||||||
|
|
||||||
|
### Adding context
|
||||||
|
|
||||||
|
We've successfully changed the behaviour and UI of our `TextField` component using two
|
||||||
|
different two separate higher order components. By default, these are *global changes*. That is,
|
||||||
|
every text field rendered by React will receive the enhancements we've put into the injector. Though
|
||||||
|
this may sometimes be useful, more often than not, we only want to add our enhancements in certain
|
||||||
|
contexts. You may, for instance, only want your character counter to display on one specific field
|
||||||
|
in one specific form.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Let's apply our transformation to just the file edit form in AssetAdmin.
|
||||||
|
|
||||||
|
__my-module/js/main.js__
|
||||||
|
```js
|
||||||
|
import Injector from 'lib/Injector';
|
||||||
|
import TextLengthChecker from './components/TextLengthChecker';
|
||||||
|
|
||||||
|
Injector.transform('text-length-transform', (updater) => {
|
||||||
|
updater.component('TextField.AssetAdmin.FileEditForm', TextLengthChecker);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## A better form action: dealing with events
|
||||||
|
|
||||||
|
Let's make a new customisation that customises the behaviour of a button. We'll have
|
||||||
|
all form actions throw a `window.confirm()` message before executing their action. Further,
|
||||||
|
we'll apply some new style to the button if it is in a loading state.
|
||||||
|
|
||||||
|
__my-module/js/components/ConfirmingFormButton.js__
|
||||||
|
```js
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default (FormAction) => (props) => {
|
||||||
|
const newProps = {
|
||||||
|
...props,
|
||||||
|
data: {
|
||||||
|
...props.data,
|
||||||
|
buttonStyle: props.loading ? 'danger' : props.data.buttonStyle
|
||||||
|
},
|
||||||
|
handleClick(e) {
|
||||||
|
if(window.confirm('Did you really mean to click this?')) {
|
||||||
|
props.handleClick(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FormAction {...newProps} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
__my-module/js/main.js__
|
||||||
|
```js
|
||||||
|
import ConfirmingFormButton from './components/ConfirmingFormButton';
|
||||||
|
|
||||||
|
Injector.transform('confirming-button-transform', (updater) => {
|
||||||
|
updater.component('FormAction', ConfirmingFormButton, 'ConfirmingFormButton');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, when you click on any form action, it will throw a confirm window before firing its given click handler.
|
@ -0,0 +1,254 @@
|
|||||||
|
# Customising React Forms
|
||||||
|
|
||||||
|
Forms that are rendered with React use the [ReduxForm](http://redux-form.com) library and are based on schema definitions that come from the server. To customise these forms, you can apply middleware that updates the schema or applies validation.
|
||||||
|
|
||||||
|
## Toggling a form field
|
||||||
|
|
||||||
|
Let's have a field hide or show based on the state of another field. We want the field "State" to show if `Country === 'US'`.
|
||||||
|
|
||||||
|
First, we need to add a customisation to all form fields that allows them to be toggleable.
|
||||||
|
|
||||||
|
_my-module/js/src/HideableComponent.js
|
||||||
|
```js
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const HideableComponent = ({Component, ...props}) => (
|
||||||
|
props.shouldHide ? null : <Component {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
HideableComponent.propTypes = {
|
||||||
|
shouldHide: React.PropTypes.boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
HideableComponent.defaultProps = {
|
||||||
|
shouldHide: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (Component) => (props) => (
|
||||||
|
props.shouldHide ? null : <Component {...props} />
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, let's apply this through Injector.
|
||||||
|
|
||||||
|
_my-module/js/main.js_
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'toggle-field',
|
||||||
|
(updater) => {
|
||||||
|
updater.component('ReduxFormField', HideableComponentCreator);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, we need to apply a schema transformation using `updater.form.alterSchema()`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-toggle',
|
||||||
|
(updater) => {
|
||||||
|
updater.form.alterSchema(
|
||||||
|
'AssetAdmin.*',
|
||||||
|
(updateSchema) => (values, form) => {
|
||||||
|
return updateSchema(
|
||||||
|
form
|
||||||
|
.updateField('State', {
|
||||||
|
shouldHide: values.Country !== 'US'
|
||||||
|
})
|
||||||
|
.getState()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditionally adding a CSS class to a form field
|
||||||
|
|
||||||
|
In this example, we'll add the class "danger" to the `Price` field when `TicketsRemaining` is less than 10.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-css',
|
||||||
|
(updater) => {
|
||||||
|
updater.form.alterSchema(
|
||||||
|
'AssetAdmin.*',
|
||||||
|
(updateSchema) => (values, form) => {
|
||||||
|
return updateSchema(
|
||||||
|
form
|
||||||
|
.setFieldClass('Price', 'danger', (values.TicketsRemaining < 10))
|
||||||
|
.getState()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using a custom component
|
||||||
|
|
||||||
|
In this example, we'll replace a plain text field for `PhoneNumber` with one that is broken up into three separate text fields.
|
||||||
|
|
||||||
|
First, we need to create the `PrettyPhoneNumberField` component.
|
||||||
|
|
||||||
|
_my-module/js/src/PrettyPhoneNumberField.js_
|
||||||
|
```js
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const [area, exchange, ext] = props.value.split('-');
|
||||||
|
function handleChange (i, e) {
|
||||||
|
const parts = props.value.split('-');
|
||||||
|
parts[i] = e.target.value;
|
||||||
|
const formatted = parts.join('-');
|
||||||
|
props.onChange(formatted, e);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
(<input type="text" value={area} onChange={handleChange.bind(null, 0)}/>)
|
||||||
|
<input type="text" value={exchange} onChange={handleChange.bind(null, 1)}/> -
|
||||||
|
<input type="text" value={ext} onChange={handleChange.bind(null, 2)}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, we'll need to override the `PhoneNumber` field with custom component.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-custom-component',
|
||||||
|
(updater) => {
|
||||||
|
updater.form.alterSchema(
|
||||||
|
'AssetAdmin.*',
|
||||||
|
(updateSchema) => (values, form) => {
|
||||||
|
return updateSchema(
|
||||||
|
form
|
||||||
|
.setFieldComponent('PhoneNumber', 'PrettyPhoneNumberField')
|
||||||
|
.getState()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom validation
|
||||||
|
|
||||||
|
In this example, we'll add a computed validation rule. If `Country` is set to "US", we'll validate the postal code against a length of 5. If not, we'll use a length of 4.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-validation',
|
||||||
|
(updater) => {
|
||||||
|
updater.form.addValidation(
|
||||||
|
'AssetAdmin.*',
|
||||||
|
(validate) => (values, errors) => {
|
||||||
|
const requiredLength = values.Country === 'US' ? 5 : 4;
|
||||||
|
if (!values.Country || !values.PostalCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return validate(
|
||||||
|
values,
|
||||||
|
{
|
||||||
|
...errors,
|
||||||
|
PostalCode: values.PostalCode.length !== requiredLength ? 'Invalid postal code' : null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a "confirm" state to a form action
|
||||||
|
|
||||||
|
In this example, we'll have a form action expose two new buttons for "confirm" and "cancel" when clicked. This type of behaviour could be useful for a delete action, for instance, as an alternative to throwing `window.confirm()`.
|
||||||
|
|
||||||
|
First, we need to create the `ConfirmingFormAction` component.
|
||||||
|
|
||||||
|
_my-module/js/src/ConfirmingFormAction.js_
|
||||||
|
```js
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default (FormAction) => {
|
||||||
|
class ConfirmingFormAction extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { confirming: false };
|
||||||
|
this.confirm = this.confirm.bind(this);
|
||||||
|
this.cancel = this.cancel.bind(this);
|
||||||
|
this.preClick = this.preClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm(e) {
|
||||||
|
this.props.handleClick(e, this.props.name || this.props.id);
|
||||||
|
this.setState({ confirming: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.setState({ confirming: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
preClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState( {confirming: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const extraButtons = [];
|
||||||
|
const { confirmText, cancelText } = this.props;
|
||||||
|
const buttonProps = {
|
||||||
|
...this.props,
|
||||||
|
extraClass: 'ss-ui-action-constructive',
|
||||||
|
attributes: {
|
||||||
|
...this.props.attributes,
|
||||||
|
type: 'button'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
delete buttonProps.name;
|
||||||
|
delete buttonProps.type;
|
||||||
|
|
||||||
|
const hideStyle = {
|
||||||
|
display: this.state.confirming ? null : 'none'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormAction { ...buttonProps } handleClick={this.preClick} />
|
||||||
|
<button style={hideStyle} key="confirm" type="submit" name={this.props.name} onClick={this.confirm}>
|
||||||
|
{confirmText}
|
||||||
|
</button>
|
||||||
|
<button style={hideStyle} key="cancel" type="button" onClick={this.cancel}>{cancelText}</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConfirmingFormAction;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, let's apply this new component to a very specific form action.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Injector.transform(
|
||||||
|
'my-confirm',
|
||||||
|
(updater) => {
|
||||||
|
updater.form.alterSchema(
|
||||||
|
'AssetAdmin.*',
|
||||||
|
(updateSchema) => (values, form) => {
|
||||||
|
return updateSchema(
|
||||||
|
form
|
||||||
|
.updateField('action_delete', {
|
||||||
|
confirmText: 'Are you sure you want to delete?',
|
||||||
|
cancelText: 'No!! Cancel!!!!'
|
||||||
|
})
|
||||||
|
.getState()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
@ -13,16 +13,6 @@ simple checkbox.
|
|||||||
For a deeper introduction to the inner workings of the CMS, please refer to our
|
For a deeper introduction to the inner workings of the CMS, please refer to our
|
||||||
guide on [CMS Architecture](/developer_guides/customising_the_admin_interface/cms_architecture).
|
guide on [CMS Architecture](/developer_guides/customising_the_admin_interface/cms_architecture).
|
||||||
|
|
||||||
## Redux Devtools
|
|
||||||
|
|
||||||
It's important to be able to view the state of the React application when you're debugging and
|
|
||||||
building the interface.
|
|
||||||
|
|
||||||
To be able to view the state, you'll need to be in a dev environment
|
|
||||||
and have the [Redux Devtools](https://github.com/zalmoxisus/redux-devtools-extension)
|
|
||||||
installed on Google Chrome or Firefox, which can be found by searching with your favourite search
|
|
||||||
engine.
|
|
||||||
|
|
||||||
## Overload a CMS template
|
## Overload a CMS template
|
||||||
|
|
||||||
If you place a template with an identical name into your application template
|
If you place a template with an identical name into your application template
|
||||||
@ -204,603 +194,10 @@ To make the actions more user-friendly you can also use alternating buttons as
|
|||||||
detailed in the [CMS Alternating Button](cms_alternating_button)
|
detailed in the [CMS Alternating Button](cms_alternating_button)
|
||||||
how-to.
|
how-to.
|
||||||
|
|
||||||
## React components
|
## React-rendered UI
|
||||||
|
For sections of the admin that are rendered with React, Redux, and GraphQL, please refer
|
||||||
Some admin modules render their UI with React, a popular Javascript library created by Facebook.
|
to [the introduction on those concepts](../07_ReactJS_Redux_and_GraphQL.md),
|
||||||
For these sections, rendering happens via client side scripts that create and inject HTML
|
as well as their respective How-To's in this section.
|
||||||
declaratively using data structures. These UI elements are known as "components" and
|
|
||||||
represent the fundamental building block of a React-rendered interface.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
### Using dependencies within your component
|
|
||||||
|
|
||||||
If your component has dependencies, you can add them via the injector using the `inject()`
|
|
||||||
higher order component. The function accepts the following arguments:
|
|
||||||
|
|
||||||
```js
|
|
||||||
inject(Component, [dependencies], mapDependenciesToProps)
|
|
||||||
```
|
|
||||||
* **Component** The component definition to inject into
|
|
||||||
* **[dependencies]**: An array of dependencies (or a string, if just one)
|
|
||||||
* **mapDependenciesToProps**: (optional) All depdencies are passed into this function as params. The function
|
|
||||||
is expected to return a map of props to dependencies. If this parameter is not specified,
|
|
||||||
the prop names and the service names will mirror each other.
|
|
||||||
|
|
||||||
__my-module/js/components/Gallery.js__
|
|
||||||
```js
|
|
||||||
import React from 'react';
|
|
||||||
import { inject } from 'lib/Injector';
|
|
||||||
|
|
||||||
class Gallery extends React.Component {
|
|
||||||
render() {
|
|
||||||
const { SearchComponent, ItemComponent } = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SearchComponent />
|
|
||||||
{this.props.items.map(item => (
|
|
||||||
<ItemComponent title={item.title} image={item.image} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default inject(
|
|
||||||
Gallery,
|
|
||||||
['GalleryItem', 'SearchBar'],
|
|
||||||
(GalleryItem, SearchBar) => ({
|
|
||||||
ItemComponent: GalleryItem,
|
|
||||||
SearchComponent: SearchBar
|
|
||||||
})
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 enhancedPhoto = (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 enhancedPhoto = (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.transform('my-transformation', (update) => {
|
|
||||||
update('TextField', CharacterCounter);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Much like the configuration layer, we need to specify a name for this transformation. This
|
|
||||||
will help other modules negotiate their priority over the injector in relation to yours.
|
|
||||||
|
|
||||||
The second parameter of the `transform` argument is a callback which receives a `update()` 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 transpile our code and load the resulting bundle file
|
|
||||||
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.transform('my-other-transformation', (update) => {
|
|
||||||
update('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.
|
|
||||||
|
|
||||||
For this example, we'll imagine these two enhancements come from different modules.
|
|
||||||
|
|
||||||
__module-a/js/main.js__
|
|
||||||
```js
|
|
||||||
import Injector from 'lib/Injector';
|
|
||||||
import CharacterCounter from './components/CharacterCounter';
|
|
||||||
Injector.transform(
|
|
||||||
'my-transformation',
|
|
||||||
(update) => update('TextField', CharacterCounter),
|
|
||||||
{ after: 'my-other-transformation' }
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
__module-b/js/main.js__
|
|
||||||
```js
|
|
||||||
import Injector from 'lib/Injector';
|
|
||||||
import TextLengthChecker from './components/TextLengthChecker';
|
|
||||||
|
|
||||||
Injector.transform(
|
|
||||||
'my-other-transformation',
|
|
||||||
(update) => update('TextField', TextLengthChecker),
|
|
||||||
{ before: 'my-transformation' }
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, both components, coming from different modules, play together nicely, in the correct
|
|
||||||
order.
|
|
||||||
|
|
||||||
![](../../../_images/react-di-3.png)
|
|
||||||
|
|
||||||
`before` and `after` also accept arrays of constraints.
|
|
||||||
|
|
||||||
```js
|
|
||||||
Injector.transform(
|
|
||||||
'my-transformation',
|
|
||||||
(update) => update('TextField', TextLengthChecker),
|
|
||||||
{ before: ['my-transformation', 'some-other-transformation'] }
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using the * flag
|
|
||||||
|
|
||||||
If you really want to be sure your customisation gets loaded first or last, you can use
|
|
||||||
`*` as your `before` or `after` reference.
|
|
||||||
|
|
||||||
```js
|
|
||||||
Injector.transform(
|
|
||||||
'my-transformation',
|
|
||||||
(update) => update('TextField', FinalTransform),
|
|
||||||
{ after: '*' }
|
|
||||||
);
|
|
||||||
```
|
|
||||||
**Note**: This flag can only be used once per transformation.
|
|
||||||
The following are not allowed:
|
|
||||||
* `{ before: ['*', 'something-else'] }`
|
|
||||||
* `{ after: '*', before: 'something-else' }`
|
|
||||||
|
|
||||||
### Helpful tip: Name your higher order components
|
|
||||||
|
|
||||||
Now that we have multiple enhancements happening to the same component, it will be really
|
|
||||||
useful for debugging purposes to reveal the names of each enhancement on the `displayName` of
|
|
||||||
the component. This will really help you when viewing the rendered component tree in
|
|
||||||
[React Dev Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en).
|
|
||||||
|
|
||||||
For this, you can use the third parameter of the `update()` function. It takes an arbitrary
|
|
||||||
name for the enhancement you're applying.
|
|
||||||
|
|
||||||
__module-a/js/main.js__
|
|
||||||
```js
|
|
||||||
(update) => update('TextField', CharacterCounter, 'CharacterCounter')
|
|
||||||
```
|
|
||||||
__module-b/js/main.js__
|
|
||||||
```js
|
|
||||||
(update) => update('TextField', TextLengthChecker, 'TextLengthChecker')
|
|
||||||
```
|
|
||||||
### Dealing with events
|
|
||||||
Let's make a new customisation that customises the behaviour of a button. We'll have
|
|
||||||
all form actions throw a `window.confirm()` message before executing their action. Further,
|
|
||||||
we'll apply some new style to the button if it is in a loading state.
|
|
||||||
|
|
||||||
__my-module/js/components/ConfirmingFormButton.js__
|
|
||||||
```js
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default (FormAction) => (props) => {
|
|
||||||
const newProps = {
|
|
||||||
...props,
|
|
||||||
data: {
|
|
||||||
...props.data,
|
|
||||||
buttonStyle: props.loading ? 'danger' : props.data.buttonStyle
|
|
||||||
},
|
|
||||||
handleClick(e) {
|
|
||||||
if(window.confirm('Did you really mean to click this?')) {
|
|
||||||
props.handleClick(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <FormAction {...newProps} />
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
__my-module/js/main.js__
|
|
||||||
```js
|
|
||||||
import ConfirmingFormButton from './components/ConfirmingFormButton';
|
|
||||||
|
|
||||||
Injector.transform('my-transformation', (update) => {
|
|
||||||
update('FormAction', ConfirmingFormButton, 'ConfirmingFormButton');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
### 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 directly within your component
|
|
||||||
|
|
||||||
On rare occasions, you may just want direct access to the injector in your component. If
|
|
||||||
your dependency requirements are dynamic, for example, you won't be able to explicitly
|
|
||||||
declare them in `inject()`. In cases like this, use `withInjector()`. This higher order
|
|
||||||
component puts the `Injector` instance in `context`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
class MyGallery extends React.Component {
|
|
||||||
render () {
|
|
||||||
<div>
|
|
||||||
{this.props.items.map(item => {
|
|
||||||
const Component = this.context.injector.get(item.type);
|
|
||||||
return <Component title={item.title} image={item.image} />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withInjector(MyGallery);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interfacing with legacy CMS JavaScript
|
|
||||||
|
|
||||||
One of the great things about ReactJS is that it works great with DOM based libraries like jQuery and Entwine. To allow legacy-land scripts to notify your React component about changes, add the following.
|
|
||||||
|
|
||||||
__my-component.js__
|
|
||||||
```javascript
|
|
||||||
import SilverStripeComponent from 'silverstripe-component';
|
|
||||||
|
|
||||||
class MyComponent extends SilverStripeComponent {
|
|
||||||
componentDidMount() {
|
|
||||||
super.componentDidMount();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
super.componentWillUnmount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MyComponent;
|
|
||||||
```
|
|
||||||
|
|
||||||
This is functionally no different from the first example. But it's a good idea to be explicit and add these `super` calls now. You will inevitably add `componentDidMount` and `componentWillUnmount` hooks to your component and it's easy to forget to call `super` then.
|
|
||||||
|
|
||||||
So what's going on when we call those? Glad you asked. If you've passed `cmsEvents` into your component's `props`, wonderful things will happen.
|
|
||||||
|
|
||||||
Let's take a look at some examples.
|
|
||||||
|
|
||||||
### Getting data into a component
|
|
||||||
|
|
||||||
Sometimes you'll want to call component methods when things change in legacy-land. For example when a CMS tab changes you might want to update some component state.
|
|
||||||
|
|
||||||
__main.js__
|
|
||||||
```javascript
|
|
||||||
import $ from 'jquery';
|
|
||||||
import React, { PropTypes, Component } from 'react';
|
|
||||||
import MyComponent from './my-component';
|
|
||||||
|
|
||||||
$.entwine('ss', function ($) {
|
|
||||||
$('.my-component-wrapper').entwine({
|
|
||||||
getProps: function (props) {
|
|
||||||
var defaults = {
|
|
||||||
cmsEvents: {
|
|
||||||
'cms.tabchanged': function (event, title) {
|
|
||||||
// Call a Redux action to update state.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return $.extend(true, defaults, props);
|
|
||||||
},
|
|
||||||
onadd: function () {
|
|
||||||
var props = this.getProps();
|
|
||||||
|
|
||||||
React.render(
|
|
||||||
<MyComponent {...props} />,
|
|
||||||
this[0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
__legacy.js__
|
|
||||||
```javascript
|
|
||||||
(function ($) {
|
|
||||||
$.entwine('ss', function ($) {
|
|
||||||
$('.cms-tab').entwine({
|
|
||||||
onclick: function () {
|
|
||||||
$(document).trigger('cms.tabchanged', this.find('.title').text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}(jQuery));
|
|
||||||
```
|
|
||||||
|
|
||||||
Each key in `props.cmsEvents` gets turned into an event listener by `SilverStripeComponent.componentDidMount`. When a legacy-land script triggers that event on `document`, the associated component callback is invoked, with the component's context bound to it.
|
|
||||||
|
|
||||||
All `SilverStripeComponent.componentWillUnmount` does is clean up the event listeners when they're no longer required.
|
|
||||||
|
|
||||||
There are a couple of important things to note here:
|
|
||||||
|
|
||||||
1. Both files are using the same `ss` namespace.
|
|
||||||
2. Default properties are defined using the `getProps` method.
|
|
||||||
|
|
||||||
This gives us the flexibility to add and override event listeners from legacy-land. We're currently updating the current tab's title when `.cms-tab` is clicked. But say we also wanted to highlight the tab. We could do something like this.
|
|
||||||
|
|
||||||
__legacy.js__
|
|
||||||
```javascript
|
|
||||||
(function ($) {
|
|
||||||
$.entwine('ss', function ($) {
|
|
||||||
$('.main .my-component-wrapper').entwine({
|
|
||||||
getProps: function (props) {
|
|
||||||
return this._super({
|
|
||||||
cmsEvents: {
|
|
||||||
'cms.tabchanged': function (event, title) {
|
|
||||||
// Call a Redux action to update state.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.cms-tab').entwine({
|
|
||||||
onclick: function () {
|
|
||||||
$(document).trigger('cms.tabchanged', this.find('.title').text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}(jQuery));
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we're using Entwine to override the `getProps` method in `main.js`. Note we've made the selector more specific `.main .my-component-wrapper`. The most specific selector comes first in Entwine, so here our new `getProps` gets called, which passes the new callback to the `getProps` method defined in `main.js`.
|
|
||||||
|
|
||||||
### Getting data out of a component
|
|
||||||
|
|
||||||
There are times you'll want to update things in legacy-land when something changes in you component.
|
|
||||||
|
|
||||||
`SilverStripeComponent` has a handly method `emitCmsEvents` to help with this.
|
|
||||||
|
|
||||||
__my-component.js__
|
|
||||||
```javascript
|
|
||||||
import SilverStripeComponent from 'silverstripe-component';
|
|
||||||
|
|
||||||
class MyComponent extends SilverStripeComponent {
|
|
||||||
componentDidMount() {
|
|
||||||
super.componentDidMount();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
super.componentWillUnmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.emitCmsEvent('my-component.title-changed', this.state.title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MyComponent;
|
|
||||||
```
|
|
||||||
|
|
||||||
__legacy.js__
|
|
||||||
```javascript
|
|
||||||
(function ($) {
|
|
||||||
$.entwine('ss', function ($) {
|
|
||||||
$('.cms-tab').entwine({
|
|
||||||
onmatch: function () {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
$(document).on('my-component.title-changed', function (event, title) {
|
|
||||||
self.find('.title').text(title);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onunmatch: function () {
|
|
||||||
$(document).off('my-component.title-changed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}(jQuery));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementing handlers
|
### Implementing handlers
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user