Skip to content

Commit

Permalink
Use react-router
Browse files Browse the repository at this point in the history
See silverstripe#5711
Refactor lib/RouteRegistry to nested react-router registration
  • Loading branch information
chillu authored and Damian Mooyman committed Jul 11, 2016
1 parent 327ddb2 commit 6d9093d
Show file tree
Hide file tree
Showing 15 changed files with 391 additions and 293 deletions.
142 changes: 82 additions & 60 deletions admin/client/src/boot/index.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,87 @@
import $ from 'jQuery';
import React from 'react';
import ReactDOM from 'react-dom';
import { combineReducers, createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import { Router as ReactRouter, useRouterHistory } from 'react-router';
import createHistory from 'history/lib/createBrowserHistory';
import Config from 'lib/Config';
import router from 'lib/Router';
import pageRouter from 'lib/Router';
import routeRegister from 'lib/RouteRegister';
import reducerRegister from 'lib/ReducerRegister';
import injector from 'lib/Injector';
import App from 'containers/App/App';
import * as configActions from 'state/config/ConfigActions';
import ConfigReducer from 'state/config/ConfigReducer';
import FormReducer from 'state/form/FormReducer';
import SchemaReducer from 'state/schema/SchemaReducer';
import RecordsReducer from 'state/records/RecordsReducer';
import CampaignReducer from 'state/campaign/CampaignReducer';
import BreadcrumbsReducer from 'state/breadcrumbs/BreadcrumbsReducer';
import TextField from 'components/TextField/TextField';
import HiddenField from 'components/HiddenField/HiddenField';
import GridField from 'components/GridField/GridField';

// Sections
// eslint-disable-next-line no-unused-vars
import CampaignAdmin from 'containers/CampaignAdmin/controller';

function getBasePath() {
const a = document.createElement('a');
a.href = document.getElementsByTagName('base')[0].href;
function initReactRouter(store) {
routeRegister.updateRootRoute({
component: App,
});
const history = useRouterHistory(createHistory)({
basename: Config.get('adminUrlBase'),
});
ReactDOM.render(
<Provider store={store}>
<ReactRouter
history={history}
routes={routeRegister.getRootRoute()}
/>
</Provider>,
document.getElementsByClassName('cms-content')[0]
);
}

let basePath = a.pathname;
function initLegacyRouter(store) {
const sections = Config.get('sections');

// No trailing slash
basePath = basePath.replace(/\/$/, '');
// Initialise routes
pageRouter.base(pageRouter.getBase());

// Mandatory leading slash
if (basePath.match(/^[^\/]/)) {
basePath = `/${basePath}`;
}
pageRouter('*', (ctx, next) => {
// eslint-disable-next-line no-param-reassign
ctx.store = store;
next();
});

// Register all top level routes.
// This can be removed when top level sections are converted to React,
// have their own JavaScript controllers, and register their own routes.
Object.keys(sections).forEach((key) => {
let route = pageRouter.resolveURLToBase(sections[key].route);
route = route.replace(/\/$/, ''); // Remove trailing slash
route = `${route}(/*?)?`; // add optional trailing slash
route = route.replace(/^\/*/, ''); // remove leading slash

return basePath;
// page.js based routing, excludes any React-powered sections
pageRouter(route, (ctx, next) => {
if (document.readyState !== 'complete' || ctx.init) {
next();
return;
}

// Load the panel then call the next route.
$('.cms-container')
.entwine('ss')
.handleStateChange(null, ctx.state);
});
});

pageRouter.start();
}

function appBoot() {
Expand All @@ -44,6 +92,10 @@ function appBoot() {
reducerRegister.add('campaign', CampaignReducer);
reducerRegister.add('breadcrumbs', BreadcrumbsReducer);

injector.register('TextField', TextField);
injector.register('HiddenField', HiddenField);
injector.register('GridField', GridField);

const initialState = {};
const rootReducer = combineReducers(reducerRegister.getAll());
const middleware = [thunkMiddleware];
Expand All @@ -58,60 +110,30 @@ function appBoot() {
// Set the initial config state.
store.dispatch(configActions.setConfig(Config.getAll()));

// Initialise routes
router.base(getBasePath());

router('*', (ctx, next) => {
// eslint-disable-next-line no-param-reassign
ctx.store = store;
next();
});

router.exit('*', (ctx, next) => {
ReactDOM.unmountComponentAtNode(document.getElementsByClassName('cms-content')[0]);
next();
});

/*
* Register all top level routes.
* This can be removed when top level sections are converted to React,
* have their own JavaScript controllers, and register their own routes.
*/
// Legacy routes will always cause a full page reload
const sections = Config.get('sections');
Object.keys(sections).forEach((key) => {
const sectionConfig = sections[key];

// Skip react routes which are handled by individual route setup
if (sectionConfig.reactRoute) {
return;
const currentPath = window.location.pathname.replace(/\/$/, '');
const matchesLegacyRoute = Object.keys(sections).find((key) => {
const section = sections[key];
const route = pageRouter.resolveURLToBase(section.route).replace(/\/$/, '');

// Skip react routes
if (section.reactRouter) {
return false;
}

let route = sectionConfig.route;
route = route.replace(/\/$/, ''); // Remove trailing slash
route = `/${route}(/*?)?`; // add optional trailing slash
routeRegister.add(route, (ctx, next) => {
if (document.readyState !== 'complete' || ctx.init) {
next();
return;
}

// Load the panel then call the next route.
$('.cms-container')
.entwine('ss')
.handleStateChange(null, ctx.state)
.done(next);
});
// Check if the beginning of the route is the same as the current location.
// Since we haven't decided on a router yet, we can't use it for route matching.
// TODO Limit full page load when transitioning from legacy to react route or vice versa
return currentPath.match(route);
});

const registeredRoutes = routeRegister.getAll();

for (const route in registeredRoutes) {
if (registeredRoutes.hasOwnProperty(route)) {
router(route, registeredRoutes[route]);
}
// Decide which router to use
if (matchesLegacyRoute) {
initLegacyRouter(store);
} else {
initReactRouter(store);
}

router.start();
}

window.onload = appBoot;
64 changes: 8 additions & 56 deletions admin/client/src/components/FormBuilder/FormBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,17 @@ import { bindActionCreators } from 'redux';
import * as formActions from 'state/form/FormActions';
import * as schemaActions from 'state/schema/SchemaActions';
import SilverStripeComponent from 'lib/SilverStripeComponent';
import FormComponent from 'components/Form/Form';
import FormActionComponent from 'components/FormAction/FormAction';
import TextField from 'components/TextField/TextField';
import HiddenField from 'components/HiddenField/HiddenField';
import GridField from 'components/GridField/GridField';
import Form from 'components/Form/Form';
import FormAction from 'components/FormAction/FormAction';
import fetch from 'isomorphic-fetch';
import deepFreeze from 'deep-freeze-strict';
import backend from 'lib/Backend';
import injector from 'lib/Injector';
import merge from 'merge';

import es6promise from 'es6-promise';
es6promise.polyfill();

// Using this to map field types to components until we implement dependency injection.
const fakeInjector = {

/**
* Components registered with the fake DI container.
*/
components: {
TextField,
GridField,
HiddenField,
},

/**
* Gets the component matching the passed component name.
* Used when a component type is provided bt the form schema.
*
* @param string componentName - The name of the component to get from the injector.
*
* @return object|null
*/
getComponentByName(componentName) {
return this.components[componentName];
},

/**
* Default data type to component mappings.
* Used as a fallback when no component type is provided in the form schema.
*
* @param string dataType - The data type provided by the form schema.
*
* @return object|null
*/
getComponentByDataType(dataType) {
switch (dataType) {
case 'Text':
return this.components.TextField;
case 'Hidden':
return this.components.HiddenField;
case 'Custom':
return this.components.GridField;
default:
return null;
}
},
};

export class FormBuilderComponent extends SilverStripeComponent {

constructor(props) {
Expand Down Expand Up @@ -285,8 +237,8 @@ export class FormBuilderComponent extends SilverStripeComponent {

return fields.map((field, i) => {
const Component = field.component !== null
? fakeInjector.getComponentByName(field.component)
: fakeInjector.getComponentByDataType(field.type);
? injector.getComponentByName(field.component)
: injector.getComponentByDataType(field.type);

if (Component === null) {
return null;
Expand Down Expand Up @@ -343,10 +295,10 @@ export class FormBuilderComponent extends SilverStripeComponent {
}

if (typeof createFn === 'function') {
return createFn(FormActionComponent, props);
return createFn(FormAction, props);
}

return <FormActionComponent key={i} {...props} />;
return <FormAction key={i} {...props} />;
});
}

Expand Down Expand Up @@ -418,7 +370,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
mapFieldsToComponents: this.mapFieldsToComponents,
};

return <FormComponent {...formProps} />;
return <Form {...formProps} />;
}
}

Expand Down
14 changes: 14 additions & 0 deletions admin/client/src/containers/App/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import SilverStripeComponent from 'lib/SilverStripeComponent';

/**
* Empty container for the moment, will eventually contain the CMS menu`
* and apply to document.body, rather than just one specific DOM element.
*/
class App extends SilverStripeComponent {
render() {
return (<div>{this.props.children}</div>);
}
}

export default App;

0 comments on commit 6d9093d

Please sign in to comment.