strapi/docs/3.x.x/en/plugin-development/frontend-use-cases.md

705 lines
16 KiB
Markdown
Raw Normal View History

2018-01-10 13:36:24 +01:00
# Front-end Use Cases
2018-01-10 11:32:24 +01:00
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`.
```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`.
```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`.
```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`.
```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.
2018-01-10 11:32:24 +01:00
### Create your injectedComponent
**Path -** `./plugins/another-plugin/admin/src/extendables/BarContainer/index.js`;
```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
2018-01-10 11:32:24 +01:00
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`.
```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`.
```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);
```
***
2018-01-10 11:45:01 +01:00
## Execute logic before mounting the plugin
2018-01-10 11:32:24 +01:00
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`.
```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;
```
***
2018-01-10 11:45:01 +01:00
## Prevent plugin rendering
2018-01-10 11:32:24 +01:00
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`.
```json
{
"routes": [
{
"method": "GET",
"path": "/autoReload",
"handler": "MyPlugin.autoReload",
"config": {
"policies": []
}
}
]
}
```
Then the associated handler:
**Path —** `./app/plugins/my-plugin/controllers/MyPlugin.js`.
```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:
```js
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`.
```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`.
```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`
```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`
```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](https://facebook.github.io/immutable-js/) to structure your data.
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/reducer.js`
```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`
```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';
2018-01-10 11:32:24 +01:00
// 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 :
```js
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](https://github.com/reactjs/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:
1. Computational power
2. Memoization
3. Composability
Creating a selector:
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/selectors.js`
```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`
```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);
}
```