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