Skip to content

Commit

Permalink
Merge a59e0ac into 818f783
Browse files Browse the repository at this point in the history
  • Loading branch information
zgsrc authored Feb 28, 2018
2 parents 818f783 + a59e0ac commit 011655c
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 37 deletions.
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,20 @@ obj.a = 2
console.log(obj.sum) // -> 4
```

#### Reactive Classes

Ground up support for reactive class models using mixins

```javascript
class SomeClass extends Computed(Observable(Object)) {
constructor() {
super()

this.value = 10 // writes to observed object automagically
}
}
```

#### React store

Hyperactiv contains built-in helpers to easily create a reactive store which re-renders your React components.
Expand Down Expand Up @@ -424,7 +438,8 @@ const handler = function(keys, value) {
}

// The deep flag ensures that the handler will be triggered when the mutation happens in a nested array/object
const observer = observe(object, { handler, deep: true })
const observer = observe(object, { bubble: true, deep: true })
observer.__handler = handler
object.a.b[0].c = 'value'

// The handler is triggered after each mutation
Expand All @@ -447,7 +462,7 @@ observe(Object | Array, {
batch: boolean,
deep: boolean,
bind: boolean,
handler: function
bubble: boolean
}) => Proxy
```

Expand All @@ -471,11 +486,9 @@ Observe nested objects and when setting new properties.

- `bind: boolean`

Automatically bind methods to the observed object.
- `handler: Function(ancestry: String[], value: Object, originalObject: Object)`
Bubble mutations up the object hierarchy, triggering handlers along the way.

Callback performed whenever the observed object is mutated.
- `bubble: boolean`

### computed

Expand Down Expand Up @@ -509,7 +522,14 @@ dispose(Function) => void

### handlers

Helper handlers used to perform various tasks whenever an observed object is mutated.
When bubble is set, you can "wire tap" any observed object by assigning a callback to the `__handler` property

```javascript
const observer = observe(object, { bubble: true, deep: true })
observer.__handler = (keys, value, oldValue, observedObject) => { }
```

Helper handlers can be used to perform various tasks whenever an observed object is mutated.

Note that handlers are written separately from the main hyperactiv codebase and need to be imported from a separate path.

Expand Down
4 changes: 2 additions & 2 deletions dist/hyperactiv.map.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion react/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 86 additions & 16 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ const observersMap = new WeakMap()

/* Tools */

const isObj = function(o) { return o && typeof o === 'object' }
const isObj = function(o) { return o && typeof o === 'object' && !(o instanceof Date) }
const isArray = Array.isArray
const defineBubblingProperties = function(object, key, parent) {
const defineBubblingProperties = function(object, key, parent, deep) {
Object.defineProperty(object, '__key', { value: key, enumerable: false, configurable: true })
Object.defineProperty(object, '__parent', { value: parent, enumerable: false, configurable: true })
deep && Object.entries(object).forEach(function([key, val]) {
if(isObj(val) && (!val.__key || !val.__parent)) defineBubblingProperties(object[key], key, object)
})
}

const batcher = {
Expand Down Expand Up @@ -61,7 +64,7 @@ const dispose = function(_) { return _.__disposed = true }

const observe = function(obj, options = {}) {
const {
props = null, ignore = null, batch = false, deep = false, handler = null, bind = false
props = null, ignore = null, batch = false, deep = false, bubble = null, bind = false
} = options

// Ignore if the object is already observed
Expand All @@ -79,8 +82,8 @@ const observe = function(obj, options = {}) {
deep && Object.entries(obj).forEach(function([key, val]) {
if(isObj(val)) {
obj[key] = observe(val, options)
// If a handler is set, we add keys to the object used to bubble up the mutation
if(handler)
// If bubble is set, we add keys to the object used to bubble up the mutation
if(bubble)
defineBubblingProperties(obj[key], key, obj)
}
})
Expand All @@ -105,24 +108,39 @@ const observe = function(obj, options = {}) {
return obj[prop]
},
set(_, prop, value) {
const propertiesMap = observersMap.get(obj)
// Don't track bubble handlers
if(prop === '__handler') {
Object.defineProperty(obj, '__handler', { value: value, enumerable: false, configurable: true })
return true
}

const propertiesMap = observersMap.get(obj)
// If the new/old value are equal, return
if((!isArray(obj) || prop !== 'length') && obj[prop] === value) return true
// Remove bubbling infrastructure and pass old value to handlers
const oldValue = obj[prop]
if(isObj(oldValue)) {
delete oldValue.__key
delete oldValue.__parent
}

// If the deep flag is set we observe the newly set value
obj[prop] = deep && isObj(value) ? observe(value, options) : value
// If we defined a handler, we define the bubbling keys recursively on the new value
handler && deep && isObj(value) && defineBubblingProperties(obj[prop], prop, obj)

if(handler) {
// Retrieve the mutated properties chain & call the handler
// If assigning to an object participating (wittingly or unwittingly) in bubbling, define the bubbling keys recursively on the new value
if(obj.__handler || obj.__parent) {
deep && isObj(value) && defineBubblingProperties(obj[prop], prop, obj, deep)

// Retrieve the mutated properties chain & call any __handlers along the way
const ancestry = [ prop ]
let parent = obj
while(parent.__key && parent.__parent) {
ancestry.unshift(parent.__key)
parent = parent.__parent
while(parent) {
if(parent.__handler && parent.__handler(ancestry, value, oldValue, proxy) === false) break
if(parent.__key && parent.__parent) {
ancestry.unshift(parent.__key)
parent = parent.__parent
} else parent = null
}
handler(ancestry, value, proxy)
}

// If the prop is watched
Expand All @@ -143,23 +161,75 @@ const observe = function(obj, options = {}) {
}
}
return true
},
deleteProperty(_, prop) {
// Prevent bubbling mutations from stray objects
const value = _[prop]
if(isObj(value)) {
delete value.__key
delete value.__parent
}
delete _[prop]
return true
}
})

if(bind) {
// Need this for binding es6 classes methods which are stored in the object prototype
const methods = [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertyNames(Object.getPrototypeOf(obj))
...Object.getPrototypeOf(obj) && ['String', 'Number', 'Object', 'Array', 'Boolean', 'Date'].indexOf(Object.getPrototypeOf(obj).constructor.name) < 0 ? Object.getOwnPropertyNames(Object.getPrototypeOf(obj)) : []
].filter(prop => prop != 'constructor' && typeof obj[prop] === 'function')
methods.forEach(key => obj[key] = obj[key].bind(proxy))
}

return proxy
}

/* Observable */

const Observable = Base => class extends Base {
constructor(data, options) {
super()
const store = observe(data || { }, options || { deep: true, batch: true })
return new Proxy(this, {
set: (obj, name, value) => {
if(typeof value === 'function') {
this[name] = value
} else {
store[name] = value
if(this[name] === undefined) Object.defineProperty(this, name, { get: () => store[name], enumerable: true, configurable: true })
}
return true
},
deleteProperty: (obj, name) => {
delete store[name]
delete obj[name]
return true
}
})
}
}

/* Computable */

const Computable = Base => class extends Base {
constructor() {
super()
Object.defineProperty(this, '__computed', { value: [ ], enumerable: false })
}
computed(fn) {
this.__computed.push(computed(fn))
}
dispose() {
while(this.__computed.length) dispose(this.__computed.pop())
}
}

export default {
observe,
computed,
dispose
dispose,
Observable,
Computable
}
Loading

0 comments on commit 011655c

Please sign in to comment.