├── src ├── Emitter.ts ├── __tests__ │ ├── enzyme.config.ts │ ├── EventEmitter.ts │ ├── Agent.ts │ ├── uses.tsx │ ├── ComponentRegistry.ts │ ├── Molecule.ts │ └── withs.tsx ├── MoleculeContext.ts ├── mole.ts ├── defs.ts ├── index.ts ├── uses.ts ├── MoleculeWrap.tsx ├── Agent.ts ├── withs.tsx ├── EventEmitter.ts ├── ComponentRegistry.ts └── Molecule.ts ├── .npmignore ├── .gitignore ├── CHANGELOG.md ├── .travis.yml ├── tsconfig.json ├── docs ├── index.md ├── MOLECULE_TREE.md ├── CONCEPTS.md └── API.md ├── LICENSE ├── package.json ├── tslint.json └── README.md /src/Emitter.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from './EventEmitter'; 2 | 3 | export default new EventEmitter(); 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | typings 4 | tsconfig.json 5 | typings.json 6 | tslint.json 7 | dist/test 8 | yarn.lock 9 | coverage 10 | .vscode 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | coverage 4 | dist 5 | node_modules 6 | npm-debug.log 7 | package-lock.json 8 | typings 9 | yarn-error.log 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /src/__tests__/enzyme.config.ts: -------------------------------------------------------------------------------- 1 | import * as enzyme from 'enzyme'; 2 | import * as Adapter from 'enzyme-adapter-react-16'; 3 | 4 | enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/MoleculeContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // Because at the moment of writing this there is no createContext in @types/react 4 | // @ts-ignore 5 | const MoleculeContext = React.createContext(); 6 | 7 | export default MoleculeContext; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.0 2 | 3 | - Depreacted `mole()`, replaced with `molecule()` for consistency 4 | - Introduced hooks: `useStore`, `useAgent`, `useMolecule` 5 | - Improved README, and the molecule induction 6 | - BC BREAK: `Molecule` react component, no longer passes `molecule` as prop to it's first-level children 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | install: 5 | - npm install -g coveralls 6 | - npm install 7 | 8 | script: 9 | - npm test 10 | - npm run coverage 11 | - coveralls < ./coverage/lcov.info || true # ignore coveralls error 12 | 13 | # Allow Travis tests to run in containers. 14 | # sudo: false 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "lib": ["es6", "dom", "esnext"], 8 | "noImplicitAny": false, 9 | "rootDir": "./src", 10 | "outDir": "./dist", 11 | "declaration": true, 12 | "allowSyntheticDefaultImports": true, 13 | "pretty": true, 14 | "jsx": "react", 15 | "removeComments": true, 16 | "typeRoots": ["node_modules/@types"] 17 | }, 18 | 19 | "include": ["**/*.ts", "**/*.tsx", "**/*.jsx"], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # React Molecule - Documentation 2 | 3 | - [Basic Concepts](./CONCEPTS.md) - Understand the basic taxonomy and logic 4 | - [Molecule Tree](./MOLECULE_TREE.md) - Yes. It can be done. 5 | - [API](./API.md) - Now you know Molecule. Use the API as your reference. 6 | 7 | ## Feel free to contribute 8 | 9 | Our aim is to make the documentation as simple and intuitive as possible, if you feel like you have trouble with some concepts or that they are hard to digest, it's very likely that they were not properly documented, so feel free to create a Pull-Request to improve it, or create an issue and we'll take care of it! 10 | 11 | ## [Access the API](./API.md) 12 | -------------------------------------------------------------------------------- /src/mole.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Molecule from './MoleculeWrap'; 3 | import { MoleculeOptions } from './defs'; 4 | 5 | function mole( 6 | optionsResolver: (props: any) => MoleculeOptions | MoleculeOptions 7 | ) { 8 | return function(Component) { 9 | return function(props) { 10 | let options; 11 | if (typeof optionsResolver === 'function') { 12 | options = optionsResolver(props); 13 | } else { 14 | options = {}; 15 | } 16 | 17 | return React.createElement(Molecule, { 18 | ...options, 19 | children: React.createElement(Component, props), 20 | }); 21 | }; 22 | }; 23 | } 24 | 25 | export default mole; 26 | -------------------------------------------------------------------------------- /src/defs.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from './EventEmitter'; 2 | import Molecule from './Molecule'; 3 | import Agent from './Agent'; 4 | 5 | export type AgentDefinitionMap = { 6 | [key: string]: (molecule: Molecule) => Agent; 7 | }; 8 | 9 | export type ComponentMap = { 10 | [component: string]: any; 11 | }; 12 | 13 | export type MoleculeOptions = { 14 | config?: any; 15 | registry?: ComponentMap; 16 | store?: any; 17 | agents?: AgentDefinitionMap; 18 | name?: string; 19 | debug?: boolean; 20 | }; 21 | 22 | export interface IAgent { 23 | emit(key: string, value: any): void; 24 | on(event: string, handler: () => void): void; 25 | off(event: string, handler: () => void): void; 26 | molecule: Molecule; 27 | emitter: EventEmitter; 28 | preventInit: boolean; 29 | init(): void; 30 | prepare(): void; 31 | clean(): void; 32 | } 33 | 34 | export type ComponentRegistryBlendOptions = { 35 | prefix?: string; 36 | throwOnCollisions?: boolean; 37 | }; 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import MoleculeContext from "./MoleculeContext"; 2 | import Registry, { createRegistry } from "./ComponentRegistry"; 3 | import Molecule from "./MoleculeWrap"; 4 | import MoleculeModel from "./Molecule"; 5 | import { withMolecule, withAgent, WithAgent, WithMolecule } from "./withs"; 6 | import mole from "./mole"; 7 | import Agent from "./Agent"; 8 | import * as Types from "./defs"; 9 | import Emitter from "./Emitter"; 10 | import { 11 | useStore, 12 | useMolecule, 13 | useAgent, 14 | useRegistry, 15 | useConfig, 16 | useAgentConfig 17 | } from "./uses"; 18 | 19 | const molecule = mole; 20 | 21 | export { 22 | mole, // deprecated 23 | molecule, 24 | Molecule, 25 | MoleculeContext, 26 | Registry, 27 | createRegistry, 28 | Agent, 29 | Emitter, 30 | withMolecule, 31 | withAgent, 32 | WithAgent, 33 | WithMolecule, 34 | // We export this one so someone can override it 35 | MoleculeModel, 36 | Types, 37 | useStore, 38 | useMolecule, 39 | useAgent, 40 | useRegistry, 41 | useConfig, 42 | useAgentConfig 43 | }; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Theodor Diaconu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/uses.ts: -------------------------------------------------------------------------------- 1 | import MoleculeContext from "./MoleculeContext"; 2 | import { useContext } from "react"; 3 | import MoleculeModel from "./Molecule"; 4 | 5 | export function useMolecule(): MoleculeModel { 6 | return useContext(MoleculeContext); 7 | } 8 | 9 | export function useStore() { 10 | const molecule = useMolecule(); 11 | 12 | if (!molecule.store) { 13 | throw new Error(`no-store-found`); 14 | } 15 | 16 | return molecule.store; 17 | } 18 | 19 | export function useAgentStore(agentName) { 20 | const agent = useAgent(agentName); 21 | 22 | if (!agent.store) { 23 | throw new Error(`no-store-found for ${agentName}`); 24 | } 25 | 26 | return agent.store; 27 | } 28 | 29 | export function useEmitter() { 30 | return useMolecule().emitter; 31 | } 32 | 33 | export function useRegistry() { 34 | return useMolecule().registry; 35 | } 36 | 37 | export function useConfig() { 38 | return useMolecule().config; 39 | } 40 | 41 | export function useAgent(name) { 42 | const molecule = useMolecule(); 43 | 44 | return molecule.agents[name]; 45 | } 46 | 47 | export function useAgentConfig(agentName) { 48 | const agent = useAgent(agentName); 49 | 50 | return agent.config; 51 | } 52 | -------------------------------------------------------------------------------- /src/MoleculeWrap.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { MoleculeOptions } from "./defs"; 3 | import MoleculeContext from "./MoleculeContext"; 4 | import MoleculeModel from "./Molecule"; 5 | import { withMolecule } from "./withs"; 6 | 7 | function isElement(element) { 8 | return React.isValidElement(element); 9 | } 10 | 11 | function isDOMTypeElement(element) { 12 | return isElement(element) && typeof element.type === "string"; 13 | } 14 | export interface Props extends MoleculeOptions { 15 | children?: any; 16 | } 17 | 18 | class Molecule extends React.Component { 19 | molecule: MoleculeModel; 20 | 21 | constructor(props) { 22 | super(props); 23 | 24 | let parent = null; 25 | if (props.molecule instanceof MoleculeModel) { 26 | parent = props.molecule; 27 | } 28 | 29 | this.molecule = new MoleculeModel(props, parent); 30 | this.molecule.init(); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.molecule.clean(); 35 | } 36 | 37 | render() { 38 | const { children } = this.props; 39 | let results; 40 | 41 | if (typeof children === "function") { 42 | results = children(this.molecule); 43 | } else { 44 | results = children; 45 | } 46 | 47 | return ( 48 | 49 | {results} 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default withMolecule(Molecule); 56 | -------------------------------------------------------------------------------- /src/__tests__/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from '../EventEmitter'; 2 | import { describe, it } from 'mocha'; 3 | import { assert } from 'chai'; 4 | 5 | describe('EventEmitter', () => { 6 | it('Should work with on and smart event', done => { 7 | const emitter = new EventEmitter({ 8 | context: 'test', 9 | }); 10 | 11 | const Events = { 12 | DEMO: { 13 | name: 'demo', 14 | }, 15 | }; 16 | 17 | emitter.on(Events.DEMO, () => { 18 | done(); 19 | emitter.removeAllListeners(); 20 | }); 21 | 22 | emitter.emit(Events.DEMO, 'Hello!'); 23 | }); 24 | 25 | it('Shuld throw error when validation does not work', done => { 26 | const emitter = new EventEmitter({ 27 | context: 'test', 28 | debug: true, 29 | }); 30 | 31 | const Events = { 32 | DEMO: { 33 | name: 'demo', 34 | validate() { 35 | throw 'I am not allowing!'; 36 | }, 37 | }, 38 | }; 39 | 40 | try { 41 | emitter.emit(Events.DEMO); 42 | } catch (e) { 43 | assert.isString(e); 44 | done(); 45 | } 46 | }); 47 | 48 | it('Shuld work with once()', done => { 49 | const emitter = new EventEmitter({ 50 | context: 'test', 51 | debug: true, 52 | }); 53 | 54 | const Events = { 55 | DEMO: { 56 | name: 'demo', 57 | }, 58 | }; 59 | 60 | let inAlready = false; 61 | emitter.once(Events.DEMO, () => { 62 | if (!inAlready) { 63 | inAlready = true; 64 | done(); 65 | } else { 66 | done('Should not be here any more!'); 67 | } 68 | }); 69 | 70 | emitter.emit(Events.DEMO); 71 | emitter.emit(Events.DEMO); 72 | emitter.emit(Events.DEMO); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-molecule", 3 | "version": "0.2.1", 4 | "description": "React Molecule is a bridge between components", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/cult-of-coders/react-molecule.git" 9 | }, 10 | "scripts": { 11 | "compile": "tsc", 12 | "pretest": "npm run compile", 13 | "test": "npm run testonly --", 14 | "lint": "tslint --type-check --project ./tsconfig.json ./src/**/*", 15 | "watch": "tsc -w", 16 | "testonly": "mocha --reporter spec --full-trace ./dist/__tests__/*.js", 17 | "testonly-watch": "mocha --reporter spec --full-trace ./dist/__tests__/*.js --watch", 18 | "coverage": "node ./node_modules/istanbul/lib/cli.js cover _mocha -- --full-trace ./dist/__tests__/*.js", 19 | "postcoverage": "remap-istanbul --input coverage/coverage.json --type lcovonly --output coverage/lcov.info", 20 | "prepublishOnly": "npm run compile" 21 | }, 22 | "peerDependencies": { 23 | "react": "^16.8.x" 24 | }, 25 | "devDependencies": { 26 | "@types/enzyme": "^3.10.3", 27 | "@types/enzyme-adapter-react-16": "^1.0.5", 28 | "@types/react": "^16.9.5", 29 | "chai": "^4.1.2", 30 | "chai-as-promised": "^7.1.1", 31 | "enzyme": "^3.10.0", 32 | "enzyme-adapter-react-16": "^1.15.1", 33 | "istanbul": "^0.4.5", 34 | "mobx": "4.1.1", 35 | "mobx-react": "^5.4.4", 36 | "mocha": "^3.5.3", 37 | "react": "^16.10.2", 38 | "react-dom": "^16.10.2", 39 | "remap-istanbul": "^0.11.1", 40 | "tslint": "^5.20.0", 41 | "typescript": "^3.2.2" 42 | }, 43 | "typings": "dist/index.d.ts", 44 | "typescript": { 45 | "definition": "dist/index.d.ts" 46 | }, 47 | "license": "MIT", 48 | "dependencies": { 49 | "eventemitter3": "^3.1.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/__tests__/Agent.ts: -------------------------------------------------------------------------------- 1 | import Molecule from '../Molecule'; 2 | import Agent from '../Agent'; 3 | import { describe, it } from 'mocha'; 4 | import { assert } from 'chai'; 5 | 6 | describe('Agent', () => { 7 | class MyAgent extends Agent { 8 | whoami() { 9 | return this.config.iam || 'coder'; 10 | } 11 | } 12 | 13 | it('Should work directly', () => { 14 | const agent = new MyAgent({ 15 | molecule: {} as Molecule, 16 | }); 17 | 18 | assert.equal('coder', agent.whoami()); 19 | }); 20 | 21 | it('Should work with factory', () => { 22 | const factory = MyAgent.factory({ 23 | iam: 'code', 24 | }); 25 | 26 | const agent = factory({} as Molecule); 27 | 28 | assert.equal('code', agent.whoami()); 29 | }); 30 | 31 | it('Should properly get the molecule', () => { 32 | const factory = MyAgent.factory({ 33 | iam: 'code', 34 | }); 35 | 36 | const agent = factory({ 37 | name: 'dummy', 38 | } as Molecule); 39 | 40 | assert.equal('dummy', agent.molecule.name); 41 | }); 42 | 43 | it('Should be able to get the agent from the molecule through getAgent()', () => { 44 | const molecule = new Molecule({ 45 | agents: { 46 | first: MyAgent.factory(), 47 | second: MyAgent.factory(), 48 | }, 49 | }); 50 | 51 | const second = molecule.getAgent('first').getAgent('second'); 52 | 53 | assert.isObject(second); 54 | }); 55 | 56 | it('Event emissions', done => { 57 | const agent = new MyAgent({ 58 | molecule: {} as Molecule, 59 | }); 60 | 61 | const fn = () => done('Error'); 62 | 63 | agent.on('do-not-throw', fn); 64 | agent.off('do-not-throw', fn); 65 | 66 | agent.on('done', () => done()); 67 | 68 | agent.emit('do-not-throw'); 69 | agent.emit('done'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/Agent.ts: -------------------------------------------------------------------------------- 1 | import { IAgent } from './defs'; 2 | import EventEmitter from './EventEmitter'; 3 | import Molecule from './Molecule'; 4 | 5 | export type AgentConfig = { 6 | molecule: Molecule; 7 | [key: string]: any; 8 | }; 9 | 10 | class Agent implements IAgent { 11 | config: any = {}; 12 | store?: any; 13 | emitter: EventEmitter; 14 | preventInit: boolean = false; 15 | 16 | constructor(config?: AgentConfig) { 17 | Object.assign(this.config, config); 18 | 19 | let configWithoutMolecule = Object.assign({}, config); 20 | delete configWithoutMolecule.molecule; 21 | 22 | this.validate(configWithoutMolecule); 23 | 24 | this.emitter = new EventEmitter({ 25 | context: config.name, 26 | debug: this.isDebug(), 27 | }); 28 | } 29 | 30 | get molecule() { 31 | return this.config.molecule; 32 | } 33 | 34 | public on(event: any, handler: any) { 35 | this.emitter.on(event, handler); 36 | } 37 | 38 | public once(event: any, handler: any) { 39 | this.emitter.once(event, handler); 40 | } 41 | 42 | public off(event: any, handler: any) { 43 | this.emitter.off(event, handler); 44 | } 45 | 46 | public emit(event: any, ...args: any[]) { 47 | this.emitter.emit(event, ...args); 48 | } 49 | 50 | /** 51 | * @return IAgent 52 | * @param name string 53 | */ 54 | public getAgent(name): IAgent { 55 | return this.molecule.getAgent(name); 56 | } 57 | 58 | prepare(): void {} 59 | init(): void {} 60 | clean(): void {} 61 | validate(config: any): void {} 62 | 63 | isDebug() { 64 | return this.config.debug || this.molecule.debug; 65 | } 66 | 67 | static factory(config?: object): ((molecule: Molecule) => any) { 68 | const def = this; 69 | return function(molecule) { 70 | return new def({ ...config, molecule }); 71 | }; 72 | } 73 | } 74 | 75 | export default Agent; 76 | -------------------------------------------------------------------------------- /src/__tests__/uses.tsx: -------------------------------------------------------------------------------- 1 | import "./enzyme.config"; 2 | import { 3 | useMolecule, 4 | useAgent, 5 | useAgentConfig, 6 | useAgentStore, 7 | useStore, 8 | useRegistry, 9 | useEmitter, 10 | useConfig 11 | } from "../uses"; 12 | import molecule from "../mole"; 13 | import Molecule from "../MoleculeWrap"; 14 | import MoleculeModel from "../Molecule"; 15 | import Agent from "../Agent"; 16 | import { describe, it } from "mocha"; 17 | import { assert } from "chai"; 18 | import * as React from "react"; 19 | import { shallow } from "enzyme"; 20 | import EventEmitter from "../EventEmitter"; 21 | import { ComponentRegistry } from "../ComponentRegistry"; 22 | 23 | describe("uses", () => { 24 | it("useMolecule & others", () => { 25 | const CONFIG = { number: 2500 }; 26 | const checkConfig = obj => { 27 | assert.isObject(obj); 28 | assert.equal(2500, obj.number); 29 | }; 30 | 31 | function HelloDumb() { 32 | const molecule = useMolecule(); 33 | const moleculeConfig = useConfig(); 34 | const emitter = useEmitter(); 35 | const store = useStore(); 36 | const agent = useAgent("loader"); 37 | const registry = useRegistry(); 38 | const agentConfig = useAgentConfig("loader"); 39 | 40 | // const agentStore = useAgentStore("loader"); 41 | // checkConfig(agentStore); 42 | 43 | assert.isTrue(molecule instanceof MoleculeModel); 44 | assert.isTrue(agent instanceof Agent); 45 | assert.isTrue(registry instanceof ComponentRegistry); 46 | 47 | checkConfig(moleculeConfig); 48 | checkConfig(store); 49 | checkConfig(agentConfig); 50 | assert.isTrue(emitter instanceof EventEmitter); 51 | 52 | return
Yes:Woop
; 53 | } 54 | 55 | const Dummy = molecule(() => { 56 | return { 57 | config: CONFIG, 58 | store: CONFIG, 59 | agents: { 60 | loader: Agent.factory(CONFIG) 61 | } 62 | }; 63 | })(HelloDumb); 64 | 65 | const wrapper = shallow(); 66 | assert.include(wrapper.html(), "Yes:Woop"); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/withs.tsx: -------------------------------------------------------------------------------- 1 | import MoleculeContext from './MoleculeContext'; 2 | import * as React from 'react'; 3 | import Molecule from './Molecule'; 4 | 5 | function getDisplayName(WrappedComponent) { 6 | return WrappedComponent.displayName || WrappedComponent.name || 'Component'; 7 | } 8 | 9 | export interface WithMoleculeProps { 10 | molecule: Molecule; 11 | } 12 | 13 | export const withMolecule = (WrappedComponent): React.SFC => { 14 | const WithMoleculeContainer: React.SFC = function(props: any) { 15 | return ( 16 | 17 | {molecule => 18 | React.createElement(WrappedComponent, { ...props, molecule }) 19 | } 20 | 21 | ); 22 | }; 23 | 24 | WithMoleculeContainer.displayName = `WithMolecule(${getDisplayName( 25 | WrappedComponent 26 | )})`; 27 | 28 | return WithMoleculeContainer; 29 | }; 30 | 31 | export const withAgent = (agentName, asName: string = 'agent') => { 32 | return function(WrappedComponent) { 33 | const WithAgentContainer: React.SFC = function(props) { 34 | return React.createElement( 35 | withMolecule(({ molecule }) => { 36 | const newProps = Object.assign({}, props, { 37 | [asName]: molecule.agents[agentName], 38 | }); 39 | 40 | return React.createElement(WrappedComponent, { 41 | ...newProps, 42 | molecule, 43 | }); 44 | }) 45 | ); 46 | }; 47 | 48 | WithAgentContainer.displayName = `WithAgent(${getDisplayName( 49 | WrappedComponent 50 | )})`; 51 | 52 | return WithAgentContainer; 53 | }; 54 | }; 55 | 56 | /** 57 | * {({agent, molecule}) => ()} 58 | */ 59 | export function WithAgent({ children, agent }) { 60 | return React.createElement( 61 | withAgent(agent)(({ agent, molecule }) => { 62 | return children({ agent, molecule }); 63 | }) 64 | ); 65 | } 66 | 67 | /** 68 | * {molecule => ()} 69 | */ 70 | export function WithMolecule({ children }) { 71 | return React.createElement( 72 | withMolecule(({ molecule }) => { 73 | return children(molecule); 74 | }) 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | import * as BaseEventEmitter from 'eventemitter3'; 2 | 3 | export type Options = { 4 | context?: string; 5 | debug?: boolean | string; 6 | parent?: EventEmitter; 7 | }; 8 | 9 | export type ObjectEvent = { 10 | name: string; 11 | validate?: (params) => void; 12 | }; 13 | 14 | export type Event = string | symbol | ObjectEvent; 15 | 16 | export default class EventEmitter extends BaseEventEmitter { 17 | options: Options; 18 | 19 | constructor(options: Options = {}) { 20 | super(); 21 | 22 | this.options = options; 23 | } 24 | 25 | emit(event: Event, ...args: any[]): boolean { 26 | if (event && typeof event === 'object' && !!event.name) { 27 | if (event.validate) { 28 | event.validate.call(null, ...args); 29 | } 30 | 31 | event = event.name; 32 | } 33 | 34 | this.logEmit(event, ...args); 35 | 36 | const result = super.emit(event, ...args); 37 | 38 | if (this.options.parent) { 39 | this.options.parent.emit(event, ...args); 40 | } 41 | 42 | return result; 43 | } 44 | 45 | on(event: Event, handler: (...args: any[]) => void): this { 46 | if (typeof event === 'object' && !!event.name) { 47 | event = event.name; 48 | } 49 | 50 | this.logOn(event); 51 | 52 | return super.on(event, handler); 53 | } 54 | 55 | off(event: Event, handler: (...args: any[]) => void): this { 56 | if (typeof event === 'object' && !!event.name) { 57 | event = event.name; 58 | } 59 | 60 | return super.off(event, handler); 61 | } 62 | 63 | once(event: Event, handler: (...args: any[]) => void): this { 64 | if (typeof event === 'object' && !!event.name) { 65 | event = event.name; 66 | } 67 | 68 | this.logOn(event); 69 | 70 | return super.once(event, handler); 71 | } 72 | 73 | private logEmit(event: any, ...args) { 74 | let { context, debug } = this.options; 75 | 76 | if (debug) { 77 | context = context || 'anonymous'; 78 | console.log(`[${context}] Emitted ${event} with arguments: `, ...args); 79 | } 80 | } 81 | 82 | private logOn(event: any) { 83 | let { context, debug } = this.options; 84 | 85 | if (debug) { 86 | context = context || 'anonymous'; 87 | console.log(`[${context}] Started listening to "${event}" event`); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/ComponentRegistry.ts: -------------------------------------------------------------------------------- 1 | import { ComponentMap, ComponentRegistryBlendOptions } from './defs'; 2 | import React from 'react'; 3 | 4 | export class ComponentRegistry { 5 | // Index to allow const { X, Y } = registry 6 | [index: string]: any; 7 | public store = {}; 8 | public parent: ComponentRegistry; 9 | 10 | constructor(store = {}, parent = null) { 11 | this.parent = parent; 12 | 13 | this.updateStore(store); 14 | } 15 | 16 | get(componentName): any | ComponentMap { 17 | if (Array.isArray(componentName)) { 18 | const componentNames = componentName; 19 | let map = {}; 20 | componentNames.forEach(componentName => { 21 | map[componentName] = this.getSingle(componentName); 22 | }); 23 | 24 | return map; 25 | } 26 | 27 | return this.getSingle(componentName); 28 | } 29 | 30 | getSingle(componentName) { 31 | if (this.store[componentName]) { 32 | return this.store[componentName]; 33 | } 34 | 35 | let component; 36 | if (this.parent) { 37 | component = this.parent.getSingle(componentName); 38 | } 39 | 40 | return component; 41 | } 42 | 43 | blend( 44 | registry: ComponentRegistry | ComponentMap, 45 | options?: ComponentRegistryBlendOptions 46 | ) { 47 | options = options || {}; 48 | 49 | let store; 50 | if (registry instanceof ComponentRegistry) { 51 | store = registry.store; 52 | } else { 53 | store = Object.assign({}, registry); 54 | } 55 | 56 | if (options.prefix) { 57 | let newStore = {}; 58 | for (let COMPONENT in store) { 59 | newStore[options.prefix + COMPONENT] = store[COMPONENT]; 60 | } 61 | store = newStore; 62 | } 63 | 64 | if (options.throwOnCollisions) { 65 | for (let COMPONENT in store) { 66 | if (this.store[COMPONENT]) { 67 | throw new Error( 68 | `Sorry, we couldn't blend because there was a collision on: ${COMPONENT} component` 69 | ); 70 | } 71 | } 72 | } 73 | 74 | this.updateStore(store); 75 | } 76 | 77 | private updateStore(store = {}) { 78 | let newStore = {}; 79 | for (let COMPONENT in store) { 80 | if ( 81 | typeof store[COMPONENT] === 'function' && 82 | store[COMPONENT].length === 2 83 | ) { 84 | let oldReference = this.getSingle(COMPONENT); 85 | 86 | newStore[COMPONENT] = function(props) { 87 | return store[COMPONENT].call(null, props, oldReference); 88 | }; 89 | } else { 90 | newStore[COMPONENT] = store[COMPONENT]; 91 | } 92 | } 93 | 94 | Object.assign(this.store, newStore); 95 | } 96 | } 97 | 98 | /** 99 | * This factory method allows us to use: const { Tags, Posts } = Registry; 100 | * @param args 101 | */ 102 | export function createRegistry(...args): ComponentRegistry { 103 | const registry = new ComponentRegistry(...args); 104 | 105 | return new Proxy(registry, { 106 | get(obj: ComponentRegistry, prop: string) { 107 | if (prop in obj) { 108 | return obj[prop]; 109 | } else { 110 | return obj.getSingle(prop); 111 | } 112 | }, 113 | }); 114 | } 115 | 116 | export default createRegistry(); 117 | -------------------------------------------------------------------------------- /src/__tests__/ComponentRegistry.ts: -------------------------------------------------------------------------------- 1 | import { ComponentRegistry, createRegistry } from './../ComponentRegistry'; 2 | import Agent from '../Agent'; 3 | import { describe, it } from 'mocha'; 4 | import { assert } from 'chai'; 5 | 6 | describe('ComponentRegistry', () => { 7 | it('createRegistry()', () => { 8 | const registry = createRegistry(); 9 | 10 | assert.isTrue(registry instanceof ComponentRegistry); 11 | }); 12 | 13 | it('Should work be able to store and fetch', () => { 14 | const dummy = () => null; 15 | 16 | const registry = createRegistry({ 17 | Dummy1: dummy, 18 | }); 19 | 20 | const { Dummy1 } = registry; 21 | 22 | assert.equal(dummy, Dummy1); 23 | }); 24 | 25 | it('Should work be able to blend properly with object map', () => { 26 | const dummy = () => null; 27 | 28 | const registry = createRegistry({ 29 | Dummy1: dummy, 30 | }); 31 | 32 | registry.blend({ 33 | Dummy2: dummy, 34 | }); 35 | 36 | const { Dummy2 } = registry; 37 | 38 | assert.equal(dummy, Dummy2); 39 | }); 40 | 41 | it('Should work be able to blend properly with object map', () => { 42 | const dummy = () => null; 43 | 44 | const registry = createRegistry({ 45 | Dummy1: dummy, 46 | }); 47 | 48 | registry.blend( 49 | { 50 | Dummy2: dummy, 51 | }, 52 | { 53 | prefix: 'John', 54 | } 55 | ); 56 | 57 | const { JohnDummy2 } = registry; 58 | ``; 59 | assert.equal(dummy, JohnDummy2); 60 | }); 61 | 62 | it('Should work be able to blend with throwOnCollisions option', done => { 63 | const dummy = () => null; 64 | 65 | const registry = createRegistry({ 66 | Dummy1: dummy, 67 | }); 68 | 69 | try { 70 | registry.blend( 71 | { 72 | Dummy1: dummy, 73 | }, 74 | { 75 | throwOnCollisions: true, 76 | } 77 | ); 78 | } catch (e) { 79 | done(); 80 | } 81 | }); 82 | 83 | it('Should work be able to blend properly with another registry', () => { 84 | const dummy = () => null; 85 | 86 | const registry = createRegistry({ 87 | Dummy1: dummy, 88 | }); 89 | 90 | const blendable = createRegistry({ 91 | Dummy2: dummy, 92 | }); 93 | 94 | registry.blend(blendable); 95 | 96 | const { Dummy2 } = registry; 97 | 98 | assert.equal(dummy, Dummy2); 99 | }); 100 | 101 | it('Should work with parents on multiple levels', () => { 102 | const dummy = () => null; 103 | 104 | const parent1 = createRegistry({ 105 | Dummy1: dummy, 106 | }); 107 | 108 | const parent2 = createRegistry({}, parent1); 109 | 110 | const child = createRegistry( 111 | { 112 | Dummy2: dummy, 113 | }, 114 | parent2 115 | ); 116 | 117 | const { Dummy2 } = child; 118 | 119 | assert.equal(dummy, Dummy2); 120 | }); 121 | 122 | it('Should work with get properly', () => { 123 | const dummy = () => null; 124 | 125 | const parent = createRegistry({ 126 | Dummy1: dummy, 127 | }); 128 | 129 | assert.equal(dummy, parent.get('Dummy1')); 130 | 131 | const { Dummy1 } = parent.get(['Dummy1']); 132 | assert.equal(dummy, Dummy1); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "interface-name": false, 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "max-line-length": [ 22 | true, 23 | 140 24 | ], 25 | "member-access": true, 26 | "member-ordering": [ 27 | true, 28 | "public-before-private", 29 | "static-before-instance", 30 | "variables-before-functions" 31 | ], 32 | "no-any": false, 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-conditional-assignment": true, 36 | "no-consecutive-blank-lines": false, 37 | "no-console": [ 38 | true, 39 | "log", 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-construct": true, 47 | "no-debugger": true, 48 | "no-duplicate-variable": true, 49 | "no-empty": true, 50 | "no-eval": true, 51 | "no-inferrable-types": false, 52 | "no-internal-module": true, 53 | "no-null-keyword": false, 54 | "no-require-imports": false, 55 | "no-shadowed-variable": true, 56 | "no-switch-case-fall-through": true, 57 | "no-trailing-whitespace": true, 58 | "no-unused-expression": true, 59 | "no-unused-variable": true, 60 | "no-use-before-declare": true, 61 | "no-var-keyword": true, 62 | "no-var-requires": true, 63 | "object-literal-sort-keys": false, 64 | "one-line": [ 65 | true, 66 | "check-open-brace", 67 | "check-catch", 68 | "check-else", 69 | "check-finally", 70 | "check-whitespace" 71 | ], 72 | "quotemark": [ 73 | true, 74 | "single", 75 | "avoid-escape" 76 | ], 77 | "radix": true, 78 | "semicolon": [ 79 | true, 80 | "always" 81 | ], 82 | "switch-default": true, 83 | "trailing-comma": [ 84 | true, 85 | { 86 | "multiline": "always", 87 | "singleline": "never" 88 | } 89 | ], 90 | "triple-equals": [ 91 | true, 92 | "allow-null-check" 93 | ], 94 | "typedef": [ 95 | false, 96 | "call-signature", 97 | "parameter", 98 | "arrow-parameter", 99 | "property-declaration", 100 | "variable-declaration", 101 | "member-variable-declaration" 102 | ], 103 | "typedef-whitespace": [ 104 | true, 105 | { 106 | "call-signature": "nospace", 107 | "index-signature": "nospace", 108 | "parameter": "nospace", 109 | "property-declaration": "nospace", 110 | "variable-declaration": "nospace" 111 | }, 112 | { 113 | "call-signature": "space", 114 | "index-signature": "space", 115 | "parameter": "space", 116 | "property-declaration": "space", 117 | "variable-declaration": "space" 118 | } 119 | ], 120 | "variable-name": [ 121 | true, 122 | "check-format", 123 | "allow-leading-underscore", 124 | "ban-keywords", 125 | "allow-pascal-case" 126 | ], 127 | "whitespace": [ 128 | true, 129 | "check-branch", 130 | "check-decl", 131 | "check-operator", 132 | "check-separator", 133 | "check-type" 134 | ] 135 | } 136 | } -------------------------------------------------------------------------------- /docs/MOLECULE_TREE.md: -------------------------------------------------------------------------------- 1 | # Molecule Tree 2 | 3 | This concept refers to the fact that your molecules can talk with each others through events. 4 | 5 | ```jsx 6 | const App = ( 7 | 8 | 9 | 10 | ); 11 | 12 | const UserPage = ( 13 | 14 | 15 | 16 | 17 | ); 18 | ``` 19 | 20 | By default, each event sent out to `userPage` molecule gets to the parent. So it propagates bottom-up. Alternatively you can also emit an event on `root` molecule and send it down to all children: 21 | 22 | ```js 23 | molecule.deepmit(event, params); 24 | ``` 25 | 26 | How does this help us ? 27 | 28 | Well imagine you may want to store a global accessible store from all your molecules. Since all `Atoms` interract with the molecule only. You may want maybe to have a store that knows the current user and its roles. 29 | 30 | You could apply this configuration to the root molecule: 31 | 32 | ```js 33 | import { observable } from 'mobx'; 34 | 35 | { 36 | agents: { 37 | user: UserAgent.factory() 38 | }, 39 | store: observable.map({ 40 | currentUser: null, 41 | authenticating: true 42 | }) 43 | } 44 | ``` 45 | 46 | And now when UserLoader initialises we do some fetching and properly updated currentUser: 47 | 48 | ```js 49 | import { Agent } from 'react-molecule'; 50 | 51 | class UserAgent extends Agent { 52 | init() { 53 | loadMyUserSomehow().then(user => { 54 | const { store } = molecule; 55 | 56 | store.currentUser = user; 57 | store.authenticating = false; 58 | }); 59 | } 60 | 61 | hasRole(role) { 62 | const { store } = molecule; 63 | const { currentUser } = store; 64 | 65 | return currentUser.roles.includes(role); 66 | } 67 | 68 | isAdmin() { 69 | return this.hasRole('ADMIN'); 70 | } 71 | } 72 | ``` 73 | 74 | You can first have a component on top that displays a loading icon while the user is fetched: 75 | 76 | ```jsx 77 | import { observer } from 'mobx-react'; 78 | 79 | const AppMolecule = observer(({molecule}) => { 80 | const store = { molecule.store }; 81 | 82 | if (store.authenticating) { 83 | return 84 | } 85 | 86 | return 87 | }); 88 | ``` 89 | 90 | Now your components can be very smart about this. Your agent can also contain logic about interacting with what it does therefore you can have a deep component, in a deep molecule that can do this: 91 | 92 | ```jsx 93 | const UserListFilters = ({ molecule }) => { 94 | // Note the root! 95 | const userAgent = molecule.root.getAgent('user'); 96 | const isAdmin = userAgent.isAdmin(); 97 | 98 | return { 99 | /* Something based on the isAdmin condition */ 100 | }; 101 | }; 102 | ``` 103 | 104 | And ofcourse, if you would like the UserListFilters to change if `currentUser` changes (therefore the roles can change), just wrap it in an observer: `observer(UserListFilters)` 105 | 106 | As you can see, if all the logic is called through `molecule` testing your components becomes a breeze, as you can smartly mock it and expect different dispatches to it. 107 | 108 | There may also be situations where you want to dispatch a `top-down` event to all your molecules. Something like `shut down all your requests` message: 109 | 110 | ```js 111 | molecule.root.deepmit('shutdown'); 112 | ``` 113 | 114 | The possibilities of using this are endless and it is a matter of smartly working with them. 115 | 116 | ## [Back to Table of Contents](./index.md) 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Molecule 2 | 3 | [![Build Status](https://travis-ci.org/cult-of-coders/react-molecule.svg?branch=master)](https://travis-ci.org/cult-of-coders/react-molecule) 4 | [![Coverage Status](https://coveralls.io/repos/github/cult-of-coders/react-molecule/badge.svg?branch=master)](https://coveralls.io/github/cult-of-coders/react-molecule?branch=master) 5 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | 7 | Molecule has been built to allow creation of smart, hackable react libraries. Molecule is esentially a smart context object that allows you to do the following: 8 | 9 | - Handles listening, and emissions of events 10 | - Can encapsulate logic to allow easy testing and dependency injection 11 | - Enables component overriding via registry 12 | - Ability to manage a reactive store, isolated from your components 13 | 14 | An example where `react-molecule` has been efficiently used is here: https://www.npmjs.com/package/easify 15 | 16 | ## Install 17 | 18 | `npm install --save react-molecule` 19 | 20 | ```js 21 | import { molecule, useMolecule } from "react-molecule"; 22 | const Page = molecule()(PageComponent); 23 | 24 | const PageComponent = () => { 25 | const molecule = useMolecule(); 26 | // Use it 27 | }; 28 | ``` 29 | 30 | ## Example 31 | 32 | Molecule's flexibility is extreme. There are lots of way you can use it. Below we explore an example, where we have a list, and we want to refresh the list when clicking a button. 33 | 34 | ```jsx 35 | import { Agent } from "react-molecule"; 36 | 37 | // You define logic in Agents 38 | class InvoiceLoader extends Agent { 39 | // This runs when the molecule is firstly initialised 40 | init() { 41 | this.loadInvoices(); 42 | } 43 | 44 | loadInvoices() { 45 | const { store } = this.molecule; 46 | loadInvoiceQuery().then(result => { 47 | store.invoices = result; 48 | }); 49 | } 50 | } 51 | ``` 52 | 53 | ```jsx 54 | import { molecule, useStore, useAgent } from "react-molecule"; 55 | import { observable } from "mobx"; 56 | import { observer } from "mobx-react"; 57 | 58 | // This initialises the molecule by injecting agents, and a reactive store 59 | const InvoiceListPage = molecule(props => { 60 | return { 61 | agents: { 62 | // We want to have a single instance of Agent that can be configured 63 | invoiceLoader: InvoiceLoader.factory() 64 | }, 65 | store: observable({ 66 | invoices: [] 67 | }) 68 | }; 69 | })(InvoiceList); 70 | 71 | const InvoiceList = observer(() => { 72 | // We can access the molecule's store directly 73 | const { invoices } = useStore(); 74 | 75 | // We can also get access to the agents 76 | const invoiceLoader = useAgent("invoiceLoader"); 77 | return ( 78 |
    79 |
  • 80 | 81 |
  • 82 | {invoices.map(invoice => { 83 | ; 84 | })} 85 |
86 | ); 87 | }); 88 | ``` 89 | 90 | What do we gain exactly using this approach? 91 | 92 | - By isolating logic inside agents, testing `React components` logic transforms into testing `Agents` 93 | - We have a way to store reactive data, in which multiple agents can work together 94 | 95 | This is just scratching the surface, let's explore more in the documentation. 96 | 97 | ## [Documentation](./docs/index.md) 98 | 99 | [Start reading the documentation](./docs/index.md) then use the [API](./docs/API.md) for reference. 100 | 101 | ## [API](./docs/API.md) 102 | 103 | After you read the documentation you can use the API for reference: 104 | [Click here to read it](./docs/API.md) 105 | 106 | ## Support 107 | 108 | Feel free to contact us at contact@cultofcoders.com 109 | -------------------------------------------------------------------------------- /src/Molecule.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from './EventEmitter'; 2 | 3 | import { IAgent, MoleculeOptions } from './defs'; 4 | import Agent from './Agent'; 5 | import MainRegistry, { 6 | createRegistry, 7 | ComponentRegistry, 8 | } from './ComponentRegistry'; 9 | 10 | export default class Molecule { 11 | name: string = 'anonymous'; 12 | config: any = {}; 13 | debug: boolean = false; 14 | agents: { [key: string]: any } = {}; 15 | emitter: EventEmitter; 16 | store: any = {}; 17 | registry: ComponentRegistry; 18 | parent?: Molecule; 19 | children: Molecule[] = []; 20 | 21 | constructor(value: MoleculeOptions, parent?: Molecule) { 22 | const { config, agents, store, registry, name, debug } = value; 23 | 24 | this.debug = !!debug; 25 | 26 | if (parent) { 27 | this.parent = parent; 28 | parent.children.push(this); 29 | } 30 | 31 | this.emitter = new EventEmitter({ 32 | context: name, 33 | debug, 34 | parent: parent ? parent.emitter : null, 35 | }); 36 | 37 | this.registry = createRegistry({}, MainRegistry); 38 | if (value.registry) { 39 | this.registry.blend(value.registry, {}); 40 | } 41 | 42 | if (value.agents) { 43 | this.storeAgents(value.agents); 44 | } 45 | 46 | if (value.name) { 47 | this.name = value.name; 48 | } 49 | 50 | if (value.store) { 51 | this.store = value.store; 52 | } 53 | 54 | if (config) { 55 | this.config = config; 56 | } 57 | } 58 | 59 | get root() { 60 | if (!this.parent) { 61 | return this; 62 | } 63 | 64 | return this.parent.root; 65 | } 66 | 67 | init() { 68 | // This gives all agent the chance to hook into other agents before they do anything 69 | for (let agentName in this.agents) { 70 | const agent = this.agents[agentName]; 71 | agent.prepare(); 72 | } 73 | 74 | for (let agentName in this.agents) { 75 | const agent = this.agents[agentName]; 76 | if (!agent.preventInit) { 77 | agent.init(); 78 | } 79 | } 80 | } 81 | 82 | clean() { 83 | if (this.parent) { 84 | this.parent.children = this.parent.children.filter( 85 | molecule => molecule !== this 86 | ); 87 | } 88 | 89 | this.emitter.removeAllListeners(); 90 | for (let agentName in this.agents) { 91 | const agent = this.agents[agentName]; 92 | 93 | agent.emitter.removeAllListeners(); 94 | agent.clean(); 95 | } 96 | } 97 | 98 | public on(event: any, handler: any) { 99 | this.emitter.on(event, handler); 100 | } 101 | 102 | public once(event: any, handler: any) { 103 | this.emitter.once(event, handler); 104 | } 105 | 106 | public off(event: any, handler: any) { 107 | this.emitter.off(event, handler); 108 | } 109 | 110 | public emit(event: any, ...args: any[]) { 111 | this.emitter.emit(event, ...args); 112 | } 113 | 114 | public deepmit(event: any, ...args: any[]) { 115 | this.emit(event, ...args); 116 | this.children.forEach(child => { 117 | child.deepmit(event, ...args); 118 | }); 119 | } 120 | 121 | /** 122 | * @param name 123 | */ 124 | closest(name: string) { 125 | if (this.name === name) { 126 | return this; 127 | } 128 | 129 | if (this.parent) { 130 | return this.parent.closest(name); 131 | } 132 | 133 | return null; 134 | } 135 | 136 | getAgent(name): Agent { 137 | const agent = this.agents[name]; 138 | 139 | if (!agent) { 140 | throw new Error( 141 | `We could not find the agent with name: "${name}" inside this molecule.` 142 | ); 143 | } 144 | 145 | return agent; 146 | } 147 | 148 | /** 149 | * Constructing the agents 150 | * @param _agents 151 | */ 152 | private storeAgents(_agents) { 153 | let agents = {}; 154 | 155 | for (let agentName in _agents) { 156 | agents[agentName] = _agents[agentName](this); 157 | } 158 | 159 | this.agents = agents; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/__tests__/Molecule.ts: -------------------------------------------------------------------------------- 1 | import { ComponentRegistry, createRegistry } from './../ComponentRegistry'; 2 | import Molecule from '../Molecule'; 3 | import Agent from '../Agent'; 4 | import { describe, it } from 'mocha'; 5 | import { assert } from 'chai'; 6 | 7 | describe('MoleculeModel', () => { 8 | it('Should instantiate properly', () => { 9 | const dummy = () => null; 10 | 11 | const molecule = new Molecule({ 12 | name: 'dummy', 13 | store: { 14 | value: 'dummy', 15 | }, 16 | registry: { 17 | Dummy1: dummy, 18 | }, 19 | config: { 20 | value: 'dummy-config', 21 | }, 22 | }); 23 | 24 | assert.isTrue(molecule.registry instanceof ComponentRegistry); 25 | assert.equal('dummy', molecule.name); 26 | assert.equal('dummy', molecule.store.value); 27 | assert.equal(dummy, molecule.registry.get('Dummy1')); 28 | assert.equal('dummy-config', molecule.config.value); 29 | }); 30 | 31 | it('Should instantiate and clean agents properly', done => { 32 | let inInit = false; 33 | let inValidate = false; 34 | let inPrepare = false; 35 | 36 | class MyAgent extends Agent { 37 | validate(config) { 38 | inValidate = true; 39 | } 40 | prepare() { 41 | if (!inValidate) { 42 | done('Should have been in-validate first'); 43 | } 44 | 45 | inPrepare = true; 46 | } 47 | init() { 48 | if (!inPrepare) { 49 | done('Should have been in-prepare first'); 50 | } 51 | 52 | inInit = true; 53 | } 54 | clean() { 55 | assert.isTrue(inValidate); 56 | assert.isTrue(inInit); 57 | assert.isTrue(inPrepare); 58 | 59 | done(); 60 | } 61 | } 62 | 63 | const molecule = new Molecule({ 64 | agents: { 65 | dummy: MyAgent.factory(), 66 | }, 67 | }); 68 | 69 | molecule.init(); 70 | molecule.clean(); 71 | }); 72 | 73 | it('Should get the current agents and throw if not found', done => { 74 | class MyAgent extends Agent {} 75 | 76 | const molecule = new Molecule({ 77 | agents: { 78 | dummy: MyAgent.factory(), 79 | }, 80 | }); 81 | 82 | assert.isTrue(molecule.getAgent('dummy') instanceof MyAgent); 83 | 84 | try { 85 | molecule.getAgent('dummy-not-exists'); 86 | } catch (e) { 87 | done(); 88 | } 89 | }); 90 | 91 | it('Should work as a tree with children, root and closest', () => { 92 | const root = new Molecule({ 93 | name: 'i_am_root', 94 | }); 95 | 96 | const parent = new Molecule( 97 | { 98 | name: 'i_am_parent', 99 | }, 100 | root 101 | ); 102 | 103 | const child = new Molecule( 104 | { 105 | name: 'i_am_child', 106 | }, 107 | parent 108 | ); 109 | 110 | assert.isTrue(child.root === root); 111 | assert.isTrue(root.root === root); 112 | assert.isTrue(root.children.length === 1); 113 | assert.isTrue(parent.children.length === 1); 114 | assert.isTrue(child.children.length === 0); 115 | assert.isTrue(child.closest('i_am_parent') === parent); 116 | assert.isTrue(parent.closest('i_am_parent') === parent); 117 | assert.isTrue(root.closest('i_am_parent') === null); 118 | 119 | const orphan = new Molecule({ 120 | name: 'i_am_orphan', 121 | }); 122 | 123 | assert.isFalse(!!orphan.parent); 124 | 125 | child.clean(); 126 | assert.isTrue(parent.children.length === 0); 127 | }); 128 | 129 | it('Should work with deep event propagation', done => { 130 | const root = new Molecule({ 131 | name: 'i_am_root', 132 | }); 133 | 134 | const parent = new Molecule( 135 | { 136 | name: 'i_am_parent', 137 | }, 138 | root 139 | ); 140 | 141 | const child = new Molecule( 142 | { 143 | name: 'i_am_child', 144 | }, 145 | parent 146 | ); 147 | 148 | child.on('deep', () => done()); 149 | 150 | root.deepmit('deep'); 151 | }); 152 | 153 | it('Should work with deep and bottom-up event propagation', done => { 154 | const root = new Molecule({ 155 | name: 'i_am_root', 156 | }); 157 | 158 | const parent = new Molecule( 159 | { 160 | name: 'i_am_parent', 161 | }, 162 | root 163 | ); 164 | 165 | const child = new Molecule( 166 | { 167 | name: 'i_am_child', 168 | }, 169 | parent 170 | ); 171 | 172 | let inRoot = false; 173 | root.on('deep', () => (inRoot = true)); 174 | child.on('deep', () => { 175 | assert.isTrue(inRoot); 176 | done(); 177 | }); 178 | 179 | root.deepmit('deep'); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /src/__tests__/withs.tsx: -------------------------------------------------------------------------------- 1 | import "./enzyme.config"; 2 | import { withMolecule, WithMolecule, withAgent, WithAgent } from "../withs"; 3 | import molecule from "../mole"; 4 | import Registry, { 5 | ComponentRegistry, 6 | createRegistry 7 | } from "./../ComponentRegistry"; 8 | import Molecule from "../MoleculeWrap"; 9 | import MoleculeModel from "../Molecule"; 10 | import Agent from "../Agent"; 11 | import { describe, it } from "mocha"; 12 | import { assert } from "chai"; 13 | import * as React from "react"; 14 | import { shallow } from "enzyme"; 15 | import { useMolecule } from ".."; 16 | 17 | describe("withs", () => { 18 | it("molecule", () => { 19 | function HelloDumb({ value }) { 20 | const molecule = useMolecule(); 21 | 22 | return ( 23 |
24 | {molecule instanceof MoleculeModel && value == 123 25 | ? `Yes:${molecule.config.text}` 26 | : "No"} 27 |
28 | ); 29 | } 30 | 31 | const Dummy = molecule(() => { 32 | return { 33 | config: { 34 | text: "Woop" 35 | } 36 | }; 37 | })(HelloDumb); 38 | 39 | const wrapper = shallow(); 40 | assert.include(wrapper.html(), "Yes:Woop"); 41 | }); 42 | 43 | it("withMolecule", () => { 44 | function HelloDumb({ molecule, value }) { 45 | return ( 46 |
47 | {value == 123 && molecule instanceof MoleculeModel ? "Yes" : "No"} 48 |
49 | ); 50 | } 51 | 52 | const Hello = withMolecule(HelloDumb); 53 | 54 | const Dummy = () => ( 55 | 56 |
57 | 58 |
59 |
60 | ); 61 | 62 | const wrapper = shallow(); 63 | assert.include(wrapper.html(), "Yes"); 64 | }); 65 | 66 | it("", () => { 67 | function HelloDumb({ molecule }) { 68 | return
{molecule instanceof MoleculeModel ? "Yes" : "No"}
; 69 | } 70 | 71 | const Dummy = () => ( 72 | 73 |
74 | 75 | {molecule => } 76 | 77 |
78 |
79 | ); 80 | 81 | const wrapper = shallow(); 82 | assert.include(wrapper.html(), "Yes"); 83 | }); 84 | 85 | it("withAgent", () => { 86 | function HelloDumb({ agent }) { 87 | return
{agent instanceof Agent ? "Yes" : "No"}
; 88 | } 89 | 90 | const Hello = withAgent("dummy")(HelloDumb); 91 | class MyAgent extends Agent {} 92 | 93 | const Dummy = () => ( 94 | 95 | 96 | 97 | ); 98 | 99 | const wrapper = shallow(); 100 | assert.include(wrapper.html(), "Yes"); 101 | }); 102 | 103 | it("", () => { 104 | function HelloDumb({ agent }) { 105 | return
{agent instanceof Agent ? "Yes" : "No"}
; 106 | } 107 | 108 | class MyAgent extends Agent {} 109 | 110 | const Dummy = () => ( 111 | 112 | 113 | {({ agent, molecule }) => { 114 | return ; 115 | }} 116 | 117 | 118 | ); 119 | 120 | const wrapper = shallow(); 121 | assert.include(wrapper.html(), "Yes"); 122 | }); 123 | 124 | it("Molecule should be able to have access to the parent molecule", () => { 125 | const Hello: React.SFC = () => { 126 | const molecule = useMolecule(); 127 | 128 | const isOk = 129 | molecule.name === "child" && 130 | molecule.parent && 131 | molecule.parent.name === "parent"; 132 | 133 | return
{isOk ? "Yes" : "No"}
; 134 | }; 135 | 136 | const Wrapper = () => ( 137 | 138 | 139 | 140 | 141 | 142 | ); 143 | 144 | const wrapper = shallow(); 145 | assert.include(wrapper.html(), "Yes"); 146 | }); 147 | 148 | it("Molecule & parents -- should dispatch event to parent molecule", done => { 149 | const Hello: React.SFC = () => { 150 | const molecule = useMolecule(); 151 | 152 | molecule.emit("propagate"); 153 | return null; 154 | }; 155 | 156 | function agent(molecule) { 157 | return { 158 | prepare() {}, 159 | validate() {}, 160 | clean() {}, 161 | init() { 162 | molecule.on("propagate", () => done()); 163 | } 164 | }; 165 | } 166 | 167 | const Wrapper = () => ( 168 | 169 | 170 | 171 | 172 | 173 | ); 174 | 175 | const wrapper = shallow(); 176 | wrapper.html(); 177 | }); 178 | 179 | it("Should work with enveloping the component", () => { 180 | const Item = ({ value }) => (value ? "Yes" : "No"); 181 | Registry.blend({ 182 | Item 183 | }); 184 | 185 | const EvenelopeItem = (props, Item) => { 186 | return ; 187 | }; 188 | 189 | Registry.blend({ 190 | Item: EvenelopeItem 191 | }); 192 | 193 | const Wrapper = () => ( 194 | 195 | {molecule => { 196 | const { Item } = molecule.registry; 197 | 198 | return ; 199 | }} 200 | 201 | ); 202 | 203 | const wrapper = shallow(); 204 | assert.include(wrapper.html(), "Yes"); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /docs/CONCEPTS.md: -------------------------------------------------------------------------------- 1 | # React Molecule - Concepts 2 | 3 | We have several concepts we need to explore: 4 | 5 | - Molecule 6 | - Agent 7 | - Registry 8 | 9 | ## Molecule 10 | 11 | The `molecule` is a smart object that is acts as a logic encapsulator (with agents), a communication channel (with events) and a single source of truth. 12 | 13 | ```jsx 14 | import { molecule, useMolecule } from "react-molecule"; 15 | 16 | const UserPage = molecule()(() => ); 17 | 18 | const UserList = () => { 19 | const molecule = useMolecule(); 20 | // getting access to that smart object 21 | }; 22 | ``` 23 | 24 | By default molecule contains an `EventEmitter` (npm: [eventemitter3](https://github.com/primus/eventemitter3)) and we can attach and emit events on it directly: 25 | 26 | ```js 27 | molecule.emit("searching", value); 28 | molecule.on("searching", value => {}); 29 | ``` 30 | 31 | ## Agent 32 | 33 | Agents are the way to interact with the outside world. (making API calls, modifying client-state, processing molecule events, etc) 34 | 35 | For example, you have a function that loads data from a REST API, let's put it inside an Agent. 36 | 37 | ```js 38 | import { Agent } from "react-molecule"; 39 | 40 | class UserLoader extends Agent { 41 | loadUsers() { 42 | return new Promise((resolve, reject) => { 43 | fetch("https://jsonplaceholder.typicode.com/users") 44 | .then(response => response.json()) 45 | .then(users => resolve(users)); 46 | }); 47 | } 48 | } 49 | ``` 50 | 51 | An agent needs to extend the `Agent` class so a lot of magic can be done, and essentially what we need to pass to the `Molecule` as `agents` is a map pointing to a factory function that takes in the `molecule` instance as argument. 52 | 53 | ```jsx 54 | const UserPage = molecule(() => { 55 | agents: { 56 | loader: UserLoader.factory() 57 | } 58 | }) => ); 59 | ``` 60 | 61 | Now you can safely call the agent inside `UserList`, and expect a response. 62 | 63 | ```jsx 64 | const UserList = () => { 65 | const loader = useAgent("loader"); 66 | const [users, setUsers] = useState([]); 67 | 68 | // this runs only once when the component is first mounted 69 | useEffect(() => { 70 | // This example show how we use Agents as simple units of logic 71 | // We don't really see here how they are tied with the molecule 72 | loader.loadUsers().then(users => setUsers); 73 | }); 74 | 75 | return "..."; 76 | }; 77 | ``` 78 | 79 | Let's see how agents can interact inside molecule context: 80 | 81 | ```js 82 | import { Agent } from "react-molecule"; 83 | import { observable } from "mobx"; 84 | 85 | class UserLoader extends Agent { 86 | static events = { 87 | loaded: "userLoader::loaded" 88 | search: "userLoader::search" 89 | }; 90 | 91 | store = observable({ 92 | users: [] 93 | }); 94 | 95 | init() { 96 | const { molecule } = this; 97 | 98 | this.loadUsers(); 99 | 100 | // Note: this event is at agent level, not at the whole molecule level 101 | this.on(UserLoader.events.search, search => { 102 | this.loadUsers(search); 103 | }); 104 | } 105 | 106 | loadUsers(search) { 107 | return fetch(`https://jsonplaceholder.typicode.com/users?q=${search}`) 108 | .then(response => response.json()) 109 | .then(users => { 110 | this.store.users = users; 111 | 112 | // Other agents, or components can listen to this event, and act upon it 113 | molecule.emit(UserLoader.events.loaded, users); 114 | }); 115 | } 116 | } 117 | ``` 118 | 119 | So because I have an `observable`, I can easily wrap an observer that would render the users from the agent: 120 | 121 | ```jsx 122 | import { useAgentStore, useEmitter, useAgent } from "react-molecule"; 123 | import { observer } from "mobx-react"; 124 | 125 | const UserList = observer(() => { 126 | const { users } = useAgentStore("loader"); 127 | const agent = useAgent(); 128 | return ( 129 | <> 130 | agent.emit(UserLoader.events.search, e.target.value)}> 131 | {users.map(u => '...')} 132 | 133 | ); 134 | }); 135 | ``` 136 | 137 | This means, that my children or other agents, can listen to this `molecule-level event` and react to it accordingly. 138 | 139 | Agent also support configuration when passing it to the molecule constructor options: 140 | 141 | ```jsx 142 | const UserPage = molecule( 143 | () => ({ 144 | agents: { 145 | loader: RESTAPILoader.factory({ 146 | endpoint: "http://rest.me/users" 147 | // And inside `RESTAPILoader` you can access this via `this.config.endpoint` inside your agent's functions. 148 | }) 149 | } 150 | }), 151 | () => 152 | ); 153 | ``` 154 | 155 | ## Agent Lifecycle 156 | 157 | When the molecule comes alive it instantiates the agents and injects `molecule` into them. 158 | 159 | Agent has two methods `prepare()` and `init()`, first the molecule runs `prepare()` on all agents, 160 | then runs `init()` on all agents. Conceptually the reason for both is that sometimes other agents may want to change 161 | the behavior of other agents before they do initialisation. 162 | 163 | So we can regard `prepare()` phase as a phase where we hook between agents. 164 | When the component gets unmounted `clean()` method is called on all agents. 165 | 166 | Summary: 167 | 168 | - validate(config) (You can do validation on the configuration of the Agent) 169 | - prepare() (Hook into other agents) 170 | - init() (Do your thing) 171 | - clean() (Don't leave anything hanging) 172 | 173 | ## Configurability 174 | 175 | When you instantiate an agent or a molecule with a specific config, then that config must not change in the life-span of the molecule or the agents. 176 | 177 | Molecules and agents can be configured so their behavior can be customised, or the children can render/act differently depending on these configs, example: 178 | 179 | ```jsx 180 | import { molecule, useAgentConfig, useConfig } from "react-molecule"; 181 | 182 | const MyPage = () => { 183 | const { multiply } = useAgentConfig("calculator"); // agent-level config 184 | const { listType } = useConfig(); // molecule-level config 185 | 186 | // Act differently based on these configs 187 | }; 188 | 189 | export default molecule(props => ({ 190 | config: { 191 | listType: "simple" 192 | }, 193 | agents: { 194 | calculator: CalculatorAgent.factory({ 195 | multiply: 0.9 196 | }) 197 | } 198 | }))(MyPage); 199 | ``` 200 | 201 | ## Registry 202 | 203 | Let's say you want to build a sort of mini library for your nice application, and you would like to allow overriding of certain components within your mini-lib, here's how you can do it via a `Global Registry`: 204 | 205 | ```jsx 206 | import { Registry, useRegistry } from "react-molecule"; 207 | import { CustomUserList } from "./components"; 208 | 209 | Registry.blend({ 210 | UserList: CustomUserList 211 | }); 212 | 213 | const UserPage = () => { 214 | const { UserList } = useRegistry(); 215 | 216 | return ; 217 | }; 218 | ``` 219 | 220 | Pretty straight forward, right? There are of course, many subtle and interesting aspects of the `Registry` however, they will be explored later on. 221 | 222 | Now the interesting aspect of Registry is that it can be at the molecule level, and other molecules can override it: 223 | 224 | ```jsx 225 | import { Registry, useRegistry } from "react-molecule"; 226 | import { CustomUserList, MoleculeCustomUserList } from "./components"; 227 | 228 | Registry.blend({ 229 | UserList: CustomUserList 230 | }); 231 | 232 | const UserPage = molecule( 233 | () => ({ 234 | registry: { 235 | UserList: MoleculeCustomUserList 236 | } 237 | }), 238 | () => { 239 | const { UserList } = useRegistry(); 240 | 241 | // It'll actually be MoleculeCustomUserList 242 | return ; 243 | } 244 | ); 245 | ``` 246 | 247 | ## Inter-communication between agents 248 | 249 | Now things do start to get interesting. The reason why we decoupled the logic from the molecule itself through agents, is that agents can be coded in such a way that they allow their behavior to be manipulated by other agents. 250 | 251 | Let's think about it this way, we have a data-loader agent (loads data from a REST api), and we have a search-agent (modifies the request in such a way that it permits searching) 252 | 253 | In order to implement two different agents one for loading and one for searching, they need to be able to: 254 | 255 | - `SearchAgent` may modify the request of `DataLoader` 256 | - `SearchAgent` may trigger a data reload of `DataLoader` 257 | 258 | Let's see how this manipulation can be done through events. 259 | 260 | ```js 261 | class DataLoaderAgent extends Agent { 262 | static events = { 263 | preLoad: "preLoad" 264 | }; 265 | 266 | store = observable({ 267 | results: [] 268 | }); 269 | 270 | init() { 271 | this.load(); 272 | } 273 | 274 | load() { 275 | const { endpoint } = this.config; 276 | let payload = { endpoint }; 277 | 278 | // Here we emit an event that can be manipulated by it's listeners. 279 | this.emit(DataLoaderAgent.events.preLoad, payload); 280 | 281 | fetch(payload.endpoint) 282 | .then(r => r.json()) 283 | .then(results => { 284 | this.store.results = results; 285 | }); 286 | } 287 | } 288 | ``` 289 | 290 | By emitting that event, we can have other agents listen to that event and modify it. 291 | 292 | ```js 293 | class SearchAgent extends Agent { 294 | static events = { 295 | search: "search" 296 | }; 297 | 298 | prepare() { 299 | // Note that we can pass the agent's name in SearchAgent's factory 300 | // If you use consistent name, you can just rely on just 'agent' or 'loader' 301 | 302 | // Keep in mind you're in molecule territory here 303 | this.loader = this.getAgent(this.config.agent); 304 | 305 | // We hook in here to make absolutely sure that the loader doesn't do initial loading at init() 306 | this.loader.on(DataLoaderAgent.events.preLoad, payload => { 307 | // we can manipulate it here every time 308 | payload.endpoint = payload.endpoint + `?q=${this.store.currentSearch}`; 309 | }); 310 | } 311 | 312 | search(value) { 313 | // Agent-level listener: 314 | this.store.currentSearch = search; 315 | this.loader.load(); 316 | } 317 | } 318 | ``` 319 | 320 | Implementing the molecule: 321 | 322 | ```js 323 | const UserPageMolecule = molecule( 324 | () => ({ 325 | agents: { 326 | search: SearchAgent.factory({ agent: "loader" }), 327 | loader: DataLoaderAgent.factory({ 328 | endpoint: "https://jsonplaceholder.typicode.com/users" 329 | }) 330 | } 331 | }), 332 | UserPage 333 | ); 334 | ``` 335 | 336 | ```jsx 337 | const UserPage = () => { 338 | const searchAgent = useAgent("search"); 339 | 340 | return ( 341 | <> 342 | searchAgent.search(e.target.value)} /> 343 | 344 | 345 | ); 346 | }; 347 | 348 | const UserList = () => { 349 | const { results } = useAgentStore("loader"); 350 | // render the results 351 | }; 352 | ``` 353 | 354 | You could say, hey, I can implement search in my data-loader agent, and that wouldn't be a problem. However, imagine 355 | a data-grid, where you have: search, pagination, per-page, filters, sorting. Plus, the fact that you allow this hackability 356 | is a big step forward. 357 | 358 | ## Debugging 359 | 360 | Sometimes, events can be very hard to debug, this is why molecule has a special variable `debug`, while enabled it will console.log all the event activity, you can also use it in the agent to log information relevant to your agent functionality: 361 | 362 | ```jsx 363 | // inside Agent's methods: 364 | if (this.isDebug()) { 365 | // console.log something 366 | } 367 | ``` 368 | 369 | ```js 370 | molecule( 371 | () => ({ 372 | debug: true, 373 | agents: { 374 | loader: DataLoader.factory() 375 | } 376 | }), 377 | Page 378 | ); 379 | ``` 380 | 381 | ## Events Clean-up 382 | 383 | You do not have to worry about `deregistering` your event listeners, they are automatically cleaned, at `Agent` and `Molecule` level when the molecule gets unmounted. However, if you have children, that listen of events of `Agent` and `Molecule`, you'll have to handle the cleanup yourself if they can get unmounted while the molecule is still alive (mounted). 384 | 385 | ## [Back to Table of Contents](./index.md) 386 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Molecule 4 | 5 | Molecule is the container of communication for all its components. 6 | 7 | ```js 8 | import { molecule } from 'react-molecule'; 9 | 10 | const Page = molecule(() => MoleculeOptions?)(Component); 11 | ``` 12 | 13 | ### Molecule Options 14 | 15 | | Option | Type | Description | 16 | | -------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 17 | | name | `string` | Give your molecule a name, this can be used for debugging purposes when debug is true | 18 | | registry | `object` or `ComponentRegistry` | If you pass a simple object like: `{Item}` it will automatically create a `ComponentRegistry` for you. This options is used to extend the registry at molecule level | 19 | | debug | `boolean` | Defaults to _false_. If this option is _true_ then it will log all events that are passed through it (instantiated, emitted, handled) | 20 | | agents | `{[agent]: (molecule) => Agent}` | The agents is a map of agent factories. So you either do `{myAgent: (molecule) => new MyAgent({molecule})}` or you can do `{myAgent: MyAgent.factory()}`. You also have the ability to create agents without passing molecule (if you don't need it): `{myAgent: () => MyAgent }` | 21 | | store | `any` | The store can be anything you wish, either a simple map, either an `observable` from [MobX](https://mobx.js.org/) | 22 | | config | `any` | You can simply set a configuration at molecule level that would allow the children to read from it and adapt their behavior based on it. This is different from the store because configuration should never be changed. Imagine store as being `state` and config as being `props` | 23 | 24 | ### molecule object 25 | 26 | You can access all the information provided in `config` in the `molecule` model. Exception being `agents`, which are stored as the actual instantiations not the factory functions. 27 | 28 | You can also access `molecule.emitter` if you need it. It returns an instance of the `EmitterModel` which is basically `eventemitter3` that allows a special type of Event. 29 | 30 | | Member | Returns | Description | 31 | | ------------------------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 32 | | getAgent(name) | Agent | Returns the defined agents. Throws an `Error` if no agent is found with that name. | 33 | | registry | `ComponentRegistry` | This an instantiation of the registry that you provided. If you provided a map, it's a registry that has as parent the global `Registry` | 34 | | emit(event, ...values) | void | Emits events to the laser-focused molecule. For example `molecule.emit('search', 'John')` this will notify all listeners | 35 | | on(event, handler) | void | Listens to events emitted to the molecule. `molecule.on('search', (value) => {...})`. You can also use `off()` but in theory don't worry about it, when molecule is unmounted this is handled automatically | 36 | | once(event, handler) | void | Same as `on()` but after the first event is caught it will stop listening to it | 37 | | deepmit(event, ...values) | void | Emits events to itself first, then to all the children. This can be useful in some edge-cases | 38 | | parent | `Molecule` | This stores the parent Molecule. Events are propagated to the parent molecule automatically, so if you have a `` within a ``, events emitted on the molecule are sent up to the parent | 39 | | children | `Molecule[]` | The children of the molecule | 40 | | root | `Molecule` | Returns the top-most molecule in the `Molecule Tree`. Returns itself if it does not have any parent. | 41 | | closest(name) | `Molecule` | 42 | | Returns the first parent it finds with that name | 43 | 44 | ## Agent 45 | 46 | Agent is the way components inside a molecule communicate with outside world. 47 | 48 | ```js 49 | import { Agent, molecule } from "react-molecule"; 50 | 51 | class MyAgent extends Agent {} 52 | 53 | molecule(() => { 54 | return { 55 | agents: { 56 | my: MyAgent.factory() 57 | } 58 | }; 59 | })(Component); 60 | ``` 61 | 62 | | Member | Returns | Description | 63 | | ---------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 64 | | molecule | `Molecule` | Returns the molecule the agent has been instantiated in | 65 | | config | `object` | Returns the configuration of the agent. The one provided in `Agent.factory(config)` when passing it to the molecule | 66 | | prepare() | void | When molecule initialises (component constructs) it first calls prepare() on all agents to give them a chance to hook into each other | 67 | | init() | void | This is run after prepare() has run on all agents. | 68 | | validate(config) | void | This refers to the configuration you given when instantiating the agent: `new Agent(config)`. Most likely you pass the config when creating the factory: `{myAgent: MyAgent.factory({ endpoint: '...' })` | 69 | | emit(event, ...values) | void | Emits events to the laser-focused molecule. For example `agent.emit('search', 'John')` this will notify all listeners | 70 | | on(event, handler) | void | Listens to events emitted to the molecule. `agent.on('search', (value) => {...})` | 71 | | once(event, handler) | void | Same as `on()` but after the first event is caught it will stop listening to it | 72 | 73 | ## Registry 74 | 75 | This is where your hackable components co-exist in a sort of JS object with little sugars on top. 76 | 77 | ```js 78 | // `Registry` represents the global registry of components. 79 | import { Registry } from "react-molecule"; 80 | 81 | const Hello = () => "Hello!"; // Any React Component 82 | 83 | Registry.blend({ 84 | Hello 85 | }); 86 | 87 | // Now you are able to access it from anywhere 88 | const { Hello } = Registry; 89 | 90 | // And render it 91 | ``` 92 | 93 | #### Creating Registries 94 | 95 | ```js 96 | import { createRegistry } from 'react-molecule'; 97 | 98 | const Item = () => 'Hello!'; 99 | 100 | // First argument represents the initial elements 101 | const CustomRegistry = createRegistry({Item}, parent?); 102 | ``` 103 | 104 | If `Item` isn't found, it will try to look for it in the parent, and if the parent has a parent, it will look using a `bottom-up` approach. 105 | 106 | #### Blending Registries 107 | 108 | ```js 109 | CustomRegistry.blend( 110 | { HelloAgain: MyReactComponent } 111 | { 112 | prefix?: 'Say', // The name will be prefixed SayHelloAgain => MyReactComponent 113 | throwOnCollisions: true, // Defaults: false. If there is a pre-existing 'SayHelloAgain' it will throw an exception 114 | } 115 | ); 116 | ``` 117 | 118 | #### Enveloping Registry Items 119 | 120 | When you are working with a package built with `react-molecule` and you want to override a certain component. There may be many situations where you just want to wrap that component, or pass different props to it. 121 | 122 | This is why we have a special type of blend, and it's enough to specify a function with 2 arguments: 123 | 124 | ```jsx 125 | CustomRegistry.blend({ 126 | // Note the second argument which represents the old component. 127 | Item: (props, Item) => { 128 | return ; 129 | } 130 | }); 131 | ``` 132 | 133 | What this does, is that when you are using `Item` from the registry inside your molecule, or outside, if there has been that component already defined you can envelop it. You have to check if that `Component` exists. The reason we don't throw an exception is that you may want to customize it deeper. 134 | 135 | ## Emitter 136 | 137 | Uses `eventemitter3` npm package. API can be found here: https://nodejs.org/api/events.html 138 | 139 | Used in `molecule` and `agents` 140 | 141 | Emitter extends the standard one by allowing smart events (which are basically events that can validate their parameters): 142 | 143 | ```js 144 | import { Emitter, EmitterModel } from "react-molecule"; 145 | 146 | const Events = { 147 | SAY_HELLO: { 148 | name: "say.hello", 149 | validate(params) { 150 | // Throw if they are invalid 151 | } 152 | } 153 | }; 154 | 155 | // Use it as you would use a string! 156 | Emitter.on(Events.SAY_HELLO, () => {}); 157 | Emitter.emit(Events.SAY_HELLO, params); 158 | 159 | // Craft your own instances of emitter, if needed, Emitter acts as a global version of it 160 | // Each emitter from molecules or agent is an instantiation of this model 161 | const MyEmitter = new EmitterModel({ 162 | context: "MyEmitter", // Helpful when debug is true 163 | debug: true, // Defaults to false. If true, will console.log emits, listenings and handles 164 | parent: ParentEmitter // Optionally pass a parent, each emit propagates to the parent 165 | }); 166 | ``` 167 | 168 | Note: We don't recommend using the global emitter at all, unless your application contains other rendering engines than React (yes, this can happen, especially for people migrating from a statically rendered PHP app). 169 | 170 | ## React Hooks 171 | 172 | ```jsx 173 | import { 174 | useMolecule, 175 | useAgent, 176 | useAgentConfig, 177 | useAgentStore, 178 | useStore, 179 | useRegistry, 180 | useEmitter, 181 | useConfig 182 | } from "react-molecule"; 183 | 184 | function Component() { 185 | const molecule = useMolecule(); 186 | const moleculeConfig = useConfig(); 187 | const moleculeStore = useStore(); 188 | const moleculeRegistry = useRegistry(); 189 | const moleculeEmitter = useEmitter(); 190 | 191 | const loaderAgent = useAgent("loader"); // agent name 192 | const loaderAgentConfig = useAgentConfig("loader"); 193 | const loaderAgentStore = useAgentConfig("loader"); 194 | } 195 | ``` 196 | 197 | #### useAgent 198 | 199 | ```jsx 200 | const agent = useAgent("loader"); 201 | ``` 202 | 203 | ## React HOCs 204 | 205 | #### withMolecule 206 | 207 | Access the enveloping molecule in a (deeply) nested child. Receives `molecule` inside props. 208 | 209 | ```jsx 210 | import { withMolecule } from "react-molecule"; 211 | 212 | export default withMolecule(UserList); 213 | ``` 214 | 215 | #### withAgent 216 | 217 | Access a certain agent from the enveloping molecule in a nested child. Receives `molecule` and `agent` inside props. 218 | 219 | ```jsx 220 | import { withAgent } from 'react-molecule'; 221 | 222 | // Injects `agent` as prop to UserList 223 | export default withAgent('loader')(UserList); 224 | 225 | // Injects `loader` as prop to UserList 226 | // Looks weird, but in case you need it, you don't have to have a HOC to transform the `agent` prop to something else 227 | export default withAgent('loader', 'loader')(UserList); 228 | ``` 229 | 230 | #### 231 | 232 | Passing the molecule via functional children: 233 | 234 | ```jsx 235 | import { WithMolecule } from "react-molecule"; 236 | 237 | {molecule => }; 238 | ``` 239 | 240 | #### 241 | 242 | Passing `{agent, molecule}` via functional children: 243 | 244 | ```jsx 245 | import { WithAgent } from "react-molecule"; 246 | 247 | 248 | {({ agent, molecule }) => } 249 | ; 250 | ``` 251 | 252 | Optionally, you could provide `asName` prop to `WithAgent`: 253 | 254 | ```jsx 255 | import { WithAgent } from "react-molecule"; 256 | 257 | 258 | {({ loader, molecule }) => } 259 | ; 260 | ``` 261 | --------------------------------------------------------------------------------