mini-js
Version:
Mini Js is a Javascript library/framework which is inspired by vue.js, it supports two way data binding, virtual dom, directives, routing etc.
964 lines (603 loc) • 22.8 kB
JavaScript
var __router__ = false;
var define_property = function (obj, prop, value, def) {
if (value == undefined) {
obj[prop] = def
} else {
obj[prop] = value;
}
}
var init_methods = function (instance, methods) {
var data = instance.$data;
function init_method(name, method) {
data[name] = function () {
// attache data getter and setter to the instance so they can b
instance.$data.get = instance.get.bind(instance);
instance.$data.set = instance.set.bind(instance);
// pass data to method so they can
return method.apply(instance.$data, arguments);
}
}
for (var method in methods) {
init_method(method, methods[method])
}
}
var call_hooks = function (instance, name) {
var hook = instance.$hooks[name];
if (hook !== undefined) {
hook.call(instance);
}
}
var Observer = function (instance) {
// Associated Moon Instance
this.instance = instance;
// Computed Property Cache
this.cache = {};
// Computed Property Setters
this.setters = {};
// Set of events to clear cache when dependencies change
this.clear = {};
// Property Currently Being Observed for Dependencies
this.target = null;
// Dependency Map
this.map = {};
}
var create_element = function (tag, val, props, meta, children) {
return {
type: tag,
val: val,
props: props,
children: children,
meta: meta || default_metadata()
}
}
var TEXT_TYPE = '#text', eventModifiers = {}, components = {};
/**
* Compiles Arguments to a VNode
* @param {String} tag
* @param {Object} attrs
* @param {Object} meta
* @param {Object|String} children
* @return {Object} Object usable in Virtual DOM (VNode)
*/
var m = function (tag, attrs, meta, children) {
var component = null;
if (tag == TEXT_TYPE) {
// Text Node
// Tag => #text
// Attrs => meta
// Meta => val
return create_element(TEXT_TYPE, meta, {attrs: {}}, attrs, []);
} else if ((component = components[tag]) !== undefined) {
//console.log(' found a component in \"m\" function :: ', component);
// can write code for custom compomemts
}
return create_element(tag, "", attrs, meta, children);
// In the end, we have a VNode structure like:
// {
// type: 'h1', <= nodename
// props: {
// attrs: {'id': 'someId'}, <= regular attributes
// dom: {'textContent': 'some text content'} <= only for DOM properties added by directives,
// directives: {'m-mask': ''} <= any directives
// },
// meta: {}, <= metadata used internally
// children: [], <= any child nodes
// }
}
/**
* Renders a Class in Array/Object Form
* @param {Array|Object|String} classNames
* @return {String} renderedClassNames
*/
m.render_class = function (classNames) {
if (typeof classNames === "string") {
// If they are a string, no need for any more processing
return classNames;
}
var renderedClassNames = "";
if (Array.isArray(classNames)) {
// It's an array, so go through them all and generate a string
for (var i = 0; i < classNames.length; i++) {
renderedClassNames += (m.render_class(classNames[i])) + " ";
}
} else if (typeof classNames === "object") {
// It's an object, so to through and render them to a string if the corresponding condition is truthy
for (var className in classNames) {
if (classNames[className]) {
renderedClassNames += className + " ";
}
}
}
// Remove trailing space and return
renderedClassNames = renderedClassNames.slice(0, -1);
return renderedClassNames;
}
/**
* Renders "m-for" Directive Array
* @param {Array|Object|Number} iteratable
* @param {Function} item
*/
m.render_loop = function (iteratable, item) {
var items = null;
if (Array.isArray(iteratable)) {
items = new Array(iteratable.length);
// Iterate through the array
for (var i = 0; i < iteratable.length; i++) {
items[i] = item(iteratable[i], i);
}
} else if (typeof iteratable === "object") {
items = [];
// Iterate through the object
for (var key in iteratable) {
items.push(item(iteratable[key], key));
}
} else if (typeof iteratable === "number") {
items = new Array(iteratable);
// Repeat a certain amount of times
for (var i$1 = 0; i$1 < iteratable; i$1++) {
items[i$1] = item(i$1 + 1, i$1);
}
}
return items;
}
/**
* Renders an Event Modifier
* @param {Number} keyCode
* @param {String} modifier
*/
m.render_event_modifier = function (keyCode, modifier) {
return keyCode === eventModifiers[modifier];
}
function Mini(options) {
if (options === undefined) {
options = {}
}
this.$options = options;
// save/set app name
define_property(this, '$name', options.name, 'root');
var data = options.data;
// save data
if (data == undefined) {
this.$data = {}
} else if (typeof data == 'function') {
this.$data = data();
} else {
this.$data = data;
}
// set render function if there
define_property(this, '$render', options.render, noop);
// set custom hooks
define_property(this, '$hooks', options.hooks, {});
var methods = options.methods;
if (methods !== undefined) {
init_methods(this, methods); // save methods in $data object
}
this.make_reactive(this.$data); // make objects reactive
this.$events = {};
this.$dom = {};
this.$observer = new Observer(this);
this.$destroyed = true;
this.$queued = false;
// computed method can be added here;
//=====================================
//this.init(); // initialize the app.
console.log(' checking for router :: ', __router__);
if(!__router__){
console.log(' no router found');
this.init();
}
}
var append_child = function (node, v_node, parent) {
//console.log(" appending :: ", node, v_node, parent);
parent.appendChild(node);
// can write code for custom component here, if v_node is a custom component;
}
var add_event_listeners = function (node, event_listeners) {
var add_handler = function (type) {
var handle = function (evt) {
var handlers = handle.handlers;
for (var i = 0; i < handlers.length; i++) {
handlers[i](evt);
}
}
handle.handlers = event_listeners[type]; // add handler to v_node
event_listeners[type] = handle;
node.addEventListener(type, handle); // attach event to node
}
for (var type in event_listeners) {
add_handler(type);
}
}
var diff_props = function (node, node_props, v_node, props) { // old_node, old_props, new_node, new_props
//console.log(' diff props 1 :: ', props);
var v_node_props = props.attrs;
for (var v_node_prop_name in v_node_props) {
var v_node_prop_value = v_node_props[v_node_prop_name];
var node_prop_value = node_props[v_node_prop_name];
if ((v_node_prop_value !== undefined && v_node_prop_value !== null && v_node_prop_value !== false) && (node_prop_value == undefined || node_prop_value || false && node_prop_value || null || v_node_prop_value !== node_prop_value)) {
node.setAttribute(v_node_prop_name, v_node_prop_value == true ? '' : v_node_prop_value);
}
}
// Diff Node Props with VNode Props
for (var node_prop_name in node_props) {
var v_node_prop_value$1 = v_node_props[node_prop_name];
if (v_node_prop_value$1 == undefined || v_node_prop_value$1 == false || v_node_prop_value$1 == null) {
node.removeAttribute(node_prop_name);
}
}
var v_node_directives = null; // execute directive
if ((v_node_directives = props.directives) !== undefined) {
for (var directive in v_node_directives) {
var directive_fn = null;
if ((directive_fn = v_node_directives[directive]) !== undefined) {
directive_fn(node, v_node_directives[directive], v_node);
}
}
}
var dom = null; // add/update any dom props
if ((dom = props.dom) !== undefined) {
for (var dom_prop in dom) {
var dom_prop_value = dom[dom_prop];
if (node[dom_prop] !== undefined) {
node[dom_prop] = dom_prop_value;
}
}
}
//console.log(' diff props 2 :: ', props);
}
var diff_event_listeners = function (node, new_event_listeners, old_event_listeners) {
for (var type in new_event_listeners) {
var old_event_listener = old_event_listeners[type];
if (old_event_listener == undefined) {
// if old node dont have new event listner, then remove it from the node
node.removeEventListener(type, old_event_listener); // it takes type of listener and its handler/callback function
} else {
old_event_listeners[type].handler = new_event_listeners[type];
}
}
}
var create_node_from_v_node = function (v_node) {
var type = v_node.type,
meta = v_node.meta,
el = null;
//console.log(' crreate a node: ', v_node)
if (type == '#text') {
el = document.createTextNode(v_node.val)
} else {
var children = v_node.children;
el = document.createElement(type);
var first_child = children[0];
if (children.length == 1 && first_child.type == '#text') {
el.textContent = first_child.val;
first_child.meta.el = el.firstChild;
} else {
for (var i = 0; i < children.length; i++) {
var v_child = children[i];
append_child(create_node_from_v_node(v_child), v_node, el)
}
}
var event_listeners = null; // add all event listeners;
if ((event_listeners = meta.event_listeners) !== undefined) {
add_event_listeners(el, event_listeners);
}
}
// write code for diff here tomorrow
diff_props(el, {}, v_node, v_node.props)
// hydrate
v_node.meta.el = el;
return el;
}
var replace_child = function (old_node, new_node, v_node, parent) {
var component_instance = null;
if ((component_instance = old_node._mini_) !== undefined) {
component_instance.destroy();
}
//console.log(' replacing the child', new_node, old_node);
parent.replaceChild(new_node, old_node);
// check for component;
var component = null;
if ((component = v_node.meta.component) !== undefined) {
create_node_from_v_node(new_node, v_node, component);
}
}
var remove_child = function (node, parent) {
var component_instance = null;
if ((component_instance = node._mini_) !== undefined) {
// Component was unmounted, destroy it here
component_instance.destroy();
}
parent.removeChild(node);
}
/**
* Converts attributes into key-value pairs
* @param {Node} node
* @return {Object} Key-Value pairs of Attributes
*/
var extract_attrs = function (node) {
var attrs = {};
for (var raw_attrs = node.attributes, i = raw_attrs.length; i--;) {
attrs[raw_attrs[i].name] = raw_attrs[i].value;
}
return attrs;
}
var hydrate = function (node, v_node, parent) {
var node_name = node !== null ? node.nodeName.toUpperCase() : null;
var meta = v_node.meta;
if (node_name !== v_node.type) {
var new_node = create_node_from_v_node(v_node);
replace_child(node, new_node, v_node, parent);
return new_node;
} else if (v_node == TEXT_TYPE) { // check if both are text type;
if (node.textContent !== v_node.val) {
node.textContent = v_node.val;
}
meta.el = node;
} else if (meta.component !== undefined) {
// code for component diff goes here;
} else {
meta.el = node; // hydrate
var props = v_node.props;
diff_props(node, extract_attrs(node), v_node, props);
// add event listeners
var event_listeners = null;
if ((event_listeners = meta.event_listeners) !== undefined) {
add_event_listeners(node, event_listeners);
}
// ensure inner HTML wasn't change
var dom_props = props.dom;
if (dom_props == undefined || dom_props.innerHTML == undefined) {
var children = v_node.children,
length = children.length;
var i = 0,
current_child_node = node.firstChild,
v_child = length !== 0 ? children[0] : null,
next_sibling = null;
while (v_child !== null || current_child_node !== null) {
next_sibling = null;
if (current_child_node == undefined) {
append_child(create_node_from_v_node(v_child), v_child, node);
} else {
next_sibling = current_child_node.nextSibling;
if (v_child == null) {
remove_child(current_child_node, node);
} else {
hydrate(current_child_node, v_child, node);
}
}
i++;
v_child = i < length ? children[i] : null;
current_child_node = next_sibling;
}
}
return node;
}
}
/**
* Diffs VNodes, and applies Changes
* @param {Object} oldVNode
* @param {Array} oldChildren
* @param {Object} vnode
* @param {Array} children
* @param {Number} index
* @param {Object} parent
*/
var diff = function (old_v_node, old_children, v_node, children, index, parent) {
var old_meta = old_v_node.meta;
var meta = v_node.meta;
if (old_v_node.type !== v_node.type) {
old_children[index] = v_node;
replace_child(old_meta.el, create_node_from_v_node(v_node), v_node, parent);
} else if (meta.should_render == true) {
//console.log(' didnt match in type in diff :: ', old_v_node, v_node);
if (v_node.type == TEXT_TYPE) {
var val = v_node.val;
if (old_v_node.val !== val) {
old_v_node.val = val;
old_meta.el.textContent = val;
}
} else if (meta.component !== undefined) {
//console.log(' inside diff , i found an component :: ', old_v_node, v_node);
// code for diff component will go here
} else {
var node = old_meta.el;
// diff props
var old_props = old_v_node.props;
var props = v_node.props;
diff_props(node, old_props.attrs, v_node, props);
old_props.attrs = props.attrs;
var event_listeners = null;
if ((event_listeners = meta.event_listeners) !== undefined) {
diff_event_listeners(node, event_listeners, old_meta.event_listeners);
}
// ensure html wasn't changed
var dom_props = props.dom;
if (dom_props == undefined || dom_props.innerHTML == undefined) {
// diff children;
var children$1 = v_node.children,
old_children$1 = old_v_node.children,
old_length = old_children$1.length,
new_length = children$1.length;
if (new_length == 0 && old_length !== 0) {
var first_child = null;
while ((first_child = node.firstChild) !== null) {
remove_child(first_child, node);
}
old_v_node.children = [];
} else if (old_length == 0) {
var child_v_node = null;
for (var i = 0; i < new_length; i++) {
child_v_node = children$1[i];
append_child(create_node_from_v_node(child_v_node), child_v_node, node);
}
old_v_node.children = children$1;
} else {
var total_length = new_length > old_length ? new_length : old_length;
var old_child = null,
child = null;
for (var i$1 = 0; i$1 < total_length; i$1++) {
if (i$1 >= new_length) {
// remove extra child
remove_child(old_children$1.pop().meta.el, node);
} else if (i$1 >= old_length) {
child = children$1[i$1];
append_child(create_node_from_v_node(child), child, node);
old_children$1.push(child);
} else {
// if both child don't have same reference then diff them
old_child = old_children$1[i$1];
child = children$1[i$1];
if (old_child !== child) {
diff(old_child, old_children$1, child, children$1, i$1, node);
}
}
}
}
}
//console.log(' i am done here ', v_node);
}
}
}
var hash_RE = /\[(\w+)\]/g; // replace "[", "]" with . ;
var resolve_key_path = function (instance, data, key, value) {
//console.log(' finding key path :: ', key, value, data);
key = key.replace(hash_RE, "$1");
var path = key.split('.'), temp_data = data;
var i = 0;
for (i; i < path.length - 1; i++) {
var prop_name = path[i];
data = data[prop_name];
}
//console.log(data[path[i]]);
data[path[i]] = value;
// new key may be getting added to the object so make the new key reactive
instance.make_reactive(data, value);
return path[0];
}
var queue_build = function (instance) {
if (instance.$queued === false || instance.$destroyed == false) {
instance.$queued = true;
setTimeout(function () {
instance.build();
call_hooks(instance, 'updated');
instance.$queued = false;
}, 0)
}
}
Mini.prototype.get = function (key) {
return this.$data[key];
}
Mini.prototype.set = function (key, val) {
var observer = this.$observer;
var base = resolve_key_path(this, this.$data, key, val);
//console.log(' setting the data :: ', base, this.$data);
// ******** code for components ******//
// Invoke custom setter
// var setter = null;
// if((setter = observer.setters[base]) !== undefined) {
// setter.call(this, val);
// }
//
// // Notify observer of change
// observer.notify(base, val);
// ***** ends here ********* //
queue_build(this);
}
Mini.prototype.call_method = function (method, args) {
// Get arguments
args = args || [];
args.push(this.$data);
// Call method in context of instance
return this.$data[method].apply(this, args);
}
Mini.prototype.off = function (eventName, handler) {
if (eventName === undefined) {
// No event name provided, remove all events
this.$events = {};
} else if (handler === undefined) {
// No handler provided, remove all handlers for the event name
this.$events[eventName] = [];
} else {
// Get handlers from event name
var handlers = this.$events[eventName];
// Get index of the handler to remove
var index = handlers.indexOf(handler);
// Remove the handler
handlers.splice(index, 1);
}
}
Mini.prototype.destroy = function () {
// Remove event listeners
this.off();
// Remove reference to element
this.$el = null;
// Setup destroyed state
this.$destroyed = true;
// Call destroyed hook
call_hooks(this, 'destroyed');
}
Mini.prototype.render = function () {
return this.$render(m);
}
Mini.prototype.patch = function (old, v_node, parent) {
if (old.meta !== undefined) { // check if v_node is not a v_node
//console.log(' not old : ', old, v_node);
if (old.type !== v_node.type) {
var new_root = create_node_from_v_node(v_node);
replace_child(old.meta.el, new_root, v_node, parent);
// update bounded instance
new_root._mini_ = this;
this.$el = new_root;
} else {
diff(old, [], v_node, [], 0, parent);
}
} else if (old instanceof Node) { // check old is instance of dom's Node
//console.log(' is old ', old);
var new_node = hydrate(old, v_node, parent);
if (new_node !== old) {
this.$el = v_node.meta.el;
this.$el._mini_ = this;
}
}
}
Mini.prototype.build = function () {
var dom = this.render(); // get new virtual DOM
var old = null; // old items to patch
console.log('this.$dom.meta :: ', this.$dom.meta);
if (this.$dom.meta !== undefined) { // if dom not destroyed
old = this.$dom;
} else {
old = this.$el;
this.$dom = dom;
}
console.log(' initial dom ins :: ', dom,old);
this.patch(old, dom, this.$el.parentNode)
}
Mini.compile = function (template) {
var tokens = lexical_analysis(template);
var ast = parser(tokens);
return generator(ast);
}
Mini.prototype.mount = function (el) {
this.$el = typeof el == 'string' ? document.querySelector(el) : el; // get dom element
this.$destroyed = false;
if (this.$el == null) {
//console.log(` Cannot find element "${el}"`);
}
this.$el._mini_ = this; // sync element and mini instance
console.log(' checking for template: ', this.$options.template, this.$el.outerHTML, this.$render);
define_property(this, '$template', this.$options.template, this.$el.outerHTML); // if template is given the use it else use html inside the dom element
if (this.$render === noop) {
this.$render = Mini.compile(this.$template);
}
console.log(' template is : ', this.$template);
this.build(); // run build first
call_hooks(this, 'mounted');
}
Mini.prototype.init = function () {
//console.log(' calling hooks');
call_hooks(this);
var el = this.$options.el;
if (el !== undefined) {
this.mount(el);
}
}