const _base = Symbol('base') const _ref = Symbol('ref') class ModuleBase{ constructor(base){ this[_base] = base this[_ref] = getRef(this) } /** * unique module id * @returns {string} */ get id(){ return this.constructor.name } /** * full module ref including all parents * @returns {string} */ get ref(){ return this[_ref] } /** * module title in navigation * @returns {string} */ get title(){ return this.id } /** * module group in navigation * @returns {string} */ get group(){ return null } /** * react component * @returns {function} */ get component() { return null } /** * router route * @return {object} */ get route(){ return getRoute(this) } /** * router path * @return {string} */ get path(){ return this.id } /** * children modules * @return {Array} */ get children(){ return [] } /** * @type {function} */ reduce //.... } The code above uses symbols to implement encapsulation.Now the declaration of the module is more familiar - it is necessary to inherit the
ModuleBase class, redefine the necessary getters, and optionally add a define to reduce , which will be the reducer. /SomeEntity /components /Master.js /children /index.js /create.js /update.js /index.js Grid component from the system kernel is used for data output; therefore, it suffices to define only the modules for these operations.Index - for filtering, pagination and links. Create and Update take the form to create and edit. const getRoute = module => { const route = { path: module.path, title: module.title, component: module.component } const children = module.children if(children) { ModuleBase.check(children) const index = children.filter(x => x.id.endsWith(INDEX)) if(index.length > 0){ // share title with parent module route.indexRoute = { component: index[0].component } } route.childRoutes = module.children .filter(x => !x.id.endsWith(INDEX)) .map(getRoute) } return route } class ModuleBase{ //.... combineReducers(){ const childrenMap = {} let children = Array.isArray(this.children) ? this.children : [] ModuleBase.check(children) const withReducers = children.filter(x => typeof(x.reduce) === 'function' || x.children.length > 0) for (let i = 0; i < withReducers.length; i++) { childrenMap[children[i].id] = children[i] } if(withReducers.length == 0){ return reducerOrDefault(this.reduce) } const reducers = {} for(let i in childrenMap){ reducers[i] = childrenMap[i].combineReducers() } const parent = this const reducer = typeof(this.reduce) === 'function' ? (state, action) => { if(!state){ state = parent.initialState } const nextState = parent.reduce(state, action) if(typeof(nextState) !== 'object'){ throw Error(parent.id + '.reduce returned wrong value. Reducers must return plain objects') } for(let i in childrenMap){ if(!nextState[i]){ nextState[i] = childrenMap[i].initialState } nextState[i] = {...reducers[i](nextState[i], action)} if(typeof(nextState[i]) !== 'object'){ throw Error(childrenMap[i].id + '.reduce returned wrong value. Reducers must return plain objects') } } return {...nextState} } : combineReducers(reducers) return reducer } This is not the most effective implementation of such a reducer. Unfortunately, even she took me quite a lot of time. I would be grateful if someone in the comments tells you how to do better.
/Update is replaced by /:id/Index omitted ( indexRoute used)Delete . Removal is done from the Index module./ , then it is advisable to make the App module and put all the others into it in order to use one approach everywhere.This will allow the App (if needed) to handle any application events and modify the state of any child module. Perhaps it is too cool for anyone, even the coolest reducer. I do not recommend overriding reduce at all for the parent module of the application. However, such a reducer can be useful for system operations.
mapDispatchToProps will mapDispatchToProps discussed below.ModuleBase is the first and integral part of the kernel. Without it, your code to the application does not pick up. ModuleBase provides the following API:CRUD should be easy to do. Add a DataGridModuleBase and FormModuleBase . Until now we have not specified which components are used in the modules.The connect (react-redux) function is essentially a container factory.
DataGridModule we need:DataGrid componentDataGridContainer containerModuleBase.connect function. It remains to receive data from the server. You can create a new class for each grid and override componentDidMount or other methods of the component life cycle. The approach, in general, is working, but has two significant drawbacks:I recommend to familiarize with the documentation of react concerning impurity. Use them with caution, otherwise you can step on a variety of rakes.
class and extends are first class objects in ES6. In other words, the const Enhanced = superclass => class extends superclass valid. This is possible thanks to the JavaScript prototype inheritance system.Preloader and ServerData impurities to the ServerData : const Preloader = Component => class extends Component { render() { const propsToCheck = subset(this.props, this.constructor.initialState) let isInitialized = true let isFetching = false for(let i in propsToCheck){ if(typeof(propsToCheck[i][IS_FETCHING]) === 'boolean'){ if(!isFetching && propsToCheck[i][IS_FETCHING]){ isFetching = true } // if something except "isFetching" presents it's initialized if(isInitialized && Object.keys(propsToCheck[i]).length === 1){ isInitialized = false } } } return isInitialized ? (<Dimmer.Dimmable dimmed={isFetching}> <Dimmer active={isFetching} inverted> <Loader /> </Dimmer> {super.render()} </Dimmer.Dimmable>) : (<Dimmer.Dimmable dimmed={true}> <Dimmer active={true} inverted> <Loader /> </Dimmer> <div style={divStyle}></div> </Dimmer.Dimmable>) } } const ServerData = superclass => class extends mix(superclass).with(Preloader) { componentDidMount() { this.props.queryFor( this.props.params, subset(this.props, this.constructor.initialState)) } isFetching: true displays the dimmer on top of the component. If, apart from isFetching , there are no properties in the object, we consider that they should come from the server and do not display the component at all (we assume it is not initialized).ServerData automatically mixes the preloader and overrides the componentDidMount .Module.connect via mapDispatchToProps . export const queryFactory = dispatch => { if(typeof (dispatch) != 'function'){ throw new Error('dispatch is not a function') } return (moduleId, url, params = undefined) => { dispatch({ type: combinePath(moduleId, GET), params }) return new Promise(resolve => { dispatch(function () { get(url, params).then(response => { const error = 'ok' in response && !response.ok const data = error ? {ok: response.ok, status: response.status} : response dispatch({ type: combinePath(moduleId, GET + (error ? FAILED : SUCCEEDED)), ...data }) resolve(data) }) }) }) } } export const queryAll = (dispatch, moduleRef, params, ...keys) => { const query = queryFactory(dispatch) if(!keys.length){ throw new Error('keys array must be not empty') } const action = combinePath(moduleRef, keys[0]) let promise = query(action, fixPath(action), params) for(let i = 1; i < keys.length; i++){ promise.then(() => { let act = combinePath(moduleRef, keys[i]) query(act, fixPath(act), params) }) } } export const queryFor = (dispatch, moduleRef, params, state) => { const keys = [] for (let i in state) { if (state[i].isFetching !== undefined) { keys.push(toUpperCamelCase(i)) } } return queryAll(dispatch, moduleRef, params, ...keys) queryFactory we create a query function, which makes a request to the server, dispatches the corresponding events in the store and returns a promise , so that we can build a query chain of the function in the queryAll , which the queryFor function, which is oriented to the presence of ServerData.fromServer = (initialState, ...keys) => { for(let i = 0; i < keys.length; i++){ initialState[keys[i]].isFetching = false } return initialState } ServerData.reducerFor = (moduleRef, initialState, next = null, method = GET) => { if(!moduleRef){ throw Error('You must provide valid module name') } if(!initialState){ throw Error('You must provide valid initialState') } const reducer = {} for (let i in initialState) { reducer[i] = hasFetching(initialState, i) ? ServerData.serverRequestReducerFactory(combinePath(moduleRef, i), initialState[i], next, method) : passThrough(initialState[i]) } if(Object.keys(reducer) < 1){ throw Error('No "isFetching" found. Cannot build reducer') } const combined = combineReducers(reducer) return combined } export default class DataGridModuleBase extends ModuleBase { constructor(base){ super(base) // Create is required due to children module this.reduce = ServerData.reducerFor(this.ref, DataGridContainer.initialState) } get component () { return this.connect(DataGridContainer) } } export default class SomeEntityGrid extends DataGridModuleBase { } //.. const _children= Symbol('children') export default class App extends ModuleBase{ constructor(base){ super(base) this[_children] = [new SomeEntityGrid(this)] } get path (){ return '/' } get component () { return AppComponent } get children(){ return this[_children] } If you have read to the end, you can implement FromModuleBase by analogy.
/core /ModuleBase.js /api.js /components /containers /modules /mixins CRUD ).components and containers folders contain frequently used components and containers, respectively.Source: https://habr.com/ru/post/327196/
All Articles