Here is the function in question:
/**
* @el [Element] Target HTML Element
* @attr [String] attribute to bind
* @match [Object] Result from regex.exec
*/
const bindValue = (el, attr, match) => {
let binding = getBinding(el, attr);
if (attr === "textContent") {
let fn = new Function(
`scope`,
`with(scope) {
return ${match[1]}
}`,
);
binding.attrs[attr].bindings.push((ctx, values) => {
return ctx.replace(match[0], fn(values));
});
}
};
So, what this is doing is taking a string from an HTML template for a web component, like:
<div>{{name == 'Homer' ? 'No Homers Allowed" : name}}</div>
and translating it into an executable function within the scope of the component’s bound properties and methods.
In order to not re-render the whole template whenever any bound property is set, during the first render I’m tracking which template binding is being rendered, and using getters on the bound properties to keep track of which bindings are calling which properties, so I can call only those bindings again when that property is set.
What that means is that I need the getter functions to run only when they’re called from within the function created by the Function constructor, which means that they cannot be passed as individual variables to the Function constructor, otherwise the getters for the whole scope run, instead of just those referenced in the template string.
As far as I know, the with
statement is the only way to create local variables from either an Object or Array of values other than eval
, which is an even worse way of doing it than by using with
. This seems like probably the only legitimate use of with
, since it’s being used basically to create the scope of a generated function, not replace the scope within an existing function, but it’s still deprecated. I’d like to replace it, if possible, but I can’t figure out a way to do so without sacrificing either the power or simplicity of the template format.
For context, here’s the full code of the web component extension I’m working on which the previous bit is taken from:
class Component extends HTMLElement {
constructor() {
super();
this.__connected__ = false;
// this.__useShadow__ = false;
// Binding cache
this.__bindings__ = [];
this.__observed__ = {};
this.__values__ = {};
this.__children__ = [];
// Binding utilities
this.__current_target__ = null;
}
static get observedAttributes() {
return [];
}
static get observedProperties() {
return [];
}
listen(key, handle, fn) {
if (this.__observed__.hasOwnProperty(key)) {
let observers = this.__observed__[key];
let idx = observers.findIndex((o) => o.el === this && o.attr === handle);
if (idx === -1) {
observers.push({
el: this,
attr: handle,
context: "",
bindings: [
(context, values) => {
return fn(values[key]);
},
],
});
} else {
observers.splice(idx, 1, {
el: this,
attr: handle,
context: "",
bindings: [
(context, values) => {
return fn(values[key]);
},
],
});
}
}
}
unlisten(key, handle, fn) {
if (this.__observed__.hasOwnProperty(key)) {
let observers = this.__observed__[prop];
let idx = observers.findIndex((o) => o.el === this && o.attr === handle);
if (idx !== -1) {
observers.splice(idx, 1);
}
}
}
connectedCallback() {
const name = this.tagName.toLowerCase();
if (!this.__connected__) {
this.__connected__ = true;
// if (this.__useShadow__) {
// if (typeof __template__ !== "undefined") {
// let template = __template__.content.cloneNode(true);
// const shadowRoot = this.attachShadow({ mode: "open" });
// shadowRoot.appendChild(template);
// }
// } else {
this.__children__ = [...this.children];
if (!!ComponentRegistry.templates[name]) {
let template =
ComponentRegistry.templates[name].content.cloneNode(true);
this.__parseTemplate__(template);
this.appendChild(template);
}
// console.log(this, this.constructor.observedProperties);
this.constructor.observedProperties.map((prop) => {
// if (!this.hasOwnProperty(prop)) {
this.__observed__[prop] = [];
var p = {};
p[prop] = {
get: () => {
if (
this.__current_target__ !== null &&
this.__observed__.hasOwnProperty(prop)
) {
let { el, attr, context, bindings } = this.__current_target__;
let observers = this.__observed__[prop];
let idx = observers.findIndex(
(o) => o.el === el && o.attr === attr,
);
if (idx === -1) {
observers.push({ ...this.__current_target__ });
} else {
observers.splice(idx, 1, { ...this.__current_target__ });
}
}
return this.__values__[prop];
},
set: (value) => {
if (value !== this.__values__[prop]) {
this.__values__[prop] = value;
let observers = this.__observed__[prop] || [];
let len = observers.length;
let values = this.__get_values__();
for (var i = 0; i < len; i++) {
let { el, attr, context, bindings } = observers[i];
for (let i = 0; i < bindings.length; i++) {
context = bindings[i](context, values);
}
el[attr.replace(":", "")] = context;
if (!["array", "object"].includes(typeof context)) {
if (!!el.setAttribute) {
el.setAttribute(attr.replace(":", ""), context);
}
}
}
}
},
};
try {
Object.defineProperties(this, p);
} catch (e) {
console.log("Error defining properties", e);
}
// } else {
// console.log("Already exists?", this[prop]);
// }
});
Object.keys(this.__observed__).map((key) => {
if (this.hasAttribute(key)) {
this[key] = this.getAttribute(key);
}
});
this.__render__();
if (!!this.connected) {
this.connected();
}
}
}
__parseTemplate__(template) {
const eventExp = /@([a-z|A-Z]+)/;
const attrExp = /:([a-z|A-Z]+)/;
const boundExp = /{{(.*?)}}/g;
const getBinding = (el, attr) => {
let binding = this.__bindings__.find((b) => b.el === el);
if (!binding) {
binding = {
el: el,
attrs: {},
};
this.__bindings__.push(binding);
}
if (!binding.attrs[attr]) {
let context = el[attr];
if (!!el.getAttribute) {
context = el.getAttribute(attr);
}
binding.attrs[attr] = {
context: context,
bindings: [],
};
}
return binding;
};
const bindValue = (el, attr, match) => {
let binding = getBinding(el, attr);
if (attr === "textContent") {
let fn = new Function(
"scope",
`
with(scope) {
return ${match[1]}
}`,
);
binding.attrs[attr].bindings.push((ctx, values) => {
return ctx.replace(match[0], fn(values));
});
}
};
const bindAttribute = (el, attr, context) => {
let binding = getBinding(el, attr);
let fn = new Function(
"scope",
`
with(scope) {
return ${context}
}`,
);
binding.attrs[attr].bindings.push((ctx, values) => {
return fn(values);
});
};
const bindEvent = (el, attr, context) => {
let fn = new Function(
"scope",
`
with(scope) {
return ${context}
}`,
);
el.addEventListener(attr.replace("@", ""), (e) => {
let values = this.__get_values__();
values["$event"] = e;
fn(values);
});
};
const walk = (el) => {
let attrs = [...(el.attributes || [])];
let children = [...(el.childNodes || [])];
if (el.nodeType === 3) {
let match;
while ((match = boundExp.exec(el.textContent)) !== null) {
bindValue(el, "textContent", match, el.textContent);
}
} else {
for (var i = 0; i < attrs.length; i++) {
if (attrExp.test(attrs[i].name)) {
bindAttribute(el, attrs[i].name, attrs[i].value);
// el.setAttribute(attrs[i].name.replace(":", ""), attrs[i].value);
}
if (eventExp.test(attrs[i].name)) {
bindEvent(el, attrs[i].name, attrs[i].value);
// el.setAttribute(attrs[i].name);
}
}
}
children.map((c) => {
walk(c);
});
};
walk(template);
}
__get_values__() {
let values = {};
let methods = Object.getOwnPropertyNames(
Object.getPrototypeOf(this),
).filter((method) => {
return typeof this[method] === "function";
});
let keys = Object.keys(this.__observed__);
let callable = [...keys, ...methods];
for (let i = 0; i < callable.length; i++) {
if (typeof this[callable[i]] === "function") {
Object.defineProperty(values, callable[i], {
get: () => {
return this[callable[i]].bind(this);
},
});
} else {
Object.defineProperty(values, callable[i], {
get: () => {
return this[callable[i]];
},
});
}
}
return values;
}
__render__() {
let values = this.__get_values__();
for (var i = 0; i < this.__bindings__.length; i++) {
let binding = this.__bindings__[i];
let el = binding.el;
for (let attr in binding.attrs) {
let { context, bindings } = binding.attrs[attr];
this.__current_target__ = { el, attr, context, bindings };
for (let i = 0; i < bindings.length; i++) {
context = bindings[i](context, values);
}
el[attr.replace(":", "")] = context;
if (!["array", "object"].includes(typeof context)) {
if (!!el.setAttribute) {
el.setAttribute(attr.replace(":", ""), context);
}
}
}
}
this.__current_target__ = null;
}
}