Last active
August 15, 2017 04:37
-
-
Save jcgregorio/482c5b6b7ee4e2c78004a5df7fe48461 to your computer and use it in GitHub Desktop.
Binds a Redux state store to and from HTML elements.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Binder makes it easy to setup bindings between data stored in a Redux | |
| // store and HTML elements. | |
| class Binder { | |
| // store - A Redux store. | |
| // dispatch - A function called to update the state in store. | |
| // ele - The root element where all element searches begin from, i.e. | |
| // querySelectorAll is run on this element. Defaults to document. | |
| constructor(store, dispatch, ele = document) { | |
| this.to = []; | |
| this.store = store; | |
| this.oldState = {}; // Empty object so that filling in the default value triggers writes. | |
| this.ele = ele; | |
| this.dispatch = dispatch; | |
| this.store.subscribe(this._newState.bind(this)); | |
| } | |
| _newState() { | |
| let delta = sr.object.getDelta(this.store.getState(), this.oldState); | |
| this.to.forEach(item => { | |
| let value = delta; | |
| for (var i = 0; i < item.parsedPath.length; i++) { | |
| value = value[item.parsedPath[i]]; | |
| if (value === undefined) { | |
| return | |
| } | |
| } | |
| item.func(value); | |
| }); | |
| } | |
| // add - Add a binding between some part of the state and an element. | |
| // | |
| // statePath - A "." separated path into the state object. Ex: | |
| // The statePath "a.b" selects 2 in the object {a: { b: 2, c: 3}}. | |
| // The empty statePath, "", selects any change to the state. | |
| // query - A CSS selector that determines which element(s) are being bound. | |
| // The selector will be run from the 'ele' passed into the | |
| // constructor. Ex: "div > li", "length". | |
| // to - Defines how the state maps to the element. Can be an object of the form: | |
| // | |
| // { | |
| // attr: "input", | |
| // content: false, | |
| // }; | |
| // | |
| // Where 'attr' is the name of the DOM attribute to set. | |
| // If content is true then the attribute is treated as a Content attribute. | |
| // Not specifying 'content' is the same as specifying content: false. | |
| // | |
| // The value of 'to' can also be a function that takes a single argument of the state | |
| // value at the given statePath. The function will be called every time | |
| // value at the given statePath changes. This can be used, for example, to expand | |
| // templates based on the updated state. | |
| // | |
| // from - [Optional] Defines how the elements value maps to the state. Can be | |
| // an object of the form: | |
| // | |
| // { | |
| // event: "click", | |
| // proc: e => e.target.dataset.foo, | |
| // filter: e => e.target.tagName == "LI", | |
| // } | |
| // | |
| // Where 'event' is the name of the event to register for. When the | |
| // event is fired 'proc' will extract the value to store in the state | |
| // at statePath. The 'filter' is optional and can be used to filter out | |
| // unwanted events. | |
| // | |
| // If the 'listener' is set on the object then its value, which should | |
| // be a function, is added as an event handler to the selected | |
| // elements. The function should update the store's state when called. | |
| // | |
| add(statePath, query, to, from) { | |
| let parsedPath = statePath.split("."); | |
| if (statePath === "") { | |
| parsedPath = []; | |
| } | |
| let pathLeadSegments = statePath.split("."); | |
| let finalPathSegment = pathLeadSegments.pop(); | |
| let elements = [].slice.call(this.ele.querySelectorAll(query)); | |
| let toFunc = undefined; | |
| if (typeof to === "object") { | |
| if (to.content) { | |
| toFunc = state => elements.forEach(ele => ele.setAttribute(to.attr, state)); | |
| } else { | |
| toFunc = state => elements.forEach(ele => ele[to.attr] = state); | |
| } | |
| } else { | |
| toFunc = to; | |
| } | |
| this.to.push({ | |
| func: toFunc, | |
| parsedPath: parsedPath, | |
| }); | |
| if (!from) { | |
| return | |
| } | |
| if (from.proc) { | |
| elements.forEach(ele => ele.addEventListener(from.event, e => { | |
| if (from.filter && !from.filter(e)) { | |
| return | |
| } | |
| let state = dup(this.store.getState()); | |
| pathLeadSegments.forEach(segment => { state = state[segment]; }); | |
| state[finalPathSegment] = from.proc(e); | |
| this.dispatch(state); | |
| })); | |
| } else if (from.listener) { | |
| elements.forEach(ele => ele.addEventListener(from.event, from.listener)); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment