plugin-grind-mixins
Plugin Grind Mixins is a tool for building ES6 class mixins that are fast, flexible, and free of dependency confusion. It is inspired by Grind Framework and has a handy interface for Grind projects. Note that features can be used as well in non-Grind projects.
Plugin Grind Mixins, internally, is built using mixin patterns suggested by Raganwald and justinfagnani, however it provides a much different interface.
Installation
First, add the plugin-grind-mixins
package via your preferred package manager:
npm install --save plugin-grind-mixins
Types of Mixins
plugin-grind-mixins grants access to two types of mixins: Inheritance and Merge. They can be used together, though they work quite differently.
Inheritance Mixins
Inheritance mixins are added to the inheritance chain of a class. Because they use inheritance, these mixins can use super()
(unlike Merge mixins or attributes added through Object.assign()
and the like).
Building Inheritance Mixins
Build the mixin by pointing a function at the class you want to mix in:
// class logic
Applying Inheritance Mixins
Apply Inhertance mixins at class declaration with:
Example Inheritance Mixin
In this example, Bird and Predator classes are mixed into the Heron class so that the inheritance chain becomes Heron > Predator > Bird > Animal
:
//AnimalMixins.js const Predator = { super thisisPredator = true } { return `The hunts using ` } const Bird = { return 'talons' }
//Heron.js BaseAnimal { super thisanimalName = 'heron' } const heron = heronisPredator// trueheron// The heron hunts fish using talons
Mixins are applied in order of entry, so Bird is a base class for Predator. Thus, Predator can use super()
to access functions in Bird, though not vice-versa.
Merge Mixins
Merge mixins add properties directly onto a declared class or class prototype. Note that they do not work through inheritance and, therefore, cannot use super()
.
Example that shows a number of Merge mixins (through a JSON MergeSchema):
// AnimalMixins.js const WaterTraits = { return `Swims toward the ` } { return 'Hunting Fish' } { /* code to asynchronously update model with the number of total fish eaten */ } { types } { return 'Breathing through gills' } const LandTraits = { return `Runs toward the ` } { types }
// Alligator.js static { return onPrototype: merge: WaterTraits use: 'swim' 'huntFish' LandTraits use: 'run' append: WaterTraits use: 'environment' LandTraits use: 'environment' awaitPrepend: WaterTraits use: 'eatFish' } { return types } { return `Eats fish`} const alligator = alligator// Swims toward the shorealligator// Runs toward the horizonalligator // [ 'rivers', 'shores' ]await alligator// (Alligator model total number of fish eaten is increased by 3)// Eats 3 fishalligator// TypeError: alligator.breathThroughGills is not a function
Merge Types
There are four types of merges: merge
, mergeOver
, prepend
, and append
merge
adds new functions to the target. Errors if any of the functions already exist on the target.
mergeOver
overrides functions that already exist on the target. Automatically passes in the original function as the first argument. Errors if any of the functions do not yet exist on the target.
prepend
adds 'before hooks' to functions that already exist on the target. All arguments passed into the function are passed into the before hooks. Errors if any of the functions do not yet exist on the target. Before hooks run synchronously.
append
adds 'after hooks' to functions that already exist on the target. All arguments passed into the function are passed into the after hooks. Errors if any of the functions do not yet exist on the target. After hooks run synchronously.
For prepend and append, if the hooks should run asynchronously, use the special merges awaitPrepend
and awaitAppend
. Note, the methods these hooks are applied to will now each return a promise.
Building Merge Mixins
Merge mixins are constructed using MergeSchemas
, which detail how to apply functions found in Merge Objects
to the target class/prototype.
Structuring Merge Objects
Merge Objects are simply objects with keys pointing to functions.
Example:
// AnimalTypeMixins.js const WaterTraits = // this function, intending to be mixed with mergeOver(), passes in the overridden method as its first argument { return `Swims toward the ` } { types } const LandTraits = // this function, intending to be mixed with mergeOver(), passes in the overridden method as its first argument { return `Runs toward the ` } { 'walks' } { types }
Adding Dependencies to Merge Objects
Merge Objects can specify class/instance methods or variables that their functions depend upon. An Error will throw when the mixin is applied to a class missing the dependency.
To add dependencies to a function, have its key point to an object with the function behind an action
key and the array of dependencies behind a depends
key.
Example
const WaterTraits = swim: { return `Swims toward the at speed: ` } depends: 'speed'
Note: When applying dependents to a class instance, it will only work with instance functions, not variables.
Using MergeSchemas
There are two ways to use a MergeSchema to apply a Merge Object to a class:
- Register a JSON schema - best for applying mixins across an entire class.
- Use API methods - best for a single class instance.
MergeSchema via a JSON Schema
JSON schemas apply Merge Objects in accordance with a JSON MergeSchema.
JSON schemas are registered using:
mix(class).register(SchemaName)
:
Parameters
classObject
- the class to register a JSON Schema for.SchemaName
- the name of the static variable or function that will return the JSON schema. Defaults to 'mergeMixins'.
Example Basic JSON Schema
static MergeSchema = merge: LandTraits use: 'run' 'walk' WaterTraits use: 'transitionToLand' mergeOver: LandTraits use: 'hasTail' prepend: LandTraits use: 'environment' WaterTraits use: 'environment'
Advanced JSON Schema
Additional features add control over dependencies and minimize method conflicts.
Use
In the JSON schemas, you can pick and choose which functions to use from a Merge Object using the use
field. Even if using all the functions in a Merge Object, best practice is to always explicitly name each function in the use
field for the sake of clarity.
static MergeSchema = merge: LandTraits use: 'run' 'walk'
Alias
You can use a Merge Object function but alias it to a different name.
In the use
field, put 'name as alias'
.
static MergeSchema = merge: LandTraits use: 'run as runFast' 'walk' alligator /* will work */alligator /* will fail unless run() is defined elsewhere */
Override Depends
You may want to temporarily change Merge Object dependencies as they are applied. For example, perhaps you want the dependency to point to an aliased merge function.
To add/remove/rename Merge Object dependencies, use the key overrideDepends
.
It should be structured as such:
overrideDepends: 'functionName:[newDependency1,newDependency2],functionName2[newDependency1,newDependency2]'
Example:
static MergeSchema = merge: WaterTraits use: 'swimType as stroke' 'breathingType' WaterTraits use: 'swim' overrideDepends: 'swim:[breathingType,stroke]' alligator /* this will work */
onPrototype
Instead of applying Merge Objects only to class constructors, you can apply them to prototypes. Each initialized class prototype would then have the merge attributes. To target a class's prototype, nest the relevant part of the JSON schema inside of onPrototype: { }
Example:
static mergeMixins = onPrototype: merge: WaterTraits use: 'swim' LandTraits use: 'run' merge: WaterTraits: use: 'huntFish' Alligator// Eating Fish const alligator = alligator// Swims toward the shorealligator// Runs toward the horizon
Repeating a Merge Method
On rare occasionan, you may want to apply a merge method more than once, perhaps because there is a sensitive order in which Merge Object functions must be applied.
To do this, number the merge methods by adding an incrementing number to the end of their names.
Example:
const mergeSchema = merge: LandTraits use: 'hasFeet' mergeOver: LandTraits use: 'run' 'walk' merge2: WaterTraits use: 'transitionToLand'
Imagine that the overrides run/walk depend on new method hasFeet() existing. New method transitionToLand, in turn, depends on run/walk existing.
Applying JSON Schemas Directly On Class Instances
In examples so far, schemas have been built directly in class declarations so they apply to the constructor and all prototypes. Let's say, however, you want to apply a schema on a single class instance without applying it to the entire prototype (i.e. all instances).
To do that, use useSchema()
const mergeSchema = merge: LandTraits use: 'hasFeet' mergeOver: LandTraits use: 'run' 'walk'
Parameters
instance
- the class instance to register a JSON Schema for.mergeSchema
- the JSON schema object to apply to the instance.
Example:
const MergeSchema = merge: LandTraits use: 'run' const alligator = const alligator2 = alligator// Runs toward the horizonalligator2// TypeError: alligator2.run is not a function
MergeSchema via Merge API
The Merge API is an alternate way to construct a MergeSchema using methods.
The API methods correspond to the four main merge methods (plus awaitAppend
and awaitPrepend
) and the arguments passed to the methods look much like their JSON schema counterparts.
Example Usage of the Merge API:
const alligator = alligator /* this will work */
Note: all API methods are chainable.
Advanced Merge API
Use
You should add use
clauses to specify which Merge Object functions to use. Apply this the same way as in the JSON schema (link).
Override Depends
Sometimes you want to add/remove/rename the dependencies for a function in the Mixin Object. You can do that with overrideDepends
. Apply this the same way as in the JSON schema (link).
onPrototype
The API is meant to be used directly on class instances. However, if you want to use the API on a class constructor in order to effect all prototypes use onPrototype()
.
Example:
const alligator = alligator// 'Runs toward the shore'alligator// 'Swims toward the shore'
Note that, for effecting a class constructor/all prototypes, using a JSON schema is usually easier and cleaner.
Return the Class Instance
By default, every Mixin API method returns a Mixin object. If you want to, instead, return the altered class instance, add *AndDeclare()
to the final API method.
Example:
const alligator = const alligator2 = alligatorconstructorname /* instance of Alligator */// Alligatoralligator2constructorname /* instance of Mixin */// Mixin
Grind Sugar
Plugin Grind Mixins can be used with or without Grind. Using the Grind ecosystem, however, we can add useful syntactic sugar and reduce the number of imports necessary to use mixins.
First, you'll need to add MixinProvider
to your app/Bootstrap.js
.
// app/Bootstrap.js const app = appproviders
Mix Syntax
Adding mixins inside of a class/function can now use the app.mixins.mix(classOrInstance)
function:
const alligator = appmixins
Note that app.mixins
will not work on a top level class declaration, as the provider will not yet have been run and the app
object isn't available.
Example that will fail:
// AlligatorModel.js appmixins // // This will fail. this.app is not defined.
Caching Mixins
You can cache Inheritance classes and Merge Objects so that they no longer need to be imported when you use them.
Cahcing should happen in app/Providers/MixinBuilderProvder.js
, which you'll need to add to app/Bootstrap.js
// app/Bootstrap.js const app = appprovidersappproviders
// app/Providers/MixinBuilderProvider.js { appmixins appmixins appmixins appmixins}
To cache Merge Objects, use buildMerge(name, MergeObject)
. For Inhertiance classes, use buildChain(name, InheritanceMixin)
.
Parameters
name
- the name of the cached mixin, which is how it will be referenced.MergeObject
- the Merge Object to cache.InheritanceMixin
- the Inheritance mixin to cache.
Now when using the Mixin Object or Inheritance class, you can reference it using its string name.
For a Merge Object:
const alligator = thisappmixins
Or for an Inheritance Mixin (assuming it's inside of an already declared class):
const Heron = appmixins
Advanced Cached Mixin Object
You can still use use
, alias
, and overrideDepends
in MergeSchemas that use cached Mixin Objects.
Example:
static mergeMixins = onPrototype: mergeOver: 'LandAnimal(run)' 'WaterAnimal(swim)' merge: 'LandAnimal(hunt, walk as walkSlow)' string: 'WaterAnimal(transitionToLand)' overrideDepends: 'transitionToLand:[swim,walkSlow]'
Pre-Registering JSON Schemas
Classes can be registered in the MixinBuilderProvider
so that as soon as the provider is run, the class's JSON Schema will be applied and can be used right away without further registering.
To register a class, use:
appmixins
Parameters
classObject
- the class to register a MergeSchema for.SchemaName
- the name of the static variable for class function that returns the JSON schema. Defaults to 'mergeMixins'
Example:
// app/Providers/MixinBuilderProvider.js { appmixins}
// Alligator.js static mergeMixins = onPrototype: merge: WaterTraits use: 'swim' 'huntFish' LandTraits use: 'run' { return ` and looks for fish.` } // No need to register Alligator here. Go ahead and use the mixin capabilities!const alligator = alligator// Swims toward the shore and looks for fish.alligator// Runs toward the horizon
Contributing
Contributions are always welcome! You are encouraged to open issues and merge requests.
Some of the dev environment is borrowed from Grind Framework, while the README borrows some language from the Grind Docs.
To run the tests, use npm run test
.
Release Notes
0.1.4
Inheritance
- Inheritance chains no longer require a base class.
Dev Environment
- Update travis for node 8.
- Update devDependencies.
0.1.3
MixinProvider
- Add a default priority to avoid errors when used in Grind 0.6.
- Re-cache mixins each time the provider is rerun to properly track changes to mixin code in development.