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.
// my-module/js/components/CharacterCounter.js
import React from 'react';
const CharacterCounter = (TextField) => (props) => (
<div>
<TextField {...props} />
<small>Character count: {props.value.length}</small>
</div>
);
export default CharacterCounter;
Now let's add this higher order component to Injector.
// my-module/js/main.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 and load the resulting bundle file into the admin page.
# my-module/_config/config.yml
---
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:
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
import React from 'react';
// ...
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
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.
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
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
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
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.
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
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
import React from 'react';
export default (FormAction) => (props) => {
const newProps = {
...props,
data: {
...props.data,
buttonStyle: props.loading ? 'danger' : props.data.buttonStyle
},
handleClick(e) {
/* eslint-disable-next-line no-alert */
if (window.confirm('Did you really mean to click this?')) {
props.handleClick(e);
}
}
};
return <FormAction {...newProps} />;
};
// my-module/js/main.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.