feature-router
Version:
Feature Based Navigation (using redux state)
272 lines (221 loc) • 10.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.logf = undefined;
exports.default = createRouteAspect;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _featureU = require('feature-u');
var _StateRouter = require('./StateRouter');
var _StateRouter2 = _interopRequireDefault(_StateRouter);
var _verify = require('./util/verify');
var _verify2 = _interopRequireDefault(_verify);
var _lodash = require('lodash.isfunction');
var _lodash2 = _interopRequireDefault(_lodash);
var _lodash3 = require('lodash.isplainobject');
var _lodash4 = _interopRequireDefault(_lodash3);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } // peerDependencies
// peerDependency:
// our logger (integrated/activated via feature-u)
var logf = exports.logf = _featureU.launchApp.diag.logf.newLogger('- ***feature-router*** routeAspect: ');
// NOTE: See README for complete description
function createRouteAspect() {
var namedParams = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
// ***
// *** validate parameters
// ***
var check = _verify2.default.prefix('createRouteAspect() parameter violation: ');
// ... namedParams
check((0, _lodash4.default)(namedParams), 'only named parameters may be supplied');
// descturcture our individual namedParams
// ... NOTE: We do this here (rather in the function signature) to have access
// to the overall namedParams variable - for validation purposes!
// Access via the JavaScript implicit `arguments[0]` variable is
// NOT reliable (in this context) exhibiting a number of quirks :-(
var _namedParams$name = namedParams.name,
name = _namedParams$name === undefined ? 'route' : _namedParams$name,
fallbackElm = namedParams.fallbackElm,
componentDidUpdateHook = namedParams.componentDidUpdateHook,
_namedParams$allowNoR = namedParams.allowNoRoutes,
allowNoRoutes = _namedParams$allowNoR === undefined ? false : _namedParams$allowNoR,
unknownNamedArgs = _objectWithoutProperties(namedParams, ['name', 'fallbackElm', 'componentDidUpdateHook', 'allowNoRoutes']);
// ... name (NOTE: name check takes precedence to facilitate `Aspect.name` identity in subsequent errors :-)
check(name, 'name is required');
check(typeof name === 'string', 'name must be a string'); // NOTE: didn't want to introduce lodash.isstring dependency (in the mix of everything else going on in the 1.0.0 upgrade)
// ... unrecognized positional parameter
// NOTE: when defaulting entire struct, arguments.length is 0
check(arguments.length <= 1, 'name:' + name + ' ... unrecognized positional parameters (only named parameters can be specified) ... ' + arguments.length + ' positional parameters were found');
// ... unrecognized named parameter
var unknownArgKeys = Object.keys(unknownNamedArgs);
check(unknownArgKeys.length === 0, 'name:' + name + ' ... unrecognized named parameter(s): ' + unknownArgKeys);
// ... fallbackElm
check(fallbackElm, 'name:' + name + ' ... fallbackElm is required (a reactElm)');
// AI: check fallbackElm is a reactElm
// ... componentDidUpdateHook
if (componentDidUpdateHook) {
check((0, _lodash2.default)(componentDidUpdateHook), 'name:' + name + ' ... componentDidUpdateHook must be a function (when supplied)');
}
// ... allowNoRoutes
check(allowNoRoutes === true || allowNoRoutes === false || Array.isArray(allowNoRoutes), 'name:' + name + ' ... allowNoRoutes must be a boolean -or- an array of routes');
// ***
// *** create/promote our new aspect
// ***
var routeAspect = (0, _featureU.createAspect)({
name: name,
validateFeatureContent: validateFeatureContent,
assembleFeatureContent: assembleFeatureContent,
initialRootAppElm: initialRootAppElm,
config: {
fallbackElm$: fallbackElm,
componentDidUpdateHook$: componentDidUpdateHook,
allowNoRoutes$: allowNoRoutes
}
});
return routeAspect;
}
/**
* Validate self's aspect content on supplied feature.
*
* NOTE: To better understand the context in which any returned
* validation messages are used, **feature-u** will prefix them
* with: 'createFeature() parameter violation: '
*
* @param {Feature} feature - the feature to validate, which is known
* to contain this aspect.
*
* @return {string} an error message when the supplied feature
* contains invalid content for this aspect (null when valid).
*
* @private
*/
function validateFeatureContent(feature) {
var content = feature[this.name];
var errMsg = this.name + ' (when supplied) must be a routeCB or routeCB[] emitted from featureRoute()';
if (Array.isArray(content)) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = content[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var routeCB = _step.value;
if (!isValid(routeCB)) {
return errMsg;
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
} else if (!isValid(content)) {
return errMsg;
}
return null; // valid
}
function isValid(routeCB) {
if (!(0, _lodash2.default)(routeCB)) {
return false; // must be a function
} else if (!Number.isInteger(routeCB.routePriority)) {
return false; // must be emitted from featureRoute()
} else return true; // valid
}
/**
* Accumulate all routes from our features.
*
* @param {Fassets} fassets the Fassets object used in cross-feature-communication.
*
* @param {Feature[]} activeFeatures - The set of active (enabled)
* features that comprise this application.
*
* @private
*/
function assembleFeatureContent(fassets, activeFeatures) {
var _this = this;
// accumulate all routes from our features
// ... also embellish each route with the featureName for diagnostic purposes
var hookSummary = [];
var routes = activeFeatures.reduce(function (accum, feature) {
var routeContent = feature[_this.name];
if (routeContent) {
hookSummary.push('\n Feature.name:' + feature.name + ' <-- promotes ' + _this.name + ' AspectContent');
if (Array.isArray(routeContent)) {
accum.push.apply(accum, _toConsumableArray(routeContent));
routeContent.forEach(function (route) {
return route.featureName = feature.name;
});
} else {
accum.push(routeContent);
routeContent.featureName = feature.name;
}
} else {
hookSummary.push('\n Feature.name:' + feature.name);
}
return accum;
}, []);
// report the accumulation of routes
if (routes.length > 0) {
logf('assembleFeatureContent() gathered routes from the following Features: ' + hookSummary);
}
// handle special case where NO routes were gathered from features
else {
// by default, this is an error condition (when NOT overridden by client)
if (!this.config.allowNoRoutes$) {
throw new Error('***ERROR*** feature-router found NO routes within your features ' + ('... did you forget to register Feature.' + this.name + ' aspects in your features? ') + '(please refer to the feature-router docs to see how to override this behavior).');
}
// when client override is an array, interpret it as routes
if (Array.isArray(this.config.allowNoRoutes$)) {
logf.force('WARNING: NO routes were found in your Features (i.e. Feature.' + this.name + '), ' + 'but client override (routeAspect.config.allowNoRoutes$=[{routes}];) ' + 'directed a continuation WITH specified routes.');
routes = this.config.allowNoRoutes$;
}
// otherwise, we simply disable feature-router and continue on
else {
logf.force('WARNING: NO routes were found in your Features (i.e. Feature.' + this.name + '), ' + 'but client override (routeAspect.config.allowNoRoutes$=truthy;) ' + 'directed a continuation WITHOUT feature-router.');
}
}
// retain for later use
this.routes = routes;
}
/**
* Inject our `<StateRouter>` in the `rootAppElm`.
*
* We use `initialRootAppElm()` because `<StateRouter>` does NOT
* support children (by design).
*
* @param {Fassets} fassets the Fassets object used in cross-feature-communication.
*
* @param {reactElm} curRootAppElm - the current react app element root.
*
* @return {reactElm} rootAppElm seeded with our `<StateRouter>`.
*
* @private
*/
function initialRootAppElm(fassets, curRootAppElm) {
// no-op if we have NO routes
if (this.routes.length === 0) {
// NOTE: for this condition, the appropriate logf.force() is generated (above)
return curRootAppElm;
}
// insure we don't clobber any supplied content
// ... by design, <StateRouter> doesn't support children
if (curRootAppElm) {
throw new Error('***ERROR*** Please register routeAspect (from feature-router) before other Aspects ' + 'that inject content in the rootAppElm ... <StateRouter> does NOT support children.');
}
// seed the rootAppElm with our StateRouter
logf('initialRootAppElm() introducing <StateRouter> component into rootAppElm');
return _react2.default.createElement(_StateRouter2.default, { routes: this.routes,
fallbackElm: this.config.fallbackElm$,
componentDidUpdateHook: this.config.componentDidUpdateHook$,
namedDependencies: { fassets: fassets } });
}