Extending feature-u
feature-u is extendable! It operates in an open plugable architecture where Extendable Aspects integrate feature-u to other frameworks, matching your specific run-time stack. This is good, because not everyone uses the same frameworks!
You want state management using
redux
? There is an Aspect for that:feature-redux
.You want to maintain your logic using
redux-logic
? There is an Aspect for that:feature-redux-logic
.You want your features to autonomously promote their screens within the application? There is an Aspect for that:
feature-router
.Can't find an Aspect that integrates the XYZ framework? You can create your own using
createAspect()
! For extra credit, you should consider publishing your package, so other XYZ users can benefit from your work!
It is important to understand that feature-u does not alter the interface to these frameworks in any way. You use them the same way you always have. feature-u merely provides a well defined organizational layer, where the frameworks are automatically setup and configured by accumulating the necessary resources across all your features.
Locating Extensions
To locate the latest set of published feature-u extensions, simply
search the npm registry using a 'feature-u'
keyword. All
packages that extend feature-u should include this keyword in
their package.
During feature-u's initial release (3/2018), I published the three Aspects mentioned above (matching my preferred stack), so at that time there were three plugins. Assuming feature-u gains momentum, the hope is that other authors will contribute their work.
If you create your own extension and decide to publish it, don't
forget to include the 'feature-u'
keyword in your package, so
others can easily locate your extension.
Aspect Object (extending feature-u)
To extend feature-u, you merely define and promote an
Aspect
object (using createAspect()
).
The Aspect
object promotes a series of life-cycle methods
that feature-u invokes in a controlled way. This life-cycle is
controlled by launchApp()
... it is supplied the Aspects,
and it invokes their methods.
The essential characteristics of the Aspect
life-cycle is
to:
- accumulate
AspectContent
across all features - perform the desired setup and configuration
- expose the framework in some way (by injecting a component in the
root DOM, or some
Aspect Cross Communication
mechanism)
For complete details, please refer to the section on
Aspect Life Cycle Methods
.
Defining rootAppElm
In feature-u the rootAppElm
is the top-level react DOM that
represents the display of the entire application.
This is a non-changing omnipresent DOM that achieves it's dynamics
through a series of both framework components and application injected
utilities. As an example, a typical rootAppElm
may contain:
- a navigational component providing screen dynamics (through an Aspect Framework injection)
- a state promotional component making state available to subordinates (through an Aspect Framework injection)
- a notification utility (through an application Feature injection)
- a left-nav menu (through an application Feature injection)
- etc. etc. etc.
The rootAppElm
is defined through a progressive accumulation of DOM
injections, using a combination of both Aspects
and
Features
.
There are three API's involved and they all accept a curRootAppElm
parameter, and return a new rootAppElm
that includes the supplied
element, accommodating the accumulative process.
The three API's are listed here, and are executed in this order.
They include life-cycle-hooks that are defined from both
Aspects
and Features
.
Aspect.initialRootAppElm(app, curRootAppElm): rootAppElm
Feature.appWillStart({app, curRootAppElm}): rootAppElm || falsy
Aspect.injectRootAppElm(app, curRootAppElm): rootAppElm
It is important to understand that the Feature
hook is
sandwiched between the two Aspect
hooks. Without this
insight, you would most certainly wonder what the difference was
between the two Aspect hooks.
A null rootAppElm
seeds the entire process. The first hook is used
by any Aspect that wishes to inject itself before all others ... and
so on. The end result is a DOM hierarchy where the first injected
element is manifest at the bottom of the hierarchy, and the last
injection ends up on top.
To put this in perspective, let's analyze an example where we are
using two Aspects (feature-redux
, and
feature-router
), and an application Feature that manages a
left-nav menu available throughout the application.
The Aspect for
feature-router
(routeAspect
) injects a<StateRouter>
component that (by design) can have no other children. Therefore it uses the first api:routeAspect: Aspect.initialRootAppElm()
function initialRootAppElm(app, 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 return <StateRouter routes={this.routes} fallbackElm={this.config.fallbackElm$} componentWillUpdateHook={this.config.componentWillUpdateHook$} namedDependencies=/>; }
The
leftNav
application feature, injects it's Drawer/SideBar component through the second API (the only one available to Features):leftNav: Feature.appWillStart()
function appWillStart({app, curRootAppElm}) { return ( <Drawer ref={ ref => registerDrawer(ref) } content={<SideBar/>} onClose={closeSideBar}> {curRootAppElm} </Drawer> ); }
The Aspect for
feature-redux
(reducerAspect
) injects the redux<Provider>
component that must encompass all other components (i.e. be on top). Therefore it uses the third API:reducerAspect: Aspect.injectRootAppElm()
function injectRootAppElm(app, curRootAppElm) { return ( <Provider store={this.appStore}> {curRootAppElm} </Provider> ); }
The end result of this example generates the following DOM:
<Provider store={this.appStore}>
<Drawer ref={ ref => registerDrawer(ref) }
content={<SideBar/>}
onClose={closeSideBar}>
<StateRouter routes={this.routes}
fallbackElm={this.config.fallbackElm$}
componentWillUpdateHook={this.config.componentWillUpdateHook$}
namedDependencies=/>
</Drawer>
</Provider>
Aspect Cross Communication
Some Aspects will rely on an Aspect Cross Communication mechanism
to accomplish their work (not to be confused with
Cross Feature Communication
).
Aspect Cross Communication is where an Aspect requires
additional information (over and above it's
AspectContent
) either from other Aspects
or
Features
. Therefore the extending Aspect must define
(and use) additional Aspect/Feature APIs.
As an example of this, consider the feature-redux
plugin.
Because it manages redux
, it also maintains the
redux middleware
. As a result, it must provide a way for
other Aspects to inject their middleware. It accomplishes this by
exposing a new Aspect API: Aspect.getReduxMiddleware()
.
An extending Aspect that introduces a new API should do the following:
Document the API, so the external client knows how to use it.
Register the API, allowing it to pass feature-u validation. Depending on whether this is an API for a
Aspect
orFeature
, use one of the following:This registration allows the new API (i.e. the
name
parameter) to be referenced in eithercreateAspect()
orcreateFeature()
respectively.This registration should occur in the
Aspect.genesis()
life cycle method (i.e. very early) to guarantee the new API is available during feature-u validation.SideBar: feature-u keeps track of the agent that owns each extension through the owner parameter. Use any string that uniquely identifies your utility (such as the aspect's npm package name). This prevents exceptions when duplicate extension requests are made by the same owner. This can happen when multiple instances of an aspect type are supported, and also in unit testing.
Utilize the API in one of the
Aspect Life Cycle Methods
to gather the additional information (from otherAspects
orFeatures
).
Example:
As a concrete example of this, let's look at some code snippets from the aforementioned
feature-redux
plugin:Here is how the new API is documented:
Middleware Integration:
Because feature-redux manages
redux
, other Aspects can promote theirredux middleware
through feature-redux'sAspect.getReduxMiddleware()
API (anAspect Cross Communication
mechanism). As an example, thefeature-redux-logic
Aspect integrates redux-logic.
Here is the new API registration:
feature-redux/src/reducerAspect.js
/** * Register feature-redux proprietary Aspect APIs (required to pass * feature-u validation). * This must occur early in the life-cycle (i.e. this method) to * guarantee the new API is available during feature-u validation. */ function genesis() { extendAspectProperty('getReduxStore', 'feature-redux'); // Aspect.getReduxStore(): store extendAspectProperty('getReduxMiddleware', 'feature-redux'); // Aspect.getReduxMiddleware(): reduxMiddleware }
Here is the new API usage:
feature-redux-logic/src/logicAspect.js
/** * Expose our redux middleware that activates redux-logic. * * This method is consumed by the feature-redux Aspect using an * "aspect cross-communication". * * @private */ function getReduxMiddleware() { return this.logicMiddleware; }
Aspect Life Cycle Methods
The following list represents a complete compilation of all Aspect Life Cycle Methods. Simply follow the link for a thorough discussion of each:
Aspect.name
Aspect.genesis(): string
Aspect.validateFeatureContent(feature): string
Aspect.expandFeatureContent(app, feature): string
Aspect.assembleFeatureContent(app, activeFeatures): void
Aspect.assembleAspectResources(app, aspects): void
Aspect.initialRootAppElm(app, curRootAppElm): rootAppElm
Aspect.injectRootAppElm(app, curRootAppElm): rootAppElm
Aspect.config
Aspect.additionalMethods()
Notes of Interest ...
Execution Order:
The order in which these methods are presented (above) represent the same order they are executed.
Aspect State Retention:
It is not uncommon for an Aspect to use more than one of these life cycle methods to do it's work. When this happens, typically there is a need for state retention (in order to pick up in one step where it left off in another).
As an example, an Aspect may:
use
Aspect.assembleFeatureContent()
to assemble it's content across all features ... retaining the contentand then use
Aspect.injectRootAppElm()
to promote the content assembled in the prior step
This state retention can be implemented in a number of different ways, depending on your philosophy and run-time environment. For example, you could use the module context of an ES6 environment, or alternatively the Aspect object instance itself.
The latter is available (should you choose to use it) because these hooks are in fact methods of the Aspect object. In other words,
this
is bound to the Aspect object instance. As a result, you are free to usethis
for your state retention.
App Parameter:
You will notice that the
app
parameter is supplied on many of these life cycle methods. As you know theApp
object is used in promotingCross Feature Communication
.While it is most likely an anti-pattern to directly interrogate the App object within the Aspect, it is frequently required to "pass through" to downstream processes (as an opaque object). This is the reason the App object is supplied!!
As examples of this:
The
Feature.logic
aspect (feature-redux-logic
) will dependency inject (DI) the app object into theredux-logic
process.The
Feature.route
aspect (feature-router
) communicates the app in it's routing callbacks.etc. etc. etc.
Aspect.name
The Aspect.name
is used to "key" AspectContent
of this
type in the Feature
object.
For example: an Aspect.name: 'xyz'
would permit a Feature.xyz:
xyzContent
construct.
As a result, Aspect names cannot clash with built-in aspects, and they must be unique (across all aspects that are in-use).
Best Practice ...
It is a good practice to allow your Aspect name to be re-configured at run-time because they must be unique, and externally published Aspects cannot know the Aspect mix that is in-use. This can be accomplished through a defensive Aspect implementation that references feature properties by indexing the Aspect.name rather than hard coding it.
As an example, the following code snippet assumes a context of an xyz Aspect method ...
xyzAspect.someMethod()
:
...
const feature = ...;
// DO NOT HARD CODE:
... feature.xyz
// RATHER INDEX:
... feature[this.name]
This allows your clientele to reset the Aspect.name as follows:
client code initialization
:
xyzAspect.name = 'xyzFoo';
Aspect.genesis()
API: genesis(): string
genesis()
is an optional Life Cycle Hook invoked one
time, at the very beginning of the app's start up process.
This hook can perform Aspect related initialization and validation:
initialization: this is where where proprietary Aspect/Feature APIs should be registered (if any) - via
extendAspectProperty()
andextendFeatureProperty()
(please see:Aspect Cross Communication
).validation: this is where an aspect can verify it's own required configuration (if any). Some aspects require certain settings (set by the application) in self for them to operate.
RETURN: an error message string when self is in an invalid state
(falsy when valid). Because this validation occurs under the control
of launchApp()
, any message is prefixed with: 'launchApp() parameter
violation: '
.
Aspect.validateFeatureContent()
API: validateFeatureContent(feature): string
validateFeatureContent()
is a validation hook allowing
this aspect to verify it's content on the supplied feature (which is
known to contain this aspect).
RETURN: an error message string when the supplied feature contains
invalid content for this aspect (falsy when valid). Because this
validation conceptually occurs under the control of
createFeature()
, any message is prefixed with:
'createFeature() parameter violation: '
.
Aspect.expandFeatureContent()
API: expandFeatureContent(app, feature): string
expandFeatureContent()
is an optional aspect expansion
hook, defaulting to the algorithm defined by managedExpansion()
.
This method (when used) should expand self's
AspectContent
in the supplied feature (which is known to
contain this aspect and is in need of expansion), replacing that
content (within the feature).
Once expansion is complete, feature-u will perform a delayed validation of the expanded content.
The default behavior simply implements the expansion algorithm
defined by managedExpansion()
:
feature[this.name] = feature[this.name](app);
This default behavior rarely needs to change. It however provides a
hook for aspects that need to transfer additional content from the
expansion function to the expanded content. As an example, the
reducer
aspect must transfer the slice property from the expansion
function to the expanded reducer.
RETURN: an optional error message string when the supplied feature contains invalid content for this aspect (falsy when valid). This is a specialized validation of the expansion function, over-and-above what is checked in the standard validateFeatureContent() hook.
Aspect.assembleFeatureContent()
API: assembleFeatureContent(app, activeFeatures): void
assembleFeatureContent()
assembles content for this
aspect across all features, retaining needed state for subsequent ops.
This method is required because this is the primary task that is
accomplished by all aspects.
Aspect.assembleAspectResources()
API: assembleAspectResources(app, aspects): void
assembleAspectResources()
is an optional hook that
assembles resources for this aspect across all other aspects, retaining
needed state for subsequent ops. This hook is executed after all the
aspects have assembled their feature content (i.e. after
assembleFeatureContent()
).
This is an optional second-pass (so-to-speak) of Aspect data
gathering, that facilitates
Aspect Cross Communication
. It allows an
extending aspect to gather resources from other aspects, using an
additional API (ex: Aspect.getXyz()
).
As an example of this, consider feature-redux
. Because it
manages redux
, it must promote a technique by which other
Aspects can register their redux middleware. This is accomplished
through the proprietary method: Aspect.getReduxMiddleware():
middleware
.
Aspect.initialRootAppElm()
API: initialRootAppElm(app, curRootAppElm): rootAppElm
initialRootAppElm()
is an optional callback hook that
promotes some characteristic of this aspect within the rootAppElm
... the top-level react DOM that represents the display of the entire
application.
The Defining rootAppElm
section highlights when to
use initialRootAppElm()
verses
injectRootAppElm()
.
NOTE: When this hook is used, the supplied curRootAppElm MUST be included as part of this definition!
RETURN: a new react app element root (which in turn must contain the supplied curRootAppElm), or simply the supplied curRootAppElm (if no change).
Aspect.injectRootAppElm()
API: injectRootAppElm(app, curRootAppElm): rootAppElm
injectRootAppElm()
is an optional callback hook that
promotes some characteristic of this aspect within the rootAppElm
... the top-level react DOM that represents the display of the entire
application.
The Defining rootAppElm
section highlights when to
use initialRootAppElm()
verses
injectRootAppElm()
.
NOTE: When this hook is used, the supplied curRootAppElm MUST be included as part of this definition!
RETURN: a new react app element root (which in turn must contain the supplied curRootAppElm), or simply the supplied curRootAppElm (if no change).
Aspect.config
The Aspect.config
is a sub-object that can optionally be used for
any type of configuration that a specific Aspect
may
need. Configurations (if any) should be documented by the specific
Aspect
, and if required, should be validated in the
Aspect.genesis()
hook.
The config
sub-object is "open" in the sense that any content is
allowed. In other words there is no need to pre-register acceptable
properties on the config
sub-object (as there is in direct properties
of the Aspect object ... i.e. extendAspectProperty()
).
In addition to configuration, it is common for Aspects to use the
config
sub-object for hidden diagnostic purposes (hidden in the
sense that they are not documented). These settings are employed
when researching an issue, and typically alter behavior in some way or
glean additional information. As such they would only be communicated
to users on a case-by-case basis.
Aspect.additionalMethods()
Aspects may contain additional "proprietary" methods in support of
Aspect Cross Communication
... a contract
between one or more aspects. This is merely an API specified by one
Aspect
, and used by another Aspect
,
facilitated through the assembleAspectResources(app, aspects): void
hook.
As an example of this, consider feature-redux
. Because it
manages redux
, it must promote a technique by which other
Aspects can register their redux middleware. This is accomplished
through the proprietary method: Aspect.getReduxMiddleware():
middleware
.