Skip to content

Instantly share code, notes, and snippets.

@plugnburn
Last active January 14, 2021 09:38
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save plugnburn/295342f92883ff562084 to your computer and use it in GitHub Desktop.
Save plugnburn/295342f92883ff562084 to your computer and use it in GitHub Desktop.
Zen.js - reactive nano-framework in 50 lines of JS

Zen.js

It seems that perfection is attained not when there is nothing more to add, but when there is nothing more to take away.

Antoine de Saint Exupéry

Zen.js is tiny attempt to create the Zen of reactive in-memory storage on the JS client-side environment.

How to obtain

Just download the library here or paste this into your HTML:

<script src="//cdn.rawgit.com/plugnburn/295342f92883ff562084/raw/2b7eb5b62140fa5262e905099ac6e3e7185f434a/zen.min.js"></script>

Usage

Basically, Zen.js is a plain JS in-memory object with some additional methods. So you can write or read properties as usual:

Zen.foo = 'bar'
console.log(Zen.foo) // => "bar"

You can import any existing object into Zen namespace by calling Zen.import:

var someObj = {a: 1, b: 2}
Zen.foo = 'bar'
Zen.import(someObj)
console.log(Zen.a, Zen.b, Zen.foo) // => 1 2 "bar"

Additionally, if you pass Zen to any function that requires a string, it will be auto-serialized to JSON in the sorted form:

console.log('' + Zen) // => {"a":1, "b":2, "foo":"bar"}

But the real magic comes when you start defining tracking functions. You can do this by just passing a callback to Zen, and its entire namespace becomes available as this:

Zen(function(){
	console.log('Something changed, let\'s check foo variable:', this.foo)
})

All registered trackers run in a single polling loop when something is changed in Zen namespace. By default, the polling loop checks the changes with an interval of 100 milliseconds. You can override this by calling Zen.interval method:

Zen.interval(1000) // perform checks each second instead of 0.1 s

Last but not least, Zen.js has a req method which allows you to make some HTTP requests (GET if you don't pass the third parameter, and POST if you do) and automatically save the result under some key in Zen namespace:

//...track the values...

Zen(function(){
	if(Zen.remoteData)
		console.log('GET result:', Zen.remoteData)
	if(Zen.apiResult)
		console.log('POST result:', Zen.apiResult)
})

//...and make actual requests at some point

Zen.req('remoteData', '//example.com/data.json') //example of a GET request
Zen.req('apiResult', '//example.com/api.json', JSON.stringify({foo:'bar'})) //example of a POST request

Advanced usage

That's really everything the library has to offer, but with this functionality everything else becomes a breeze.

Persistent storage

We can use auto-serializing and import features to make the entire Zen namespace get stored persistently:

Zen.import(localStorage.getItem('Zen'))
Zen(function(){
	localStorage.setItem('Zen', this)
})

If you want to save only a part under some key myKey, it's just as easy, except you'll have to do the marshaling manually:

Zen.myKey = JSON.parse(localStorage.getItem('myPhysicalKey'))
Zen(function(){
	localStorage.setItem('myPhysicalKey', JSON.stringify(this.myKey))
})

Data binding

Unlike some bloated (or not so bloated but still bigger) frameworks, Zen.js doesn't come with built-in DOM-to-data binding capabilities. However, they are pretty easy to create, especially using a ready DOM manipulation library (such as equally tiny Q.js, for instance).

Data-to-DOM binding example:

RateIndicator = Q('#usd-in-uah')()
Zen(function(){
	if(Zen.usdInUahRate)
		RateIndicator.val(Zen.usdInUahRate)
})

DOM-to-data binding example:

Zen.formData = {}
Q('[data-form-field]')().on('input', function(){
	Zen.formData[this.dataset.formField] = this.value
})
//track form data changes
Zen(function(){
	if(Object.keys(Zen.formData).length) {
	// ... do something here
	}
})

These examples are pretty trivial but the capabilities are obviously much bigger.

Zen.js v2

This is an alternative Zen.js implementation that introduces Zen.sub method instead of tracking the entire namespace, so it lacks Zen.interval method as unnecessary but is much more resource-efficient. With Zen.js v2, you track the changes as follows:

Zen.sub('someKey anotherKey', function(propName, oldValue, newValue){
	console.log('Property', propName, 'was changed from', oldValue, 'to', newValue)
})

As you can see, you can pass several (space-separated) property names to monitor in Zen storage, and on each change three values are passed into the subscriber callback: property name, old property value and new property value.

If you need to subscribe to all already existing properties, you can simulate old behaviour by running something like this:

Zen.sub(Object.keys(Zen).join(' '), function(propName, oldValue, newValue){ ... })

Other than that, Zen.js v2 has the same capabilities as the first version.

!function(w) {
var J=JSON, trackInterval = 100, trackerFunctions = [], tfCount = 0, shadowZen = {},
objSerialize = function (obj) {
for(var keys = Object.keys(obj).sort(), res = {}, k;k=keys.shift();) res[k] = obj[k]
return J.stringify(res)
}, poller = function(k, shadowZenSerialized) {
if((shadowZenSerialized = objSerialize(shadowZen)) !== ''+zen) {
shadowZen = J.parse(shadowZenSerialized)
for(k=0;k<tfCount;k++) {
try {trackerFunctions[k].call(zen)}
catch(e) {}
}
}
setTimeout(poller, trackInterval)
}, zen = function(cb) {
trackerFunctions.push(cb)
if(!tfCount) poller()
tfCount++
return this
}
Object.defineProperties(zen, {
interval : {value: function(interval) {
trackInterval=interval
return this
}},
'import' : {value: function(obj, k) {
if(''+obj === obj)
try{obj=J.parse(obj)} catch(e) {}
if(obj)
for(k in obj)
if(obj.hasOwnProperty(k))
zen[k] = obj[k]
return this
}},
toString: {value: function(){
return objSerialize(zen)
}},
req: {value: function(key, url, data, xhr) {
xhr = new XMLHttpRequest
xhr.open(data ? 'POST' : 'GET', url, true)
xhr.onload = function(res) {
res = xhr.responseText
try {res = J.parse(res)} catch(e) {}
zen[key] = res
}
xhr.send(data)
}}
})
w.Zen = zen
}(this)
!function(p){var f=JSON,h=100,k=[],g=0,l={},m=function(a){for(var b=Object.keys(a).sort(),c={},d;d=b.shift();)c[d]=a[d];return f.stringify(c)},n=function(a,b){if((b=m(l))!==""+c)for(l=f.parse(b),a=0;a<g;a++)try{k[a].call(c)}catch(e){}setTimeout(n,h)},c=function(a){k.push(a);g||n();g++;return this};Object.defineProperties(c,{interval:{value:function(a){h=a;return this}},"import":{value:function(a,b){if(""+a===a)try{a=f.parse(a)}catch(e){}if(a)for(b in a)a.hasOwnProperty(b)&&(c[b]=a[b]);return this}},toString:{value:function(){return m(c)}},req:{value:function(a,b,e,d){d=new XMLHttpRequest;d.open(e?"POST":"GET",b,!0);d.onload=function(b){b=d.responseText;try{b=f.parse(b)}catch(e){}c[a]=b};d.send(e)}}});p.Zen=c}(this)
!function(w) {
var J = JSON, dp = Object.defineProperties, subbers = {}
dp(w.Zen = {}, {
sub: {value: function(propSet, cb, singleProp) {
for(propSet = propSet.split(' ');singleProp = propSet.shift();) {
!function(prop){
var oldval = this[prop], newval = oldval, o = {}
if(subbers[prop]) subbers[prop].push(cb)
else {
subbers[prop] = [cb]
o[prop] = {
get: function(){return newval},
set: function(val, s, i) {
oldval = newval, newval = val
for(s = subbers[prop],i = s.length;s[--i];) s[i].call(this, prop, oldval, newval)
return val
},
configurable: true
}
if(delete Zen[prop]) dp(Zen, o)
}
}(singleProp)
}
}},
'import' : {value: function(obj, k) {
if(''+obj === obj) try{obj=J.parse(obj)} catch(e) {}
if(obj)
for(k in obj)
if(obj.hasOwnProperty(k)) this[k] = obj[k]
return this
}},
toString: {value: function(){return J.stringify(this)}},
req: {value: function(key, url, data, xhr) {
xhr = new XMLHttpRequest
xhr.open(data ? 'POST' : 'GET', url, true)
xhr.onload = function(res) {
res = xhr.responseText
try {res = J.parse(res)} catch(e) {}
this[key] = res
}
xhr.send(data)
}}
})
}(window)
!function(l){var d=JSON,k=Object.defineProperties,f={};k(l.Zen={},{sub:{value:function(a,b,c){for(a=a.split(" ");c=a.shift();)!function(a){var g=this[a],c=g,d={};f[a]?f[a].push(b):(f[a]=[b],d[a]={get:function(){return c},set:function(b,d,h){g=c;c=b;d=f[a];for(h=d.length;d[--h];)d[h].call(this,a,g,c);return b},configurable:!0},delete Zen[a]&&k(Zen,d))}(c)}},"import":{value:function(a,b){if(""+a===a)try{a=d.parse(a)}catch(c){}if(a)for(b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);return this}},toString:{value:function(){return d.stringify(this)}},req:{value:function(a,b,c,e){e=new XMLHttpRequest;e.open(c?"POST":"GET",b,!0);e.onload=function(b){b=e.responseText;try{b=d.parse(b)}catch(c){}this[a]=b};e.send(c)}}})}(window)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment