feature-u
Version:
Feature Based Project Organization for React
341 lines (315 loc) • 14 kB
JavaScript
import React from 'react';
import React_createContext from 'create-react-context'; // ponyfill react context, supporting older version of react
import verify from '../util/verify';
import isFunction from 'lodash.isfunction';
import isPlainObject from 'lodash.isplainobject';
import isString from 'lodash.isstring';
import {MyObj} from '../util/mySpace';
import isComponent from '../util/isComponent';
import logf from '../util/logf';
const fassetsNotDefined = 'NO FassetsContext.Provider';
// report the React Context that is in-use
const contextImpl = React.createContext === React_createContext ? 'native (React >16.3)' : 'ponyfilled (React <16.3)';
logf.force(`Context in-use: ${contextImpl}... React Version: ${React.version}`);
// publicly exposed (in rare case when app code defines their own DOM via registerRootAppElm())
export const FassetsContext = React_createContext(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
*/
export function withFassets({mapFassetsToProps, component, ...unknownArgs}={}) {
// validate params
const check = verify.prefix('withFassets() parameter violation: ');
// ... mapFassetsToProps
check(mapFassetsToProps,
'mapFassetsToProps is required');
const mappingIsFunction = isFunction(mapFassetsToProps);
check(mappingIsFunction || isPlainObject(mapFassetsToProps),
'mapFassetsToProps must be a mapFassetsToPropsFn or mapFassetsToPropsStruct');
// ... component
if (component) {
check(isComponent(component),
'component, when supplied, must be a React Component - to be wrapped');
}
// ... unrecognized named parameter
const 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
verify(isComponent(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',
// }
const fassetsToPropsMap = mappingIsFunction ? mapFassetsToProps(ownProps) : mapFassetsToProps;
// ... verify resolved mapping is an Object
check(isPlainObject(fassetsToPropsMap),
'mapFassetsToProps resolved to an invalid structure, MUST be a mapFassetsToPropsStruct');
// ... WITH string values
MyObj.entries(fassetsToPropsMap).forEach( ([propKey, fassetsKey]) => {
check(isString(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 (
<FassetsContext.Consumer>
{ (fassets) => { // React Context Consumer expects single function, passing it's context value (i.e. our fassets)
// ERROR when fassets is NOT defined
verify(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!!
const CompToWrap = component; // rename to caps to use JSX (below)
return <CompToWrap {...fassetsProps(fassetsToPropsMap, fassets)} {...ownProps}/>;
}
}
</FassetsContext.Consumer>
);
};
}
// 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
export function fassetsProps(fassetsToPropsMap, fassets) { // export for internal use -and- testing
return Object.assign(...MyObj.entries(fassetsToPropsMap).map( ([propKey, fassetsKey]) => ({[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.
*/