Skip to content

Commit

Permalink
Genotype preservation (#145)
Browse files Browse the repository at this point in the history
* HTMLElement.prototype.$snapshot()

* functions should not be tied to phenotype. Nucleus handles them

* Proper HTMLElement.prototype event handler handling

* Lint fix

* handle snapshot for $init

* Lint fix

* Safari bug - Object.hasOwnProperty(HTMLElement.prototype, “style”) is false
  • Loading branch information
gliechtenstein authored Jul 24, 2017
1 parent 3848952 commit b67cfa4
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 13 deletions.
1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@
"computed-property-spacing": "error",
"eol-last": "error",
"func-name-matching": "error",
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"indent": ["error", 2, { "SwitchCase": 1 }],
"key-spacing": "error",
"keyword-spacing": ["error"],
Expand Down
32 changes: 25 additions & 7 deletions cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
if (['$init'].indexOf(key) === -1) {
$node.Genotype[key] = Nucleus.bind($node, val);
} else {
val.snapshot = val; // snapshot of $init
$node.Genotype[key] = val;
}
},
Expand Down Expand Up @@ -198,6 +199,9 @@
Phenotype.$init($node);
},
multiline: function(fn) { return /\/\*!?(?:@preserve)?[ \t]*(?:\r\n|\n)([\s\S]*?)(?:\r\n|\n)[ \t]*\*\//.exec(fn.toString())[1]; },
get: function(key) {
return Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key) || Object.getOwnPropertyDescriptor($root.Element.prototype, key);
},
set: function($node, key, val) {
if (key[0] === '$') {
if (key === '$type') {
Expand All @@ -223,12 +227,14 @@
} else if (key === 'value') {
$node[key] = val;
} else if (key === 'style' && typeof val === 'object') {
var CSSStyleDeclaration = Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).get.call($node);
var CSSStyleDeclaration = Phenotype.get(key).get.call($node);
for (var attr in val) { CSSStyleDeclaration[attr] = val[attr]; }
} else if (typeof val === 'number' || typeof val === 'string' || typeof val === 'boolean') {
if ($node.setAttribute) $node.setAttribute(key, val);
} else if (typeof val === 'function') {
$node[key] = val;
// For natively supported HTMLElement.prototype methods such as onclick()
var prop = Phenotype.get(key);
if (prop) prop.set.call($node, val);
}
},
$type: function(model, namespace) {
Expand Down Expand Up @@ -349,7 +355,7 @@
// The "value" attribute needs a special treatment.
return Object.getOwnPropertyDescriptor(Object.getPrototypeOf($node), key).get.call($node);
} else if (key === 'style') {
return Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).get.call($node);
return Phenotype.get(key).get.call($node);
} else if (key in $node.Genotype) {
// Otherwise utilize Genotype
return $node.Genotype[key];
Expand All @@ -358,7 +364,7 @@
// For example, there are many DOM attributes such as "tagName" that come with the node by default.
// These are not something we directly define on a gene object, but we still need to be able to access them..
// In this case we just use the native HTMLElement.prototype accessor
return Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).get.call($node);
return Phenotype.get(key).get.call($node);
}
}
},
Expand All @@ -382,11 +388,11 @@
if (key === 'value') {
return Object.getOwnPropertyDescriptor(Object.getPrototypeOf($node), key).set.call($node, val);
} else if (key === 'style' && typeof val === 'object') {
Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).set.call($node, val);
Phenotype.get(key).set.call($node, val);
} else if (typeof val === 'number' || typeof val === 'string' || typeof val === 'boolean') {
$node.setAttribute(key, val);
} else if (typeof val === 'function') {
Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).set.call($node, val);
Phenotype.get(key).set.call($node, val);
}
}
},
Expand Down Expand Up @@ -414,7 +420,7 @@
// 1. No difference if the attribute is just a regular variable
// 2. If the attribute is a function, we create a wrapper function that first executes the original function, and then triggers a phenotype update depending on the queue condition
if (typeof v === 'function') {
return function() {
var fun = function() {
// In the following code, everything inside Nucleus.tick.call is executed AFTER the last line--v.apply($node, arguments)--because it gets added to the event loop and waits until the next render cycle.

// 1. Schedule phenotype update by wrapping them in a single tick (requestAnimationFrame)
Expand Down Expand Up @@ -452,6 +458,8 @@
// 2. Run the actual function, which will modify the queue
return v.apply($node, arguments);
};
fun.snapshot = v;
return fun;
} else {
return v;
}
Expand Down Expand Up @@ -522,6 +530,16 @@
$context.DocumentFragment.prototype.$cell = $context.Element.prototype.$cell = function(gene, options) {
return this.$build(gene, [], null, (options && options.namespace) || null, true);
};
$context.DocumentFragment.prototype.$snapshot = $context.Element.prototype.$snapshot = function() {
var json = JSON.stringify(this.Genotype, function(k, v) {
if (typeof v === 'function' && v.snapshot) { return '(' + v.snapshot.toString() + ')'; }
return v;
});
return JSON.parse(json, function(k, v) {
if (typeof v === 'string' && v.indexOf('function') >= 0) { return eval(v); }
return v;
});
};
if ($root.NodeList && !$root.NodeList.prototype.forEach) $root.NodeList.prototype.forEach = Array.prototype.forEach; // NodeList.forEach override polyfill
},
create: function($context) {
Expand Down
21 changes: 16 additions & 5 deletions test/Phenotype.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,24 +533,35 @@ describe("Phenotype", function() {
compare($node.getAttribute("data-done"), "true") // only set to the DOM attribute (as string)
compare($node["data-done"], undefined) // the property should be undefined
})
it("function", function() {
it("function (only native HTMLElement methods are supported)", function() {
const $parent = document.createElement("div");
const $node = document.createElement("div")
$node.Genotype = {}
$node.Meta = {}
$parent.appendChild($node)

// Before
compare($node.getAttribute("fun"), null)
compare($node.fun, undefined)
compare($node.getAttribute("onclick"), null)

Phenotype.set($node, "fun", function(arg) {
spy.O.getOwnPropertyDescriptor.reset();

Phenotype.set($node, "onclick", function(arg) {
return "fun " + arg;
})

// After
compare($node.getAttribute("onclick"), null) // Doesn't exist as a DOM attribute
compare(spy.O.getOwnPropertyDescriptor.callCount, 1); // tries once for HTMLElement and finds onclick so only one time trial.


// NON HTMLElement method set
spy.O.getOwnPropertyDescriptor.reset();
Phenotype.set($node, "fun", function(arg) {
return "fun " + arg;
})
compare($node.getAttribute("fun"), null) // Doesn't exist as a DOM attribute
compare($node.fun("sad"), "fun sad") // Attached as a variable
compare(spy.O.getOwnPropertyDescriptor.callCount, 2); // tries both for HTMLElement and Element, because `fun` doesn't exist.

})
})
})
Expand Down
29 changes: 29 additions & 0 deletions test/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ const spy = require("./spy.js")
const compare = function(actual, expected) {
assert.equal(stringify(actual), stringify(expected));
}
describe("DOM prototype overrides", function() {
var cleanup = require('jsdom-global')()
God.plan(window);
it("$snapshot", function() {
cleanup();
cleanup = require('jsdom-global')()

document.body.innerHTML = "";
window.c = {
$cell: true,
_model: [],
id: "el",
onclick: function(e) { console.log("clicked"); },
_fun: function(message) { return "Fun " + message; }
}
compare(document.body.outerHTML, "<body></body>");
God.create(window);
var fun = document.body.querySelector("#el")._fun;
compare(fun.snapshot.toString(), "function (message) { return \"Fun \" + message; }");

var onclick = document.body.querySelector("#el").Genotype.onclick;
compare(onclick.snapshot.toString(), "function (e) { console.log(\"clicked\"); }");

var snapshot = document.body.querySelector("#el").$snapshot();
compare(snapshot._fun.toString(), "function (message) { return \"Fun \" + message; }");
compare(snapshot.onclick.toString(), "function (e) { console.log(\"clicked\"); }");
})

});
describe("Nucleus", function() {
var cleanup = require('jsdom-global')()
God.plan(window);
Expand Down

0 comments on commit b67cfa4

Please sign in to comment.