UNPKG

feature-u

Version:

Feature Based Project Organization for React

386 lines (335 loc) 16.6 kB
'use strict'; 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. */