2017-10-11 10:51:28 +02:00

10 KiB

Helpers

Strapi provides helpers so you don't have to develop again and again the same generic functions.

Request helper

A request helper is available to handle all requests inside a plugin.

It takes three arguments:

  • requestUrl: The url we want to fetch.
  • options: Please refer to this documentation.
  • true: This third argument is optional. If true is passed the response will be sent only if the server has restarted check out the example.

Usage

Path - /plugins/my-plugin/admin/src/containers/**/sagas.js.

import { call, fork, put, takeLatest } from 'redux-saga/effects';

// Our request helper
import request from 'utils/request';
import { dataFetchSucceeded, dataFetchError } from './actions';
import { DATA_FETCH } from './constants';

export function* fetchData(action) {
  try {
    const opts = {
      method: 'GET',
    };
    const requestUrl = `/my-plugin/${action.endPoint}`;
    const data = yield call(request, requestUrl, opts);

    yield put(dataFetchSucceeded(data));
  } catch(error) {
    yield put(dataFetchError(error))
  }
}

// Individual exports for testing
function* defaultSaga() {
  yield fork(takeLatest, DATA_FETCH, fetchData);
}

export default defaultSaga;

Simple example

Let's say that we have a container that fetches Content Type configurations depending on URL change.

Routing declaration:

Here we want to create a route /content-type/:contentTypeName for the ContentTypePage container.

Path — ./plugins/my-plugin/admin/src/container/App/index.js.

import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';

import { createStructuredSelector } from 'reselect';
import { Switch, Route, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { pluginId } from 'app';

import ContentTypePage from 'containers/ContentTypePage';
import styles from './styles.scss';

class App extends React.Component {
  render() {
    return (
      <div className={`${pluginId} ${styles.app}`}>
        <Switch>
          <Route exact path="/plugins/my-plugin/content-type/:contentTypeName" component={ContentTypePage} />
        </Switch>
      </div>
    );
  }
}

App.contextTypes = {
  router: PropTypes.object.isRequired,
};

export function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {},
    dispatch
  );
}

const mapStateToProps = createStructuredSelector({});
const withConnect = connect(mapStateToProps, mapDispatchToProps);

export default compose(
  withConnect,
)(App);


Constants declaration:

Let's declare the needed constants to handle fetching data:

Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/constants.js.

export const DATA_FETCH = 'myPlugin/ContentTypePage/DATA_FETCH';
export const DATA_FETCH_ERROR = 'myPlugin/ContentTypePage/DATA_FETCH_ERROR';
export const DATA_FETCH_SUCCEEDED = 'myPlugin/ContentTypePage/DATA_FETCH_SUCCEEDED';

Actions declaration:

Let's declare our actions.

Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/actions.js.

import {
  DATA_FETCH,
  DATA_FETCH_ERROR,
  DATA_FETCH_SUCCEEDED,
} from './constants';

export function dataFetch(contentTypeName) {
  return {
    type: DATA_FETCH,
    contentTypeName,
  };
}

export function dataFetchError(errorMessage) {
  return {
    type: DATA_FETCH_ERROR,
    errorMessage,
  };
}

export function dataFetchSucceeded(data) {
  // data will look like { data: { name: 'User', description: 'Some description' } }
  return {
    type: DATA_FETCH_SUCCEEDED,
    data,
  };
}

Reducer setup:

Please refer to the Immutable documentation for informations about data structure.

Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/reducer.js.

import { fromJS, Map } from 'immutable';
import {
  DATA_FETCH,
  DATA_FETCH_ERROR,
  DATA_FETCH_SUCCEEDED
} from './constants';

const initialState = fromJS({
  contentTypeName,
  error: false,
  errorMessage: '',
  data: Map({}),
});

function contentTypePageReducer(state = initialState, action) {
  switch (action.type) {
    case DATA_FETCH:
      return state.set('contentTypeName', action.contentTypeName);
    case DATA_FETCH_ERROR:
      return state
        .set('error', true)
        .set('errorMessage', action.errorMessage);
    case DATA_FETCH_SUCCEEDED:
      return state
        .set('error', false)
        .set('data', Map(action.data.data));
    default:
      return state;
  }
}

export default contentTypePageReducer;

Selectors setup:

Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/selectors.js.

import { createSelector } from 'reselect';

/**
 * Direct selector to the contentTypePage state domain
 */
const selectContentTypePageDomain = () => state => state.get('contentTypePage');

/**
 * Other specific selectors
 */


/**
 * Default selector used by ContentTypePage
 */

const selectContentTypePage = () => createSelector(
  selectContentTypePageDomain(),
  (substate) => substate.toJS()
);

const makeSelectContentTypeName = () => createSelector(
  selectContentTypePageDomain(),
  (substate) => substate.get('contentTypeName');
)
export default selectContentTypePage;
export { makeSelectContentTypeName, selectContentTypePageDomain };

Handling route change:

Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/index.js.

import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { bindActionCreators, compose } from 'redux';
import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import { map } from 'lodash';

// Utils to create the container's store
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';

import { dataFetch } from './actions';
import { selectContentTypePage } from './selectors';
import saga from './sagas';
import reducer from './reducer';
import styles from './styles.scss';

export class ContentTypePage extends React.Component { // eslint-disable-line react/prefer-stateless-function
  constructor(props) {
    super(props);

    this.links = [
      {
        to: 'plugin/my-plugin/content-type/product',
        info: 'Product',
      },
      {
        to: 'plugin/my-plugin/content-type/user',
        info: 'User',
      },
    ];
  }

  componentDidMount() {
    this.props.dataFetch(this.props.match.params.contentTypeName);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.match.params.contentTypeName !== this.props.match.params.contentTypeName) {
      this.props.dataFetch(nextProps.match.params.contentTypeName);
    }
  }

  render() {
    return (
      <div className={styles.contentTypePage}>
        <div>
          <ul>
            {map(this.links, (link, key) => (
              <li key={key}>
                <NavLink to={link.to}>{link.info}</NavLink>
              </li>
            ))}
          </ul>
        </div>
        <div>
          <h1>{this.props.data.name}</h1>
          <p>{this.props.data.description}</p>
        </div>
      </div>
    );
  }
}

const mapStateToProps = selectContentTypePage();

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {
      dataFetch,
    },
    dispatch,
  );
}

ContentTypePage.propTypes = {
  data: PropTypes.object.isRequired,
  dataFetch: PropTypes.func.isRequired,
  match: PropTypes.object.isRequired,
};

const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withSaga = injectSaga({ key: 'contentTypePage', saga });
const withReducer = injectReducer({ key: 'contentTypePage', reducer });

export default compose(
  withReducer,
  withSaga,
  withConnect,
)(ContentTypePage);

Fetching data:

The sagas.js file is in charge of fetching data.

Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/sagas.js.

import { LOCATION_CHANGE } from 'react-router-redux';
import { takeLatest, call, take, put, fork, cancel, select } from 'redux-saga/effects';
import request from 'utils/request';
import {
  dataFetchError,
  dataFetchSucceeded,
} from './actions';
import { DATA_FETCH } from './constants';
import { makeSelectContentTypeName } from './selectors';

export function* fetchData() {
  try {
    const opts = { method: 'GET' };

    // To make a POST request { method: 'POST', body: {Object} }

    const endPoint = yield select(makeSelectContentTypeName());
    const requestUrl = `my-plugin/**/${endPoint}`;

    // Fetching data with our request helper
    const data = yield call(request, requestUrl, opts);
    yield put(dataFetchSucceeded(data));
  } catch(error) {
    yield put(dataFetchError(error.message));
  }
}

function* defaultSaga() {
  const loadDataWatcher = yield fork(takeLatest, DATA_FETCH, fetchData);

  yield take(LOCATION_CHANGE);
  yield cancel(loadDataWatcher);
}

export default defaultSaga;


Example with server autoReload watcher

Let's say that you want to develop a plugin that needs server restart on file change (like the settings-manager plugin) and you want to be aware of that to display some stuff..., you just have to send a third argument: true to our request helper and it will ping a dedicated route and send the response when the server has restarted.

Path — ./plugins/my-plugin/admin/src/containers/**/sagas.js.

import { takeLatest, call, take, put, fork, cancel, select } from 'redux-saga/effects';
import request from 'utils/request';
import {
  submitSucceeded,
  submitError,
} from './actions';
import { SUBMIT } from './constants';
// Other useful imports like selectors...
// ...

export function* postData() {
  try {
    const body = { data: 'someData' };
    const opts = { method: 'POST', body };
    const requestUrl = `**yourUrl**`;

    const response = yield call(request, requestUrl, opts, true);

    if (response.ok) {
      yield put(submitSucceeded());      
    } else {
      yield put(submitError('An error occurred'));
    }
  } catch(error) {
    yield put(submitError(error.message));
  }
}

function* defaultSaga() {
  yield fork(takeLatest, SUBMIT, postData);
  // ...
}

export default defaultSaga;