feature-u
Version:
Feature Based Project Organization for React
386 lines (335 loc) • 16.6 kB
JavaScript
;
exports.__esModule = true;
exports.FassetsContext = undefined;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
exports.withFassets = withFassets;
exports.fassetsProps = fassetsProps;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _createReactContext = require('create-react-context');
var _createReactContext2 = _interopRequireDefault(_createReactContext);
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);
var _lodash5 = require('lodash.isstring');
var _lodash6 = _interopRequireDefault(_lodash5);
var _mySpace = require('../util/mySpace');
var _isComponent = require('../util/isComponent');
var _isComponent2 = _interopRequireDefault(_isComponent);
var _logf = require('../util/logf');
var _logf2 = _interopRequireDefault(_logf);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return 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; } // ponyfill react context, supporting older version of react
var fassetsNotDefined = 'NO FassetsContext.Provider';
// report the React Context that is in-use
var contextImpl = _react2.default.createContext === _createReactContext2.default ? 'native (React >16.3)' : 'ponyfilled (React <16.3)';
_logf2.default.force('Context in-use: ' + contextImpl + '... React Version: ' + _react2.default.version);
// publicly exposed (in rare case when app code defines their own DOM via registerRootAppElm())
var FassetsContext = exports.FassetsContext = (0, _createReactContext2.default)(fassetsNotDefined); // specify a defaultValue we can detect ERROR conditions (need FassetsContext.Provider at root)
/**
* Promotes a "wrapped" Component (an HoC - Higher-order Component)
* that injects fasset props into a `component`, as specified by the
* `mapFassetsToProps` parameter. Please refer to the
* {{book.api.mapFassetsToPropsStruct}} for the details of what this
* mapping construct looks like. Examples can be found at
* {{book.guide.crossCom_uiComposition}}.
*
* Central to this process, a Higher-order Function (HoF) is created
* that encapsulates this "mapping knowledge". Ultimately, this
* HoF must be invoked (passing `component`), which exposes the HoC
* (the "wrapped" Component).
*
* ```js
* + withFassetsHoF(component): HoC
* ```
*
* There are two ways to use `withFassets()`:
*
* 1. By directly passing the `component` parameter, the HoC will be
* returned _(internally invoking the HoF)_. This is the most
* common use case.
*
* 2. By omitting the `component` parameter, the HoF will be
* returned. This is useful to facilitate "functional composition"
* _(in functional programming)_. In this case it is the client's
* responsibility to invoke the HoF _(either directly or
* indirectly)_ in order to expose the HoC.
*
* **SideBar**: For `withFassets()` to operate,
* `<FassetsContext.Provider>` must be rendered at the root of your
* application DOM. This is really a moot point, because
* **feature-u** automatically performs this initialization, so you
* really don't have to worry about this detail _(automating
* configuration is a Hallmark of **feature-u** - reducing boilerplate
* code)_.
*
* **Please Note** this function uses named parameters.
*
* @param {mapFassetsToPropsStruct|mapFassetsToPropsFn}
* mapFassetsToProps the structure defining the prop/fassetsKey
* mapping, from which fasset resources are injected into
* `component`. This can either be a direct structure
* ({{book.api.mapFassetsToPropsStruct}}) or a function returning the
* structure ({{book.api.mapFassetsToPropsFn}}).
*
* @param {ReactComp} [component] optionally, the React Component to
* be wrapped _(see discussion above)_.
*
* @returns {HoC|HoF} either the HoC (the "wrapped" Component) when
* `component` is supplied, otherwise the HoF _(see discussion
* above)_.
*
* **Examples**:
*
* 1. Inject fasset resources from a **static structure**
* ({{book.api.mapFassetsToPropsStruct}}), **auto wrapping** the
* MainPage Component ...
*
* ```js
* function MainPage({Logo, mainLinks, mainBodies}) {
* return (
* <div>
* <div>
* <Logo/>
* </div>
* <div>
* {mainLinks.map( ([fassetsKey, MainLink]) => <MainLink key={fassetsKey}/>)}
* </div>
* <div>
* {mainBodies.map( (MainBody, indx) => <MainBody key={indx}/>)}
* </div>
* </div>
* );
* }
*
* export default withFassets({
* component: MainPage, // NOTE: auto wrap MainPage
* mapFassetsToProps: { // NOTE: static structure (mapFassetsToPropsStruct)
* Logo: 'company.logo',
* // Logo: companyLogoResource,
*
* mainLinks: 'MainPage.*.link@withKeys',
* // mainLinks: [['MainPage.cart.link', cartLinkResource],
* // ['MainPage.search.link', searchLinkResource]],
* mainBodies: 'MainPage.*.body'
* // mainBodies: [cartBodyResource, searchBodyResource],
* }
* });
* ```
*
* 2. Inject fasset resources from a **functional directive**
* ({{book.api.mapFassetsToPropsFn}}), **returning the HoF** -
* immediately invoked ...
*
* ```js
* function MainPage({mainLinks, mainBodies}) {
* return (
* ... same as prior example
* );
* }
*
* export default withFassets({
* mapFassetsToProps(ownProps) { // NOTE: functional directive (mapFassetsToPropsFn)
* ... some conditional logic based on ownProps
* return {
* Logo: 'company.logo',
* // Logo: companyLogoResource,
*
* mainLinks: 'MainPage.*.link@withKeys',
* // mainLinks: [['MainPage.cart.link', cartLinkResource],
* // ['MainPage.search.link', searchLinkResource]],
* mainBodies: 'MainPage.*.body'
* // mainBodies: [cartBodyResource, searchBodyResource],
* };
* }
* })(MainPage); // NOTE: immediately invoke the HoF, emitting the wrapped MainPage Component
* ```
*
* @function withFassets
*/
function withFassets() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var mapFassetsToProps = _ref.mapFassetsToProps,
component = _ref.component,
unknownArgs = _objectWithoutProperties(_ref, ['mapFassetsToProps', 'component']);
// validate params
var check = _verify2.default.prefix('withFassets() parameter violation: ');
// ... mapFassetsToProps
check(mapFassetsToProps, 'mapFassetsToProps is required');
var mappingIsFunction = (0, _lodash2.default)(mapFassetsToProps);
check(mappingIsFunction || (0, _lodash4.default)(mapFassetsToProps), 'mapFassetsToProps must be a mapFassetsToPropsFn or mapFassetsToPropsStruct');
// ... component
if (component) {
check((0, _isComponent2.default)(component), 'component, when supplied, must be a React Component - to be wrapped');
}
// ... unrecognized named parameter
var unknownArgKeys = Object.keys(unknownArgs);
check(unknownArgKeys.length === 0, 'unrecognized named parameter(s): ' + unknownArgKeys);
// ... unrecognized positional parameter
check(arguments.length === 1, 'unrecognized positional parameters (only named parameters can be specified)');
// define our HoF that when invoked will expose our HoC wrapper
// ... this "second level of indirection" is required to interpret our mapFassetsToProps
function withFassetsHoF(component) {
// verify component is supplied and is a valid component
(0, _verify2.default)((0, _isComponent2.default)(component), 'You must pass a React Component to the function returned by withFassets()');
// return our HoC wrapper that injects selected fassets props
// ... this function has access to everything we need:
// - fassets (via the context consumer)
// - the app-specific mapping operation (from above)
// - the outlying properties supplied to the connected component (ownProps)
return function FassetsComponent(ownProps) {
// resolve our mapping ... either directly supplied, or by function invocation
// ex: {
// propKey fassetsKey
// =========== =================
// mainLinks: 'MainPage.*.link',
// mainBodies: 'MainPage.*.body',
// }
var fassetsToPropsMap = mappingIsFunction ? mapFassetsToProps(ownProps) : mapFassetsToProps;
// ... verify resolved mapping is an Object
check((0, _lodash4.default)(fassetsToPropsMap), 'mapFassetsToProps resolved to an invalid structure, MUST be a mapFassetsToPropsStruct');
// ... WITH string values
_mySpace.MyObj.entries(fassetsToPropsMap).forEach(function (_ref2) {
var _ref3 = _slicedToArray(_ref2, 2),
propKey = _ref3[0],
fassetsKey = _ref3[1];
check((0, _lodash6.default)(fassetsKey), 'mapFassetsToProps resolved to an invalid structure - all properties MUST reference a fassetsKey string ... at minimum ' + propKey + ' does NOT');
});
// wrap the supplied component with the context consumer (providing access to fassets)
// and inject the desired fassets props
return _react2.default.createElement(
FassetsContext.Consumer,
null,
function (fassets) {
// React Context Consumer expects single function, passing it's context value (i.e. our fassets)
// ERROR when fassets is NOT defined
(0, _verify2.default)(fassets !== fassetsNotDefined, 'withFassets() cannot be used when no <FassetsContext.Provider> is in the root DOM. ' + 'Normally feature-u auto configures this, except when NO Aspects/Features inject UI content. ' + 'In this case the app must do this in launchApp() registerRootAppElm() callback. ' + '... see: https://feature-u.js.org/cur/detail.html#react-registration');
// inject fasset resource props into the supplied component
// ... THIS IS WHAT WE ARE HERE FOR!!
var CompToWrap = component; // rename to caps to use JSX (below)
return _react2.default.createElement(CompToWrap, _extends({}, fassetsProps(fassetsToPropsMap, fassets), ownProps));
}
);
};
}
// either return the HoC "wrapped" Component or HoF
// ... depending on whether the component parameter is supplied
return component ? withFassetsHoF(component) : withFassetsHoF;
}
// helper function that translates supplied fassetsToPropsMap to fassetsProps
function fassetsProps(fassetsToPropsMap, fassets) {
// export for internal use -and- testing
return Object.assign.apply(Object, _toConsumableArray(_mySpace.MyObj.entries(fassetsToPropsMap).map(function (_ref4) {
var _ref5 = _slicedToArray(_ref4, 2),
propKey = _ref5[0],
fassetsKey = _ref5[1];
return _defineProperty({}, propKey, fassets.get(fassetsKey));
})));
}
//***
//*** Specification: mapFassetsToPropsStruct
//***
/**
* @typedef {Object} mapFassetsToPropsStruct
*
* A structure (used by {{book.api.withFassets}} and {{book.api.useFassets}}) defining a
* prop/fassetsKey mapping, from which fasset resources are injected
* into a Component. Please see {{book.guide.crossCom_uiComposition}}
* for examples.
*
* The injected Component properties will reference the fasset
* resource corresponding to the `fassetsKey`.
*
* Each `fassetsKey` is case-sensitive _(as are the defined resources)_.
*
* Matches are restricted to the actual fassetKeys registered through
* the {{book.api.fassetsAspect}} `define`/`defineUse` directives. In
* other words, the matching algorithm will **not** drill into the
* resource itself (assuming it is an object with depth).
*
* The special **dot** keyword (`'.'`) will yield the fassets object
* itself _(in the same tradition as "current directory")_. This is
* useful if you wish to inject fassets into downstream processes
* (such as redux `connect()` via it's `ownProps`).
*
* **Wildcards**
*
* {{book.guide.crossCom_wildcards}} (`*`) are supported in the
* fassetsKey, accumulating multiple resources (a resource array),
* matching the supplied pattern:
*
* - **without wildcards**, a single resource is injected
* _(`undefined` for none)_.
*
* - **with wildcards**, a resource array is injected, in order of
* feature expansion _(empty array for none)_.
*
* _Example ..._
*
* ```js
* mapFassetsToProps: {
* Logo: 'company.logo',
* // Logo: companyLogoResource,
*
* // NOTE: wildcard usage ...
* mainLinks: 'MainPage.*.link',
* // mainLinks: [cartLinkResource, searchLinkResource],
* mainBodies: 'MainPage.*.body'
* // mainBodies: [cartBodyResource, searchBodyResource],
* }
* ```
*
* **@withKeys**
*
* In some cases, you may wish to know the corresponding
* `fassetsKey` of the returned resource. This is especially true
* when multiple resources are returned _(using wildcards)_.
*
* As an example, React requires a `key` attribute for array
* injections _(the `fassetsKey` is a prime candidate for this, since
* it is guaranteed to be unique)_.
*
* To accomplish this, simply suffix the `fassetsKey` with the
* keyword: `'@withKeys'`. When this is encountered, the resource
* returned is a two-element array: `[fassetsKey, resource]`.
*
* _Example ..._
*
* ```js
* mapFassetsToProps: {
* Logo: 'company.logo',
* // Logo: companyLogoResource,
*
* mainLinks: 'MainPage.*.link@withKeys', // NOTE: @withKeys directive
* // mainLinks: [['MainPage.cart.link', cartLinkResource],
* // ['MainPage.search.link', searchLinkResource]],
* mainBodies: 'MainPage.*.body'
* // mainBodies: [cartBodyResource, searchBodyResource],
* }
* ```
*
* This topic is discussed in more detail in: {{book.guide.crossCom_reactKeys$}}.
*/
//***
//*** Specification: mapFassetsToPropsFn
//***
/**
* A function (used by {{book.api.withFassets}}) that returns a
* {{book.api.mapFassetsToPropsStruct}}, defining a prop/fassetsKey
* mapping, from which fasset resources are injected into a Component.
*
* @callback mapFassetsToPropsFn
*
* @param {obj} ownProps the outlying properties supplied to the
* connected component.
*
* @return {mapFassetsToPropsStruct} the structure defining a
* prop/fassetsKey mapping, from which fasset resources are injected
* into a Component.
*/