Helpers

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

Auth

auth.js lets you get, set and delete data in either the browser's localStorage or sessionStorage.

Methods

Name Description
clear(key) Remove the data in either localStorage or sessionStorage
clearAppStorage() Remove all data from both storage
clearToken() Remove the user's jwt Token in the appropriate browser's storage
clearUserInfo() Remove the user's info from storage
get(key) Get the item in the browser's storage
getToken() Get the user's jwtToken
getUserInfo() Get the user's infos
set(value, key, isLocalStorage) Set an item in the sessionStorage. If true is passed as the 3rd parameter it sets the value in the localStorage
setToken(value, isLocalStorage) Set the user's jwtToken in the sessionStorage. If true is passed as the 2nd parameter it sets the value in the localStorage
setUserInfo(value, isLocalStorage) Set the user's info in the sessionStorage. If true is passed as the 2nd parameter it sets the value in the localStorage
import auth from 'utils/auth';

// ...
//
auth.setToken('12345', true); // This will set 1234 in the browser's localStorage associated with the key: jwtToken

Colors

This function allows to darken a color.

Usage

import { darken } from 'utils/colors';

const linkColor = darken('#f5f5f5', 1.5); // Will darken #F5F5F5 by 1.5% which gives #f2f2f2.

Get URL Query Parameters

The helpers allows to retrieve the query parameters in the URL.

Example

import getQueryParameters from 'utils/getQueryParameters';

const URL = '/create?source=users-permissions';
const source = getQueryParameters(URL, 'source');

console.log(source); // users-permissions

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;