16 KiB
Front-end Use Cases
This section gives use cases examples on front-end plugin development.
Plugin advanced usage
This section contains advanced resources to develop plugins.
Inject design
The ExtendComponent
allows you to inject design from one plugin into another.
Example
Let's say that you want to enable another plugin to inject a component into the top area of your plugin's container called FooPage
;
Path — ./plugins/my-plugin/admin/src/containers/FooPage/actions.js
.
import {
ON_TOGGLE_SHOW_LOREM,
} from './constants';
export function onToggleShowLorem() {
return {
type: ON_TOGGLE_SHOW_LOREM,
};
}
Path — ./plugins/my-plugin/admin/src/containers/FooPage/index.js
.
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import PropTypes from 'prop-types';
// Import the ExtendComponent
import ExtendComponent from 'components/ExtendComponent';
// Utils
import injectReducer from 'utils/injectReducer';
// Actions
import { onToggleShowLorem } from './action'
import reducer from './reducer';
// Selectors
import { makeSelectShowLorem } from './selectors';
class FooPage extends React.Component {
render() {
const lorem = this.props.showLorem ? <p>Lorem ipsum dolor sit amet, consectetur adipiscing</p> : '';
return (
<div>
<h1>This is FooPage container</h1>
<ExtendComponent
area="top"
container="FooPage"
plugin="my-plugin"
{...props}
/>
{lorem}
</div>
);
}
}
FooPage.propTypes = {
onToggleShowLorem: PropTypes.func.isRequired,
showLorem: PropTypes.bool.isRequired,
};
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
onToggleShowLorem,
},
dispatch,
);
}
const mapStateToProps = createStructuredSelector({
showLorem: makeSelectShowLorem(),
});
const withConnect = connect(mapDispatchToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'fooPage', reducer });
export default compose(
withReducer,
withConnect,
)(FooPage);
Path — ./plugins/my-plugin/admin/src/containers/FooPage/reducer.js
.
import { fromJS } from 'immutable';
import { ON_TOGGLE_SHOW_LOREM } from './constants';
const initialState = fromJS({
showLorem: false,
});
function fooPageReducer(state= initialState, action) {
switch (action.type) {
case ON_TOGGLE_SHOW_LOREM:
return state.set('showLorem', !state.get('showLorem'));
default:
return state;
}
}
export default fooPageReducer;
Path — ./plugins/my-plugin/admin/src/containers/FooPage/selectors.js
.
import { createSelector } from 'reselect';
/**
* Direct selector to the fooPage state domain
*/
const selectFooPageDomain = () => state => state.get('fooPage');
/**
* Other specific selectors
*/
const makeSelectShowLorem = () => createSelector(
selectFooPageDomain(),
(substate) => substate.get('showLorem'),
);
export { makeSelectShowLorem };
That's all now your plugin's container is injectable!
Let's see how to inject a React Component from a plugin into another.
Create your injectedComponent
Path - ./plugins/another-plugin/admin/src/extendables/BarContainer/index.js
;
import React from 'react';
import PropTypes from 'prop-types';
// Import our Button component
import Button from 'components/Button';
// Other imports such as actions, selectors, sagas, reducer...
class BarContainer extends React.Component {
render() {
return (
<div>
<Button primary onClick={this.props.onToggleShowLorem}>
Click me to show lorem paragraph
</Button>
</div>
);
}
}
BarContainer.propTypes = {
onToggleShowLorem: PropTypes.func,
};
BarContainer.defaultProps = {
onToggleShowLorem: () => {},
};
export default BarContainer;
Tell the admin that you want to inject a React Component from a plugin into another
You have to create a file called injectedComponents.js
at the root of your another-plugin
src folder.
Path — ./plugins/another-plugin/admin/src/injectedComponents.js
.
import BarContainer from 'extendables/BarContainer';
// export an array containing all the injected components
export default [
{
area: 'top',
container: 'FooPage',
injectedComponent: BarContainer,
plugin: 'my-plugin',
},
];
Just by doing so, the another-plugin
will add a Button
which toggles the lorem
paragraph in the FooPage
view.
Routeless container store injection
If you have a container which can be a child of several other containers (i.e. it doesn't have a route); you'll have to inject it directly in the ./plugins/my-plugin/admin/src/containers/App/index.js
file as follows :
Path — ./plugins/my-plugin/admin/src/containers/App/index.js
.
// ...
import fooReducer from 'containers/Foo/reducer';
import fooSaga from 'container/Foo/sagas';
import saga from './sagas';
import { makeSelectFoo } from './selectors';
// ...
export class App extends React.Component {
render() {
return (
<div className={styles.app}>
<Switch>
{*/ List of all your routes here */}
</Switch>
</div>
);
}
}
// ...
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
},
dispatch
);
}
const mapStateToProps = createStructuredSelector({
// ...
});
const withConnect = connect(mapStateToProps, mapDispatchToProps);
// Foo reducer
const withFooReducer = injectReducer({ key: 'foo', reducer: fooReducer });
// Global reducer
const withReducer = injectReducer({ key: 'global', reducer });
// Foo saga
const withFooSaga = injectSaga({ key: 'foo', saga: fooSaga });
// Global saga
const withSaga = injectSaga({ key: 'global', saga });
export default compose(
withFooReducer,
withReducer,
withFooSaga,
withSaga,
withConnect,
)(App);
Execute logic before mounting the plugin
You can execute a business logic before your plugin is being mounted.
Usage
To do this, you need to create bootstrap.js
file at the root of your src
plugin's folder.
This file must contains a default functions that returns a Promise
.
Example
In this example, we want to populate the left menu with links that will refer to our Content Types.
Path — ./app/plugins/content-manager/admin/src/bootstrap.js
.
import { generateMenu } from 'containers/App/sagas';
// This method is executed before the load of the plugin
const bootstrap = (plugin) => new Promise((resolve, reject) => {
generateMenu()
.then(menu => {
plugin.leftMenuSections = menu;
resolve(plugin);
})
.catch(e => reject(e));
});
export default bootstrap;
Prevent plugin rendering
You can prevent your plugin from being rendered if some conditions aren't met.
Usage
To disable your plugin's rendering, you can simply create requirements.js
file at the root of your src
plugin's folder.
This file must contain a default function that returns a Promise
.
Example
Let's say that you want to disable your plugin if the server autoReload config is disabled in development mode.
Path — ./app/config/environments/development/server.json
.
{
"host": "localhost",
"port": 1337,
"autoReload": {
"enabled": true
},
"cron": {
"enabled": false
}
}
You'll first create a request to check if the autoReload
config is enabled.
Path — ./app/plugins/my-plugin/config/routes.json
.
{
"routes": [
{
"method": "GET",
"path": "/autoReload",
"handler": "MyPlugin.autoReload",
"config": {
"policies": []
}
}
]
}
Then the associated handler:
Path — ./app/plugins/my-plugin/controllers/MyPlugin.js
.
const _ = require('lodash');
const send = require('koa-send');
module.exports = {
autoReload: async ctx => {
ctx.send({ autoReload: _.get(strapi.config.environments, 'development.server.autoReload', false) });
}
}
Finally, you'll create a file called requirements.js
at the root of your plugin's src folder.
The default function exported must return a Promise
.
If you wan't to prevent the plugin from being rendered you'll have to set plugin.preventComponentRendering = true;
.
In this case, you'll have to set:
plugin.blockerComponentProps = {
blockerComponentTitle: 'my-plugin.blocker.title',
blockerComponentDescription: 'my-plugin.blocker.description',
blockerComponentIcon: 'fa-refresh',
};
To follow the example above:
Path — ./app/plugins/my-plugin/admin/src/requirements.js
.
// Use our request helper
import request from 'utils/request';
const shouldRenderCompo = (plugin) => new Promise((resolve, request) => {
request('/my-plugin/autoReload')
.then(response => {
// If autoReload is enabled the response is `{ autoReload: true }`
plugin.preventComponentRendering = !response.autoReload;
// Set the BlockerComponent props
plugin.blockerComponentProps = {
blockerComponentTitle: 'my-plugin.blocker.title',
blockerComponentDescription: 'my-plugin.blocker.description',
blockerComponentIcon: 'fa-refresh',
blockerComponentContent: 'renderIde', // renderIde will add an ide section that shows the development environment server.json config
};
return resolve(plugin);
})
.catch(err => reject(err));
});
export default shouldRenderCompo;
Customization
You can render your own custom blocker by doing as follows:
Path — ./app/plugins/my-plugin/admin/src/requirements.js
.
// Use our request helper
import request from 'utils/request';
// Your custom blockerComponentProps
import MyCustomBlockerComponent from 'components/MyCustomBlockerComponent';
const shouldRenderCompo = (plugin) => new Promise((resolve, request) => {
request('/my-plugin/autoReload')
.then(response => {
// If autoReload is enabled the response is `{ autoReload: true }`
plugin.preventComponentRendering = !response.autoReload;
// Tell which component to be rendered instead
plugin.blockerComponent = MyCustomBlockerComponent;
return resolve(plugin);
})
.catch(err => reject(err));
});
export default shouldRenderCompo;
Using React/Redux and sagas
If your application is going to interact with some back-end application for data, we recommend using redux saga for side effect management. This short tutorial will show how to fetch data using actions/reducer/sagas.
Constants declaration
Path — ./plugins/my-plugin/admin/src/containers/FooPage/constants.js
export const DATA_FETCH = 'MyPlugin/FooPage/DATA_FETCH';
export const DATA_FETCH_ERROR = 'MyPlugin/FooPage/DATA_FETCH_ERROR';
export const DATA_FETCH_SUCCEEDED = 'MyPlugin/FooPage/DATA_FETCH_SUCCEEDED';
Actions declaration
Path — ./plugins/my-plugin/admin/src/containers/FooPage/actions.js
import {
DATA_FETCH,
DATA_FETCH_ERROR,
DATA_FETCH_SUCCEEDED,
} from './constants';
export function dataFetch(params) {
return {
type: DATA_FETCH,
params,
};
}
export function dataFetchError(errorMessage) {
return {
type: DATA_FETCH_ERROR,
errorMessage,
};
}
export function dataFetchSucceeded(data) {
return {
type: DATA_FETCH_SUCCEEDED,
data,
};
}
Reducer
We strongly recommend to use Immutable.js to structure your data.
Path — ./plugins/my-plugin/admin/src/containers/FooPage/reducer.js
import { fromJS, Map } from 'immutable';
import {
DATA_FETCH_ERROR,
DATA_FETCH_SUCCEEDED,
} from './constants';
const initialState = fromJS({
data: Map({}),
error: false,
errorMessage: '',
loading: true,
});
function fooPageReducer(state = initialState, action) {
switch (action.type) {
case DATA_FETCH_ERROR:
return state
.set('error', true)
.set('errorMessage', action.errorMessage)
.set('loading', false);
case DATA_FETCH_SUCCEEDED:
return state
.set('data', Map(action.data))
.set('error', false)
.set('errorMessage', '')
.set('loading', false);
default:
return state;
}
}
export default fooPageReducer;
Sagas
Path — ./plugins/my-plugin/admin/src/containers/FooPage/sagas.js
import { LOCATION_CHANGE } from 'react-router-redux';
import { takeLatest, put, fork, call, take, cancel } from 'redux-saga/effects';
// Use our request helper
import request from 'utils/request';
// Actions
import { dataFetchError, dataFetchSucceeded } from './actions';
import { DATA_FETCH } from './constants';
export function* fetchData(action) {
try {
const requestUrl = `/baseUrl/${action.params}`;
const opts = {
method: 'GET',
};
// Fetch data
const response = yield call(request, requestUrl, opts);
// Pass the response to the reducer
yield put(dataFetchSucceeded(response));
} catch(error) {
yield put(dataFetchError(error));
}
}
// Individual export for testing
function* defaultSaga() {
// Listen to DATA_FETCH event
const fetchDataWatcher = yield fork(takeLatest, DATA_FETCH, fetchData);
// Cancel watcher
yield take(LOCATION_CHANGE);
yield cancel(fetchDataWatcher);
}
export default defaultSaga;
N.B. You can use a selector in your sagas :
import { put, select, fork, call, take, cancel } from 'redux-saga/effects';
import { makeSelectUserName } from './selectors';
export function* foo() {
try {
const userName = yield select(makeSelectUserName());
// ...
} catch(error) {
// ...
}
}
function defaultSaga() {
// ...
}
export default defaultSaga;
Selectors
Reselect is a library used for slicing your redux state and providing only the relevant sub-tree to a react component. It has three key features:
- Computational power
- Memoization
- Composability
Creating a selector:
Path — ./plugins/my-plugin/admin/src/containers/FooPage/selectors.js
import { createSelector } from 'reselect';
/**
* Direct selector to the fooPage state domain
*/
const selectFooPageDomain = () => state => state.get('fooPage');
/**
* Other specific selectors
*/
const makeSelectLoading = () => createSelector(
selectFooPageDomain(),
(substate) => substate.get('loading'),
);
/**
* Default selector used by FooPage
*/
const selectFooPage = () => createSelector(
selectFooDomain(),
(substate) => substate.toJS()
);
export default selectFooPage;
export { makeSelectLoading };
Example
Path — ./plugins/my-plugin/admin/src/containers/FooPage/index.js
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect, compose } from 'react-redux';
import PropTypes from 'prop-types';
// Main router
import { router } from 'app';
// Utils
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
// Actions
import { dataFetch } from './actions';
// sagas
import saga from './sagas';
// Selectors
import selectFooPage from './selectors';
// Reducer
import reducer from './reducer';
export class FooPage extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.error !== nextProps.error && nextProps.error) {
strapi.notification.error(nextProps.errorMessage);
}
}
componentDidUpdate(prevProps) {
if (prevProps.match.pathname !== this.props.pathname) {
this.props.dataFetch(this.props.match.params.bar);
}
}
render() {
if (this.props.error) return <div>An error occurred</div>;
return (
<div>
<h4>Data display</h4>
<span>{this.props.data.foo}</span>
<span>{this.props.data.bar}</span>
</div>
);
}
FooPage.propTypes = {
data: PropTypes.object.isRequired,
dataFetch: PropTypes.func.isRequired,
error: PropTypes.bool.isRequired,
errorMessage: PropTypes.string.isRequired,
match: PropTypes.object.isRequired,
};
const mapStateToProps = selectFoo();
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
dataFetch,
},
dispatch
);
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'fooPage', reducer });
const withSagas = injectSaga({ key: 'fooPage', saga });
export default compose(
withReducer,
withSagas,
withConnect,
)(FooPage);
}