Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Genotype preservation #145

Merged
merged 7 commits into from
Jul 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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