├── .travis.yml ├── .babelrc ├── .gitignore ├── radon ├── constants.js ├── virtualNode.js ├── constructorNode.js ├── combineNodes.js └── siloNode.js ├── index.js ├── rollup.config.js ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── LICENSE.txt ├── package.json ├── tests └── virtualSilo.test.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules 3 | npm-debug.log 4 | .DS_Store 5 | package-lock.json 6 | .github -------------------------------------------------------------------------------- /radon/constants.js: -------------------------------------------------------------------------------- 1 | export const ARRAY = 'ARRAY'; 2 | export const OBJECT = 'OBJECT'; 3 | export const PRIMITIVE = 'PRIMITIVE'; 4 | export const CONTAINER = 'CONTAINER'; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | import combineNodes from './radon/combineNodes'; 3 | import ConstructorNode from './radon/constructorNode'; 4 | 5 | export const combineState = combineNodes; 6 | export const StateNode = ConstructorNode; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import { uglify } from 'rollup-plugin-uglify'; 3 | 4 | export default { 5 | input: 'index.js', 6 | output: { 7 | file: 'build/bundle.js', 8 | format: 'cjs', 9 | sourcemap: 'inline', 10 | }, 11 | plugins: [ 12 | babel({ 13 | exclude: 'node_modules/**', 14 | }), 15 | uglify() 16 | ], 17 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Hannah Mitchell, Hayden Fithyan, Joshua Wright, Nicholas Smith] 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. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radon-js", 3 | "version": "3.2.22", 4 | "description": "Radon is an object-oriented state management framework for JavaScript applications.", 5 | "main": "build/bundle.js", 6 | "files": [ 7 | "build" 8 | ], 9 | "scripts": { 10 | "test": "jest", 11 | "test:siloNode": "jest siloNode.test.js", 12 | "watch": "jest constructorNode.test.js --watch", 13 | "test:virtualSilo": "jest virtualSilo.test.js --watch", 14 | "build": "rollup -c" 15 | }, 16 | "keywords": [ 17 | "npm", 18 | "state", 19 | "management", 20 | "oop", 21 | "object", 22 | "oriented" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/radon-js/Radon.git" 27 | }, 28 | "author": "Hannah Mitchell, Hayden Fithyan, Joshua Wright, Nicholas Smith", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/Joshua-Wright76/Radon/issues" 32 | }, 33 | "homepage": "https://github.com/Joshua-Wright76/Radon#readme", 34 | "dependencies": { 35 | "@babel/polyfill": "^7.0.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.1.2", 39 | "@babel/preset-env": "^7.1.0", 40 | "babel-core": "^7.0.0-bridge.0", 41 | "babel-preset-es2015-rollup": "^3.0.0", 42 | "jest": "^23.6.0", 43 | "jest-cli": "^23.6.0", 44 | "rollup": "^0.66.2", 45 | "rollup-plugin-babel": "^4.0.3", 46 | "rollup-plugin-uglify": "^6.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/virtualSilo.test.js: -------------------------------------------------------------------------------- 1 | import ConstructorNode from '../radon/constructorNode'; 2 | import combineNodes from '../radon/combineNodes'; 3 | 4 | describe('Initialize State', () => { 5 | const PersonState = new ConstructorNode('PersonState', 'ColorState'); 6 | PersonState.initializeState({ 7 | name: ["Michael", "Laythe"], 8 | age: {19: 19} 9 | }) 10 | 11 | const ColorState = new ConstructorNode('ColorState'); 12 | ColorState.initializeState({ 13 | red: ['crimson', 'ruby', 'scarlet', 'cherry'], 14 | blue: ['sapphire', 'navy', 'sky', 'baby'], 15 | green: ['chartreuse', 'ivy', 'teal', 'emerald'] 16 | }) 17 | 18 | ColorState.initializeModifiers({ 19 | blue: { 20 | test: (payload, previous) => { 21 | console.log('running'); 22 | } 23 | } 24 | }); 25 | 26 | PersonState.initializeModifiers({ 27 | age: { 28 | haveBirthday: (payload, previous) => { 29 | return previous + 1; 30 | } 31 | } 32 | }) 33 | 34 | let silo = combineNodes(PersonState, ColorState/*BlimpState*/); 35 | 36 | test('ID\'s should represent the lineage of the node in the silo', () => { 37 | expect(silo['ColorState'].value['red'].value['red_2'].id).toBe('ColorState.red.red_2') 38 | expect(silo['ColorState'].value['blue'].value['blue_0'].id).toBe('ColorState.blue.blue_0') 39 | expect(silo['ColorState'].value['PersonState'].value['name'].id).toBe('ColorState.PersonState.name') 40 | }) 41 | }) 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /radon/virtualNode.js: -------------------------------------------------------------------------------- 1 | import * as types from './constants' 2 | 3 | class VirtualNode { 4 | constructor (node, modifiers) { 5 | this.parent = null; 6 | this.parents = {}; 7 | if (node.parent){ 8 | 9 | this.parent = node.parent.virtualNode; 10 | this.parents[this.parent.name] = this.parent; 11 | let ancestor = this.parent; 12 | 13 | while(ancestor.parent !== null){ 14 | ancestor = ancestor.parent; 15 | this.parents[ancestor.name] = ancestor; 16 | } 17 | } 18 | 19 | this.name = node.name; 20 | this.type = node.type; 21 | this.id = node.id; 22 | 23 | if (this.type === types.PRIMITIVE){ 24 | //value should just be an empty object. 25 | //when your children are being made 26 | //they'll just put themselves into your value. 27 | this.val = node.value; 28 | } else { 29 | this.val = {}; 30 | if(this.type === types.ARRAY){ this.val = [] } 31 | } 32 | 33 | if (node.type !== types.CONTAINER){ 34 | let name = node.name; 35 | if(name.includes('_')) name = name.split('_')[name.split('_').length - 1]; 36 | 37 | node.parent.virtualNode.val[name] = this; 38 | if(this.parent.type === types.CONTAINER){ 39 | this.parent[name] = this; 40 | } 41 | } 42 | 43 | if (node.modifiers){ 44 | let modifierKeys = Object.keys(modifiers); 45 | modifierKeys.forEach(modifierKey => { 46 | this[modifierKey] = modifiers[modifierKey]; 47 | }) 48 | } 49 | 50 | } 51 | updateTo(data){ 52 | this.val = data; 53 | } 54 | } 55 | 56 | export default VirtualNode; 57 | -------------------------------------------------------------------------------- /radon/constructorNode.js: -------------------------------------------------------------------------------- 1 | class ConstructorNode { 2 | constructor(name, parentName = null) { 3 | this.name = name; 4 | this.state = {}; 5 | this.parent = parentName; 6 | 7 | this.initializeState = this.initializeState.bind(this); 8 | this.initializeModifiers = this.initializeModifiers.bind(this); 9 | } 10 | 11 | /** 12 | * Adds variables to the state 13 | * @param {object} initialState - An object with keys as variable names and values of data 14 | */ 15 | 16 | initializeState(initialState) { 17 | // make sure that the input is an object 18 | if (typeof initialState !== 'object' || Array.isArray(initialState)) throw new Error('Input must be an object'); 19 | // loop through the state variables and create objects that hold the variable and any 20 | // associated modifiers 21 | Object.keys(initialState).forEach(newVariableInState => { 22 | this.state[newVariableInState] = { 23 | value: initialState[newVariableInState], 24 | // accounts for initializeModifers being called prior to initializeState 25 | // by checking to see if this object has already been created 26 | modifiers: this.state[newVariableInState] ? this.state[newVariableInState].modifiers : {} 27 | } 28 | }); 29 | } 30 | 31 | /** 32 | * Stores modifiers in state 33 | * @param {object} initialModifiers - An object with keys associated with existing initialized variables and values that are objects containing modifiers to be bound to that specific variable 34 | */ 35 | 36 | initializeModifiers(initialModifiers) { 37 | // make sure that the input is an object 38 | if (typeof initialModifiers !== 'object' || Array.isArray(initialModifiers)) throw new Error('Input must be an object'); 39 | // loops through the state modifiers. The same object is created here as in initializeState and it 40 | // will overwrite the initializeState object. But it needs to be done this way in case the dev calls 41 | // initializeModifiers before they call initializeState. Now it works either way 42 | Object.keys(initialModifiers).forEach(newModifiersInState => { 43 | this.state[newModifiersInState] = { 44 | // accounts for initializeState being called prior to initializeModifiers. 45 | value: this.state[newModifiersInState] ? this.state[newModifiersInState].value : null, 46 | modifiers: initialModifiers[newModifiersInState] 47 | } 48 | }); 49 | } 50 | 51 | set name(name) { 52 | if (typeof name !== 'string') throw new Error('Name must be a string'); 53 | else this._name = name; 54 | } 55 | 56 | get name() { 57 | return this._name; 58 | } 59 | 60 | set parent(parent) { 61 | if (typeof parent !== 'string' && parent !== null) throw new Error('Parent must be a string'); 62 | else this._parent = parent; 63 | } 64 | 65 | get parent() { 66 | return this._parent; 67 | } 68 | 69 | set state(state) { 70 | this._state = state; 71 | } 72 | 73 | get state() { 74 | return this._state; 75 | } 76 | } 77 | 78 | export default ConstructorNode; -------------------------------------------------------------------------------- /radon/combineNodes.js: -------------------------------------------------------------------------------- 1 | // import state class for instanceof check 2 | import ConstructorNode from './constructorNode.js'; 3 | import SiloNode from './siloNode.js'; 4 | import * as types from './constants.js' 5 | import virtualNode from './virtualNode.js' 6 | 7 | const silo = {}; 8 | const virtualSilo = {}; 9 | 10 | /** 11 | * Takes all of the constructorNodes created by the developer and turns them into the silo 12 | * @param {...ConstructorNode} args - A list of constructor Nodes 13 | */ 14 | 15 | function combineNodes(...args) { 16 | let devTool = null; 17 | if(args[0] && args[0].devTool === true) { 18 | devTool = args[0]; 19 | args.shift(); 20 | } 21 | if (args.length === 0) throw new Error('combineNodes function takes at least one constructorNode'); 22 | 23 | // hastable accounts for passing in constructorNodes in any order. 24 | // hashtable organizes all nodes into parent-child relationships so the silo is easier to create 25 | const hashTable = {}; 26 | 27 | // loop through the constructorNodes passed in as arguments 28 | args.forEach(constructorNode => { 29 | if (!constructorNode || constructorNode.constructor.name !== 'ConstructorNode') throw new Error('Only constructorNodes can be passed to combineNodes'); 30 | // a node with a null parent will be the root node, and there can only be one 31 | else if (constructorNode.parent === null) { 32 | // we check to see if the root key already exists in the hashtable. If so, this means a root 33 | // has already been established 34 | if (!hashTable.root) hashTable.root = [constructorNode]; 35 | else throw new Error('Only one constructor node can have null parent'); 36 | } 37 | // if the parent isn't null, then the parent is another node 38 | else { 39 | // if the parent doesn't exist as a key yet, we will create the key and set it to an array 40 | // that can be filled with all possible children 41 | if (!hashTable[constructorNode.parent]) hashTable[constructorNode.parent] = [constructorNode]; 42 | // if parent already exists, and node being added will append to the array of children 43 | else hashTable[constructorNode.parent].push(constructorNode); 44 | } 45 | }) 46 | 47 | // ensure there is a defined root before continuing 48 | if (!hashTable.root) throw new Error('At least one constructor node must have a null parent'); 49 | 50 | // a recursive function that will create siloNodes and return them to a parent 51 | function mapToSilo(constructorNode = 'root', parentConstructorNode = null) { 52 | // the very first pass will set the parent to root 53 | const constructorNodeName = (constructorNode === 'root') ? 'root' : constructorNode.name; 54 | 55 | // recursive base case, we only continue if the current node has any constructorNode children 56 | if (!hashTable[constructorNodeName]) return; 57 | 58 | const children = {}; 59 | 60 | // loop through the children arrays in the hashtable 61 | hashTable[constructorNodeName].forEach(currConstructorNode => { 62 | const valuesOfCurrSiloNode = {}; 63 | children[currConstructorNode.name] = new SiloNode(currConstructorNode.name, valuesOfCurrSiloNode, parentConstructorNode, {}, types.CONTAINER, devTool); 64 | 65 | // abstract some variables 66 | const currSiloNode = children[currConstructorNode.name]; 67 | const stateOfCurrConstructorNode = currConstructorNode.state; 68 | 69 | // create SiloNodes for all the variables in the currConstructorNode 70 | Object.keys(stateOfCurrConstructorNode).forEach(varInConstructorNodeState => { 71 | // is the variable is an object/array, we need to deconstruct it into further siloNodes 72 | if (typeof stateOfCurrConstructorNode[varInConstructorNodeState].value === 'object') { 73 | valuesOfCurrSiloNode[varInConstructorNodeState] = currSiloNode.deconstructObjectIntoSiloNodes(varInConstructorNodeState, stateOfCurrConstructorNode[varInConstructorNodeState], currSiloNode, true); 74 | } 75 | // otherwise primitives can be stored in siloNodes and the modifiers run 76 | else { 77 | valuesOfCurrSiloNode[varInConstructorNodeState] = new SiloNode(varInConstructorNodeState, stateOfCurrConstructorNode[varInConstructorNodeState].value, currSiloNode, stateOfCurrConstructorNode[varInConstructorNodeState].modifiers, types.PRIMITIVE, devTool); 78 | valuesOfCurrSiloNode[varInConstructorNodeState].linkModifiers(); 79 | } 80 | }) 81 | 82 | // recursively check to see if the current constructorNode/siloNode has any children 83 | const siloNodeChildren = mapToSilo(currConstructorNode, currSiloNode); 84 | // if a Node did have children, we will add those returned siloNodes as values 85 | // into the current siloNode 86 | if (siloNodeChildren) { 87 | Object.keys(siloNodeChildren).forEach(siloNode => { 88 | valuesOfCurrSiloNode[siloNode] = siloNodeChildren[siloNode]; 89 | }) 90 | } 91 | }) 92 | return children; 93 | } 94 | 95 | // here we will get the root siloNode with all its children added 96 | const wrappedRootSiloNode = mapToSilo(); 97 | 98 | // add the siloNode root to the plain silo object 99 | // it will always only be a single key (the root) that is added into the silo 100 | Object.keys(wrappedRootSiloNode).forEach(rootSiloNode => { 101 | silo[rootSiloNode] = wrappedRootSiloNode[rootSiloNode]; 102 | }); 103 | 104 | function identify () { 105 | //each node's ID is a snake_case string that represents a 106 | //route to that node from the top of the silo by name 107 | forEachSiloNode(node => { 108 | node.issueID() 109 | }); 110 | } 111 | 112 | identify(); 113 | 114 | function virtualize () { //runs through each node in the tree, turns it into a virtual node in the vSilo 115 | forEachSiloNode(node => { 116 | if(!virtualSilo[node.id]){ 117 | virtualSilo[node.id] = node.virtualNode; 118 | } 119 | }) 120 | } 121 | 122 | virtualize(); 123 | 124 | forEachSiloNode(node => { 125 | // apply keySubscribe only to object and array silo nodes 126 | if (node.type === 'OBJECT' || node.type === "ARRAY") { 127 | node.modifiers.keySubscribe = (key, renderFunc) => { 128 | const name = node.name + "_" + key; 129 | const subscribedAtIndex = node.value[name].pushToSubscribers(renderFunc); 130 | node.value[name].notifySubscribers(); 131 | return () => {node.removeFromSubscribersAtIndex(subscribedAtIndex)} 132 | } 133 | }}) 134 | 135 | silo.virtualSilo = virtualSilo; 136 | return silo; 137 | } 138 | 139 | /** 140 | * Applies the callback to every siloNode in the silo 141 | * @param {function} callback - A function that accepts a siloNode as its parameter 142 | */ 143 | 144 | // callbacks have to accept a SILONODE 145 | function forEachSiloNode(callback) { 146 | // accessing the single root in the silo 147 | Object.keys(silo).forEach(siloNodeRootKey => { 148 | inner(silo[siloNodeRootKey], callback); 149 | }) 150 | 151 | // recursively navigate to every siloNode 152 | function inner(head, callback) { 153 | if (head.constructor.name === 'SiloNode') { 154 | callback(head); 155 | if (head.type === types.PRIMITIVE) return; // recursive base case 156 | 157 | else { 158 | Object.keys(head.value).forEach(key => { 159 | if (head.value[key].constructor.name === 'SiloNode') { 160 | inner(head.value[key], callback); 161 | } 162 | }) 163 | } 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * Subscribes components to siloNodes in the silo 170 | * @param {function} renderFunction - Function to be appended to subscribers array 171 | * @param {string} name - Name of the relevant component with 'State' appended 172 | */ 173 | 174 | silo.subscribe = (renderFunction, name) => { 175 | if (!name) { 176 | if (!!renderFunction.prototype) { 177 | name = renderFunction.prototype.constructor.name + 'State'; 178 | } else { 179 | throw new Error('You can\'t use an anonymous function in subscribe without a name argument.'); 180 | } 181 | } 182 | 183 | let foundNode; 184 | let subscribedAtIndex; 185 | const foundNodeChildren = []; 186 | 187 | forEachSiloNode(node => { 188 | if(node.name === name){ 189 | subscribedAtIndex = node.pushToSubscribers(renderFunction) 190 | foundNode = node 191 | foundNodeChildren.push({node: foundNode, index: subscribedAtIndex}); 192 | } 193 | }) 194 | 195 | let unsubscribe; 196 | 197 | if (!!foundNode) { 198 | if (foundNode.value) { 199 | Object.keys(foundNode.value).forEach(key => { 200 | let node = foundNode.value[key]; 201 | if(node.type !== 'CONTAINER'){ 202 | subscribedAtIndex = node.pushToSubscribers(renderFunction); 203 | foundNodeChildren.push({node: node, index: subscribedAtIndex}); 204 | 205 | } 206 | }) 207 | } 208 | 209 | unsubscribe = () => { 210 | let ob; 211 | Object.keys(foundNodeChildren).forEach(key => { 212 | ob = foundNodeChildren[key]; 213 | ob._subscribers.splice(ob.index, 1) 214 | }) 215 | } 216 | 217 | foundNode.notifySubscribers(); 218 | return unsubscribe; 219 | 220 | } else { 221 | console.error(new Error('You are trying to subscribe to something that isn\'t in the silo.')); 222 | return function errFunc () { 223 | console.error(new Error('You are trying to run unsubscribe from something that wasn\'t in the silo in the first place.')) 224 | } 225 | } 226 | } 227 | 228 | export default combineNodes; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | [![Build Status](https://img.shields.io/travis/com/radonjs/Radon/master.svg?label=Radon&style=flat-square)](https://travis-ci.com/radonjs/Radon) [![npm](https://img.shields.io/npm/v/radon-js.svg?style=flat-square)](https://npmjs.org/package/radon-js) 4 | 5 | [Radon](http://radonjs.org) is an object-oriented state management framework for JavaScript applications. 6 | 7 | Read our documentation at [radonjs.org](http://radonjs.org/docs/introduction) 8 | 9 | # Why? 10 | 11 | ## Data Encapsulation 12 | 13 | One of the first goals of Radon was to implement an object oriented state manager capable of data encapsulation. Many state managers allow pieces of state to be accessible by any component or module, and with that access follows modification allowances. This inherently conflicts with a ubiquitous object oriented programming practice: limiting scope. Limiting the scope of a variable or method provides better context for its purpose and makes it easier to reason about. Plus, there's the added bonus of protecting data from being modified by script that has several degrees of separation. Many programming languages have native features to handle data encapsulation such as privatized class attributes and methods. Unfortunately, Javascript doesn't have the same privatization features held by universal languages such as Java and C/C++. Therefore, the data encapsulation feature of Radon needed to be derived by other means. 14 | 15 | To understand encapsulation in Radon, it is first important to understand how the data is organized. Radon is built using a tree data structure. Pieces of state are stored in specially designed nodes and are organized in a way that parallels the component tree of many common frontend frameworks such as React or Vue. For example, if a developer created an initial App component that needed access to variables in state, a corresponding AppState node would be created that contained those specific variables and any accompanying modifier functions. Now let's say the App component renders two more components named Navbar and Main. If Navbar were to be stateful, it would need a corresponding node called NavbarState. If the same thing can be said for Main, then it would have a corresponding state node called MainState. If a frontend component is intended to be stateless, then there will be no corresponding state node. So now we can hopefully start to imagine that the App Component is at the top of a component tree (as the root), with NavbarState and MainState branching beneath it. The same can be said for the State Tree. AppState is our root, with NavbarState and MainState branching below. 16 | 17 | But what does this mean for data encapsulation? The intention for the State Tree is for state nodes to share their data and modifiers with corresponding frontend components. However, this implementation alone would be too constricting of the data. Therefore, frontend components are not only able to access the data from their corresponding state nodes, but also the data from its parent, grandparent, and any further parent tracing back to the root. Now there's a greater sense of flow that encourages commonly used and shared data to be stored near the root, and specialized data to be stored as leaves on the tree. In sum, frontend components will have access to parental lineage data, but will not have access to their sibling’s or children's data. Thus, varying pieces of state are exposed where they are needed, and hidden where they are not. 18 | 19 | ## Component Rendering Linked to Objects in State 20 | 21 | Another feature of Radon intends to remove unnecessary re-rendering that can emerge from modifying objects in state. In other state management systems, modifying a single key/value pair in a plain object or an index in an array will result in a re-render of any component subscribed to the object. The Radon State Tree solves this problem by deconstructing objects into state nodes by index or key/value pairs. The object deconstruction feature allows for direct modification of these indices/pairs and triggers a re-render of only the component listening to that particular data point. 22 | 23 | ## Asynchronous Modifications to State 24 | 25 | Modifiers are functions written by the developer that can only modify a single state variable. Developers have the option to create an asynchronous modifier which may seem problematic if multiple modifiers are called in tandem to edit the same piece of state. However, Radon ensures that all state changes, whether asynchronous or synchronous, occur in the order of initial invocation. This is accomplished with an asynchronous queue that awaits the completion of the most recently invoked modifier before progressing to the next. Hence, the developer does not need to worry about conflicting state changes or out of order updates. 26 | 27 | # Getting Started 28 | 29 | To install the stable version using npm as your package manager: 30 | 31 | ```npm install --save radon-js``` 32 | 33 | The Radon source code is transpiled to ES2015 to work in any modern browser. You don't need to use Babel or a module bundler to get started with Radon. 34 | 35 | Most likely, you'll also need the React bindings and the developer tools. 36 | 37 | ```npm install --save react-radon``` 38 | 39 | Unlike Radon, React doesn't provide UMD builds, so you will need to use a CommonJS module bundler like Webpack, Parcel, or Rollup to utilize Radon with React. 40 | 41 | ## How Radon Works 42 | 43 | ```javascript 44 | import { StateNode } from 'radon-js' 45 | 46 | /* 47 | StateNode is a class needed for creating instances of state. In Radon, StateNodes are created in 48 | tandem with frontend components. The naming convention is important here; if you have created 49 | a frontend component called App with the intent of statefulness, then an instance of StateNode must be 50 | declared and labeled as AppState. This will allow the App component to properly bind to AppState 51 | at compile time. 52 | 53 | 54 | The new instance of StateNode takes two arguments: the first argument is the name of the StateNode you 55 | are creating which must follow our naming convention. The second argument is the name of the parent 56 | node. One StateNode must be considered the root of the state tree. Therefore, at only one occasion can 57 | the parent argument be omitted. This instance of StateNode will be considered the root. Every other 58 | StateNode must take a parent argument. 59 | */ 60 | 61 | const AppState = new StateNode('AppState'); 62 | // or 63 | // const AppState = new StateNode('AppState', 'OtherState'); 64 | 65 | /* 66 | To declare variables in state, the method initializeState must be called which takes an object 67 | as an argument. The variable names and their data should be listed in the object as key-value pairs. 68 | */ 69 | 70 | AppState.initializeState({ 71 | name: 'Radon', 72 | status: true, 73 | arrayOfNames: [] 74 | }) 75 | 76 | /* 77 | Modifiers are functions that modify a single variable in state. Modifiers are attached to variables by 78 | calling the method initializeModifiers which also takes an object as an argument. The keys of the 79 | argument object must correspond to variables that have already been declared in AppState. The values 80 | are objects that contain the modifier functions as key-value pairs. There are two types of modifiers 81 | in Radon. The first type, as seen below, can accept either 1 or 2 arguments. The 'current' argument 82 | will automatically be injected with the bound state variable. The 'payload' argument is any data that 83 | can be used to modify or replace the 'current' value of state. Even if the current value of state is 84 | not used in the modifier, it will still be passed in automatically. 85 | */ 86 | 87 | AppState.initializeModifiers({ 88 | name: { 89 | updateName: (current, payload) => { 90 | return payload; 91 | } 92 | }, 93 | status: { 94 | toggleStatus: (current) => { 95 | return !current; 96 | } 97 | } 98 | }) 99 | 100 | /* 101 | It is important to note that when these modifiers are called from a component, only the payload argument 102 | must be passed into the function as Radon will fill the 'current' parameter by default. 103 | */ 104 | 105 | 106 | 107 | 108 | /* 109 | The second modifier type is what helps Radon eliminate unnecessary re-rendering of frontend components. 110 | This modifier type accepts three arguments and is used exclusively with objects. *Note that 111 | initializeModifiers should only be called once. It is shown again here for demonstration purposes only*. 112 | */ 113 | 114 | AppState.initializeModifiers({ 115 | arrayOfNames: { 116 | addNameToArray: (current, payload) => { 117 | current.push(payload); 118 | return current; 119 | }, 120 | updateAName: (current, index, payload) => { 121 | return payload; 122 | } 123 | } 124 | }) 125 | 126 | /* 127 | The modifier addNumberToArray is nothing new. Since the goal of the modifier is to edit the array as a 128 | whole, the entire array object is passed into the 'current' parameter. A modifier that edits the array 129 | will cause a re-render of any component that subscribes to the array. However, we may have 130 | circumstances in which we only want to edit a single index within an array. In this case we create a 131 | modifier that accepts an index. The 'current' value will always reflect arrayOfNumbers[index]. This 132 | will prevent a re-render of components listening to the entire array, and will instead only re-render 133 | components listening to the specified index. 134 | 135 | Again, it is important to note that the 'current' parameter will be injected with state automatically. 136 | */ 137 | 138 | 139 | 140 | /* 141 | The same logic applies to plain objects. Instead of passing a numerical index into a modifier, the key 142 | of a key-value pair can be passed in instead. 143 | 144 | Objects can be nested and it is possible to create modifiers for deeply nested objects. Ultimately, the 145 | modifier will always be bound to the parent object. However, the key/index parameter will transform into 146 | a longer chain of 'addresses' to tell Radon exactly where the data is stored. For example: 147 | */ 148 | 149 | names: { 150 | first: ['', 'Beth', 'Lisa'], 151 | last: { 152 | birth: ['Mitchell', 'Sanchez', 'Delaney'], 153 | married: ['Mitchell', 'Smith', 'Delaney'] 154 | } 155 | } 156 | 157 | /* 158 | To inject the name 'Hannah' into index 0 of the 'first' array, the specified 'address' would be first_0. 159 | To change the value of index 2 of the 'married' array, the specified 'address' would be last_married_2. 160 | */ 161 | 162 | /* 163 | Once all StateNodes have been declared, they should be combined in the function combineStateNodes. The 164 | returned object is known as the silo. 165 | */ 166 | 167 | import AppState from './appState'; 168 | import NarbarState from './navbarState'; 169 | import mainState from './mainState'; 170 | 171 | const silo = combineStateNodes(AppState, NavbarState, MainState); 172 | 173 | ``` 174 | 175 | 176 | ### Bind the state 177 | In order to use the **Silo** state in a component, it must be passed to the same from the top of the application. 178 | That depends on the framework binding. 179 | Below you can find a working example of use of **Radon** on **React** via [react-radon](https://github.com/radonjs/React-Radon), the react binding for this library, as an example: 180 | 181 | ```javascript 182 | import {render} from 'react-dom'; 183 | import {Provider} from 'react-radon'; 184 | 185 | // Silo from Exported combineNodes from the example before 186 | import silo from './localSiloLocation'; 187 | 188 | render( 189 | 190 | 191 | , 192 | document.getElementById('root')); 193 | 194 | 195 | // And in the component where you need the piece of state 196 | 197 | import React from 'react'; 198 | import { bindToSilo } from 'react-radon' 199 | 200 | const ReactComponent = (props) => { 201 | return ( 202 |
203 | {props.name} 204 |
205 | ) 206 | } 207 | 208 | export default bindToSilo(ReactComponent); 209 | 210 | ``` 211 | 212 | ## Built With 213 | 214 | Rollup - Module Bundler 215 | 216 | Babel - ES2015 transpiling 217 | 218 | ## Versioning 219 | 2.0.0 We use SemVer for versioning. 220 | 221 | ## Authors 222 | Hannah Mitchell, 223 | 224 | Hayden Fithyan, 225 | 226 | Joshua Wright, 227 | 228 | Nicholas Smith 229 | 230 | ## License 231 | This project is licensed under the MIT License - see the LICENSE.txt file for details 232 | -------------------------------------------------------------------------------- /radon/siloNode.js: -------------------------------------------------------------------------------- 1 | import * as types from './constants.js'; 2 | import VirtualNode from './virtualNode.js' 3 | 4 | 5 | class SiloNode { 6 | constructor(name, value, parent = null, modifiers = {}, type = types.PRIMITIVE, devTool = null) { 7 | this.name = name; 8 | this.value = value; 9 | this.modifiers = modifiers; 10 | this.queue = []; 11 | this.subscribers = []; 12 | this.parent = parent; // circular silo node 13 | this.type = type; 14 | this.devTool = devTool; 15 | 16 | // bind 17 | this.linkModifiers = this.linkModifiers.bind(this); 18 | this.runModifiers = this.runModifiers.bind(this); 19 | this.notifySubscribers = this.notifySubscribers.bind(this); 20 | this.getState = this.getState.bind(this); 21 | this.reconstructArray = this.reconstructArray.bind(this); 22 | this.reconstructObject = this.reconstructObject.bind(this); 23 | this.deconstructObjectIntoSiloNodes = this.deconstructObjectIntoSiloNodes.bind(this); 24 | this.reconstruct = this.reconstruct.bind(this); 25 | this.pushToSubscribers = this.pushToSubscribers.bind(this); 26 | this.removeFromSubscribersAtIndex = this.removeFromSubscribersAtIndex(this); 27 | 28 | // invoke functions 29 | this.runQueue = this.runModifiers(); 30 | 31 | if(this.type === 'ARRAY' || this.type === 'OBJECT'){ 32 | this.modifiers.keySubscribe = (key, renderFunction) => { 33 | const name = this.name + '_' + key; 34 | let node = this.value[name] 35 | const subscribedAtIndex = node.pushToSubscribers(renderFunction); 36 | node.notifySubscribers(); 37 | return () => {node._subscribers.splice(subscribedAtIndex, 1)} 38 | } 39 | } 40 | 41 | this.id; 42 | this.issueID(); 43 | this.virtualNode = new VirtualNode(this, this.modifiers); 44 | } 45 | 46 | get name() { 47 | return this._name; 48 | } 49 | 50 | set name(name) { 51 | if (!name || typeof name !== 'string') throw new Error('Name is required and should be a string') 52 | this._name = name; 53 | } 54 | 55 | get value() { 56 | return this._value; 57 | } 58 | 59 | set value(value) { 60 | this._value = value; 61 | } 62 | 63 | get modifiers() { 64 | return this._modifiers; 65 | } 66 | 67 | set modifiers(modifiers) { 68 | if (typeof modifiers !== 'object' || Array.isArray(modifiers)) throw new Error('Modifiers must be a plain object'); 69 | this._modifiers = modifiers; 70 | } 71 | 72 | get queue() { 73 | return this._queue; 74 | } 75 | 76 | set queue(queue) { 77 | this._queue = queue; 78 | } 79 | 80 | get parent() { 81 | return this._parent; 82 | } 83 | 84 | set parent(parent) { 85 | if (parent && parent.constructor.name !== 'SiloNode') throw new Error('Parent must be null or a siloNode'); 86 | this._parent = parent; 87 | } 88 | 89 | get subscribers() { 90 | return this._subscribers; 91 | } 92 | 93 | set subscribers(subscribers) { 94 | this._subscribers = subscribers; 95 | } 96 | 97 | get type() { 98 | return this._type; 99 | } 100 | 101 | set type(type) { 102 | if (typeof type !== 'string' || !types[type]) throw new Error('Type must be an available constant'); 103 | this._type = type; 104 | } 105 | 106 | get virtualNode(){ 107 | return this._virtualNode 108 | } 109 | 110 | set virtualNode(virtualNode){ 111 | this._virtualNode = virtualNode; 112 | } 113 | 114 | get id(){ 115 | return this._id; 116 | } 117 | 118 | 119 | 120 | pushToSubscribers(renderFunction){ 121 | this.subscribers.push(renderFunction); 122 | } 123 | 124 | removeFromSubscribersAtIndex(index){ 125 | this.subcribers = this.subscribers.slice(index, 1); 126 | } 127 | 128 | //there's no setter for the ID because you cant set it directly. you have to use issueID 129 | 130 | //issueID MUST BE CALLED ON THE NODES IN ORDER ROOT TO LEAF. it always assumes that this node's parent will 131 | //have had issueID called on it before. use applyToSilo to make sure it runs in the right order 132 | issueID(){ 133 | if(this.parent === null){ //its the root node 134 | this._id = this.name; 135 | } else { //its not the root node 136 | this._id = this.parent.id + '.' + this.name; 137 | } 138 | } 139 | 140 | notifySubscribers() { 141 | if (this.subscribers.length === 0) return; 142 | // subscribers is an array of functions that notify subscribed components of state changes 143 | this.subscribers.forEach(func => { 144 | if (typeof func !== 'function') throw new Error('Subscriber array must only contain functions'); 145 | // pass the updated state into the subscribe functions to trigger re-renders on the frontend 146 | func(this.getState()); 147 | }) 148 | } 149 | 150 | /** 151 | * Invoked once in the siloNode constructor to create a closure. The closure variable 152 | * 'running' prevents the returned async function from being invoked if it's 153 | * still running from a previous call 154 | */ 155 | runModifiers() { 156 | let running = false; // prevents multiple calls from being made if set to false 157 | 158 | async function run() { 159 | if (running === false) { // prevents multiple calls from being made if already running 160 | running = true; 161 | // runs through any modifiers that have been added to the queue 162 | while (this.queue.length > 0) { 163 | 164 | // enforces that we always wait for a modifier to finish before proceeding to the next 165 | let nextModifier = this.queue.shift(); 166 | let previousState = null; 167 | if(this.devTool) { 168 | if(this.type !== types.PRIMITIVE) { 169 | previousState = this.reconstruct(this.name, this); 170 | } else { 171 | previousState = this.value; 172 | } 173 | } 174 | this.value = await nextModifier(); 175 | if(this.devTool) { 176 | this.devTool.notify(previousState, this.value, this.name, nextModifier.modifierName); 177 | } 178 | this.virtualNode.updateTo(this.value); 179 | if (this.type !== types.PRIMITIVE) this.value = this.deconstructObjectIntoSiloNodes().value; 180 | 181 | this.notifySubscribers(); 182 | } 183 | running = false; 184 | } 185 | } 186 | 187 | return run; 188 | } 189 | 190 | /** 191 | * Deconstructs objects into a parent siloNode with a type of object/array, and 192 | * children siloNodes with values pertaining to the contents of the object 193 | * @param {string} objName - The intended key of the object when stored in the silo 194 | * @param {object} objectToDeconstruct - Any object that must contain a key of value 195 | * @param {SiloNode} parent - Intended SiloNode parent to the deconstructed object 196 | * @param {boolean} runLinkedMods - True only when being called for a constructorNode 197 | */ 198 | deconstructObjectIntoSiloNodes(objName = this.name, objectToDeconstruct = this, parent = this.parent, runLinkedMods = false) { 199 | const objChildren = {}; 200 | let type, keys; 201 | 202 | // determine if the objectToDeconstruct is an array or plain object 203 | if (Array.isArray(objectToDeconstruct.value)) { 204 | keys = objectToDeconstruct.value; 205 | type = types.ARRAY; 206 | } else { 207 | keys = Object.keys(objectToDeconstruct.value); 208 | type = types.OBJECT; 209 | } 210 | 211 | // a silonode must be created before its children are made, because the children need to have 212 | // this exact silonode passed into them as a parent, hence objChildren is currently empty 213 | const newSiloNode = new SiloNode(objName, objChildren, parent, objectToDeconstruct.modifiers, type, this.devTool); 214 | 215 | // for arrays only 216 | if (Array.isArray(objectToDeconstruct.value) && objectToDeconstruct.value.length > 0) { 217 | // loop through the values in the objectToDeconstruct to create siloNodes for each of them 218 | objectToDeconstruct.value.forEach((indexedVal, i) => { 219 | // recurse if the array has objects stored in its indices that need further deconstructing 220 | if (typeof indexedVal === 'object') objChildren[`${objName}_${i}`] = this.deconstructObjectIntoSiloNodes(`${objName}_${i}`, {value: indexedVal}, newSiloNode, runLinkedMods); 221 | // otherwise for primitives we can go straight to creating a new siloNode 222 | // the naming convention for keys involves adding '_i' to the object name 223 | else objChildren[`${objName}_${i}`] = new SiloNode(`${objName}_${i}`, indexedVal, newSiloNode, {}, types.PRIMITIVE, this.devTool); 224 | }) 225 | } 226 | 227 | // for plain objects 228 | else if (keys.length > 0) { 229 | // loop through the key/value pairs in the objectToDeconstruct to create siloNodes for each of them 230 | keys.forEach(key => { 231 | // recurse if the object has objects stored in its values that need further deconstructing 232 | if (typeof objectToDeconstruct.value[key] === 'object') objChildren[`${objName}_${key}`] = this.deconstructObjectIntoSiloNodes(`${objName}_${key}`, {value: objectToDeconstruct.value[key]}, newSiloNode, runLinkedMods); 233 | // otherwise for primitives we can go straight to creating a new siloNode 234 | // the naming convention for keys involves adding '_key' to the object name 235 | else objChildren[`${objName}_${key}`] = new SiloNode(`${objName}_${key}`, objectToDeconstruct.value[key], newSiloNode, {}, types.PRIMITIVE, this.devTool); 236 | }) 237 | } 238 | 239 | // linkModifiers should only be run if a constructorNode has been passed into this function 240 | // because that means that the silo is being created for the first time and the modifiers need 241 | // to be wrapped. For deconstructed objects at runtime, wrapping is not required 242 | if (runLinkedMods) newSiloNode.linkModifiers(); 243 | 244 | return newSiloNode; 245 | } 246 | 247 | /** 248 | * Wraps developer written modifiers in async functions with state passed in automatically 249 | * @param {string} nodeName - The name of the siloNode 250 | * @param {object} stateModifiers - An object containing unwrapped modifiers most likely from the constructorNode 251 | */ 252 | linkModifiers(nodeName = this.name, stateModifiers = this.modifiers) { 253 | if (!stateModifiers || Object.keys(stateModifiers).length === 0) return; 254 | const that = this; 255 | 256 | // loops through every modifier created by the dev 257 | Object.keys(stateModifiers).forEach(modifierKey => { 258 | 259 | // renamed for convenience 260 | const modifier = stateModifiers[modifierKey]; 261 | if (typeof modifier !== 'function' ) throw new Error('All modifiers must be functions'); 262 | 263 | // modifiers with argument lengths of 2 or less are meant to edit primitive values 264 | // OR arrays/objects in their entirety (not specific indices) 265 | else if (modifier.length <= 2) { 266 | // the dev's modifier function needs to be wrapped in another function so we can pass 267 | // the current state value into the 'current' parameter 268 | let linkedModifier; 269 | // for primitives we can pass the value straight into the modifier 270 | if (that.type === types.PRIMITIVE) linkedModifier = async (payload) => await modifier(that.value, payload); 271 | // for objects we need to reconstruct the object before it is passed into the modifier 272 | else if (that.type === types.OBJECT || that.type === types.ARRAY) { 273 | linkedModifier = async (payload) => await modifier(this.reconstruct(nodeName, that), payload); 274 | } 275 | 276 | // the linkedModifier function will be wrapped in one more function. This final function is what 277 | // will be returned to the developer 278 | // this function adds the linkedModifier function to the async queue with the payload passed in as 279 | // the only parameter. Afterward the queue is invoked which will begin moving through the 280 | // list of modifiers 281 | this.modifiers[modifierKey] = payload => { 282 | // wrap the linkedModifier again so that it can be added to the async queue without being invoked 283 | const callback = async () => await linkedModifier(payload); 284 | if(this.devTool) { 285 | callback.modifierName = modifierKey; 286 | } 287 | that.queue.push(callback); 288 | that.runQueue(); 289 | } 290 | } 291 | 292 | // modifiers with argument lengths of more than 2 are meant to edit specific indices or 293 | // key/value pairs of objects ONLY 294 | else if (modifier.length > 2) { 295 | // the dev's modifier function needs to be wrapped in another function so we can pass 296 | // the current state value into the 'current' parameter 297 | // reconstruct will reassemble objects but will simply return if a primitive is passed in 298 | const linkedModifier = async (index, payload) => await modifier(this.reconstruct(index, that.value[index]), index, payload); 299 | 300 | // the linkedModifier function will be wrapped in one more function. This final function is what 301 | // will be returned to the developer 302 | // this function adds the linkedModifier function to the async queue with the payload passed in as 303 | // the only parameter. Afterward the queue is invoked which will begin moving through the 304 | // list of modifiers 305 | this.modifiers[modifierKey] = (index, payload) => { 306 | // wrap the linkedModifier again so that it can be added to the async queue without being invoked 307 | const callback = async () => await linkedModifier(`${this.name}_${index}`, payload); 308 | // since the modifier is called on the ARRAY/OBJECT node, we need to add the callback 309 | // to the queue of the child. The naming convention is: 'objectName_i' || 'objectName_key' 310 | if(this.devTool) { 311 | callback.modifierName = modifierKey; 312 | } 313 | that.value[`${this.name}_${index}`].queue.push(callback); 314 | that.value[`${this.name}_${index}`].runQueue(); 315 | } 316 | } 317 | }) 318 | 319 | Object.keys(this.modifiers).forEach( modifierKey => { 320 | this.virtualNode[modifierKey] = this.modifiers[modifierKey]; 321 | }) 322 | } 323 | 324 | /** 325 | * A middleman function used for redirection. Should be called with an object needed reconstruction 326 | * and will then accurately assign its next destination 327 | * @param {string} siloNodeName - The name of the siloNode 328 | * @param {object} currSiloNode - The address of the parent 'OBJECT/ARRAY' siloNode 329 | */ 330 | reconstruct(siloNodeName, currSiloNode) { 331 | let reconstructedObject; 332 | if (currSiloNode.type === types.OBJECT) reconstructedObject = this.reconstructObject(siloNodeName, currSiloNode); 333 | else if (currSiloNode.type === types.ARRAY) reconstructedObject = this.reconstructArray(siloNodeName, currSiloNode); 334 | // called if the value passed in is a primitive 335 | else return currSiloNode.value; 336 | 337 | return reconstructedObject; 338 | } 339 | 340 | /** 341 | * Reconstructs plain objects out of siloNode values 342 | * @param {string} siloNodeName - The name of the siloNode 343 | * @param {object} currSiloNode - The address of the parent 'OBJECT' siloNode 344 | */ 345 | reconstructObject(siloNodeName, currSiloNode) { 346 | // our currently empty object to be used for reconstruction 347 | const newObject = {}; 348 | // loop through the siloNodes stored in the 'OBJECT' value to extract the data 349 | Object.keys(currSiloNode.value).forEach(key => { 350 | // simplified name 351 | const childObj = currSiloNode.value[key]; 352 | 353 | // get the keyName from the naming convention 354 | // if the siloNode name is 'cart_shirts', the slice will give us 'shirts' 355 | const extractedKey = key.slice(siloNodeName.length + 1); 356 | // if an additional object is stored in the values, then we must recurse to 357 | // reconstruct the nested object as well 358 | if (childObj.type === types.OBJECT || childObj.type === types.ARRAY) { 359 | newObject[extractedKey] = this.reconstruct(key, childObj); 360 | } 361 | // otherwise we have a primitive value which can easily be added to the reconstructed 362 | // object using our extractedKey to properly label it 363 | else if (childObj.type === types.PRIMITIVE) { 364 | newObject[extractedKey] = childObj.value; 365 | } 366 | }) 367 | 368 | // object successfully reconstructed at this level 369 | return newObject; 370 | } 371 | 372 | /** 373 | * Reconstructs arrays out of siloNode values 374 | * @param {string} siloNodeName - The name of the siloNode 375 | * @param {object} currSiloNode - The address of the parent 'ARRAY' siloNode 376 | */ 377 | reconstructArray(siloNodeName, currSiloNode) { 378 | // our currently empty array to be used for reconstruction 379 | const newArray = []; 380 | // loop through the siloNodes stored in the 'ARRAY' value to extract the data 381 | Object.keys(currSiloNode.value).forEach((key, i) => { 382 | // simplified name 383 | const childObj = currSiloNode.value[key]; 384 | // if an additional object is stored in the values, then we must recurse to 385 | // reconstruct the nested object as well 386 | if (childObj.type === types.ARRAY || childObj.type === types.OBJECT) { 387 | newArray.push(this.reconstruct(`${siloNodeName}_${i}`, childObj)); 388 | } 389 | // otherwise we have a primitive value which can easily be added to the reconstructed 390 | // object using our extractedKey to properly label it 391 | else if (childObj.type === types.PRIMITIVE) { 392 | newArray.push(childObj.value); 393 | } 394 | }) 395 | 396 | // array successfully reconstructed at this level 397 | return newArray; 398 | } 399 | 400 | getState(){ 401 | if(this.type === types.CONTAINER){ 402 | return this.virtualNode 403 | } else { 404 | let context = this.virtualNode; 405 | while(context.type !== types.CONTAINER){ 406 | context = context.parent; 407 | } 408 | return context; 409 | } 410 | } 411 | } 412 | 413 | export default SiloNode; --------------------------------------------------------------------------------