Say I have a class in JavaScript (yes bad bad class, bad class in JS, but its for web components, one has to use classes).
I would like to create a cached attribute getter for elements on a class, in python it would be this:
class Foo(object):
_elements = {}
def __getattr__(self, name):
if name in ['widget1', 'widget2', 'widget3']: # A long list of items, don't want to create getters for each one individiually
if not _elements.get(name):
self._elements[name] = self.getElementById(name)
return self._elements[name]
else:
# Default behaviour
return object.__getattr__(self, name)
This is the closest I got, but its ugly to use:
One must call it as this.el['widget1']
Instead of this.widget1
class Foo extends HTMLElement {
el(id) {
// Cached get element by id
this._els = this._els || {};
if (!this._els[id]) {
this._els[id] = this.getElementById(id)
}
return this._els[id]
}
getElementById is slower
But does your cached performance gain outway extra code, code complexity and time coding?
1 PICO second equals 0.00000001 Milliseconds
<div style="display:grid;grid-template-columns:1fr 1fr">
<test-component id="CACHED"></test-component>
<test-component id="BYID"></test-component>
</div>
<div id="DIFF"><b>100.000 calls per run:</b></div>
<script>
customElements.define("test-component", class extends HTMLElement {
el(id) {
this._els = this._els || {};
if (!this._els[id]) this._els[id] = document.getElementById(id);
return this._els[id]
}
get CACHED() {
return this.el("CACHED");
}
get BYID() {
return document.getElementById("BYID");
}
connectedCallback() {
let count = 100000;
for (let run = 1; run < 9; run++) {
let log = [];
for (let cnt = 0; cnt < count; cnt++) {
const t0 = performance.now();
this == CACHED ? this.CACHED : this.BYID;// test performance
log.push(performance.now() - t0);
}
this.average = (log.reduce((a, b) => a + b) / log.length)*1e9;
let diff = (BYID.average - CACHED.average).toPrecision(2);
if (this == BYID) DIFF.innerHTML += `<div>Run #${run} = <b>${diff/count}</b> PICO seconds slower per call<div>`;
}
}
})
</script>
For anyone else who has the same issue, this is what I went with, quite a lot of code upfront, but at least its easy to use, e.g. this.PROGRESS
class Foo extends HTMLElement {
el(id, docEl=false) {
// Cached get element by name
// Parameter: docEl
// Set to true to search the document instead of the shadowRoot
this._els = this._els || {};
if (!this._els[id]) {
const searchNode = docEl ? document : this.shadowRoot;
this._els[id] = searchNode.getElementById(id)
}
return this._els[id]
}
// Document Elements
get TEMPLATE() { return this.el('TEMPLATE_wc-uploader', true) }
get TEMPLATE_WC_UPLOAD(){ return this.el('TEMPLATE_wc-upload', true) }
// Shadow Root Elements
get PROGRESS() { return this.el('PROGRESS') }
get PROGRESS_HEADING() { return this.el('PROGRESS_HEADING') }
get DRAG_OVERLAY() { return this.el('DRAG_OVERLAY') }
get UPLOAD_LIST() { return this.el('UPLOAD_LIST') }
Related
I have the following code.
class Node {
constructor(value, parent, possibleChildren = []) {
this.value = value;
this.parent = parent;
this.children = []
this.setChildren(possibleChildren);
}
setChildren(possibleChildren) {
if (possibleChildren.length === 0) return [];
while (possibleChildren.length > 0) {
const value = possibleChildren.pop();
// keyword *this* messes up the context. Save them function calls for lazy execution
let childNode = () => new Node(value, this, possibleChildren);
this.children.push(childNode);
}
this.children = this.children.map(child => child())
}
getChildrenValues() {
return this.children.map((child) => child.value);
}
}
In the above the this.children variable is set properly. If I save the this.children array directly, without wrapping it in a function, I see incorrect children being set.
Example:
setChildren(possibleChildren) {
if (possibleChildren.length === 0) return [];
while (possibleChildren.length > 0) {
const value = possibleChildren.pop();
// keyword *this* messes up the context. Save them function calls for lazy execution
let childNode = new Node(value, this, possibleChildren);
this.children.push(childNode);
}
}
I know that the context of this is not consitent without the function wrapper. What I do not understand is why. Any ideas?
Calling getChildrenValues on the first example returns ["A", "B", "C"].
Calling getChildrenValues on the second example returns ["C"]
class Node {
constructor(value, parent, possibleChildren = []) {
this.value = value;
this.parent = parent;
this.children = []
this.setChildren(possibleChildren);
}
setChildren(possibleChildren) {
if (possibleChildren.length === 0) return [];
while (possibleChildren.length > 0) {
const value = possibleChildren.pop();
// keyword *this* messes up the context. Save them function calls for lazy execution
const childNode = new Node(value, this, possibleChildren);
this.children.push(childNode);
}
}
getChildrenValues() {
return this.children.map((child) => child.value);
}
}
let root = new Node(null, null, "ABC".split(""));
console.log(root.getChildrenValues())
I know that the context of this is not consistent without the function wrapper. What I do not understand is why?
This has nothing to do with the this keyword. Since you used an arrow function, it does refer to exactly the same object in both your code snippets, there is no difference.
The reason why you get different results from the two snippets is the lazy execution, but not with respect to this, but rather the possibleChildren array. In your first code, the while (possibleChildren.length > 0) runs and empties the possibleChildren array before you do the recursive new Node calls. In the second example, you call new Node during that loop, and you pass on the reference to the same possibleChildren array, which is being emptied by the recursive call and the loop therefore terminates right after the first iteration.
To fix this, just don't recursively pass the possibleChildren:
class Node {
constructor(value, parent, childrenValues = []) {
this.value = value;
this.parent = parent;
this.children = []
this.setChildrenValues(childrenValues);
}
setChildrenValues(childrenValues) {
for (let i=childrenValues.length; i--; ) {
const value = childrenValues[i];
const childNode = new Node(value, this );
// ^ no third argument, no grandchildren
this.children.push(childNode);
}
}
getChildrenValues() {
return this.children.map((child) => child.value);
}
}
let root = new Node(null, null, "ABC".split(""));
console.log(root.getChildrenValues())
I want to modify all elements that use the same query via a function. (The function is not changed.)
r is read, w is write, and a is write appended to existing content. When using this as get(query).r , .w , .a , I want to make all elements corresponding.
For example, if the class of 3 p tags is html, if you write get(".html").w("hello") in js, all tags with the class of 3 html should be changed to 'hello'.
function get(id){
if (id) {
if (window === this) {
return new get(id);
}
var query = document.querySelectorAll(id)
for(var i = 0; i < query.length; i++) {
this.e = query[i];
return this;
}
} else {
return "getError : The attribute and value of a tag such as id or class specified by get does not exist. ";
}
}
get.prototype = {
r: function () {
return this.e.innerText;
},
w: function (writing) {
return this.e.innerText = writing;
},
a: function (writing) {
return this.e.innerText = this.e.innerText += writing;
}
};
js
<p href="#" class="test">html</p>
test
<script>
get(".test").w("hello")
</script>
For this line
for(var i = 0; i < query.length; i++) {
this.e = query[i];
return this;
}
You are only returning the first element. Thus, it only changes the inner text of the first element.
It seems that you are trying to assign a prototype to HTML elements, which is not recommended, so I modified your code.
I changed it so that the get function will return the whole HTMLCollection instead. Then I used the prototype of the get function to loop through the collection and set the text of the HTML elements.
js
<p href="#" class="test">html</p>
test
<script>
function get(id){
if (id) {
if (window === this) {
return new get(id);
}
var query = document.querySelectorAll(id)
return query
} else {
return "getError : The attribute and value of a tag such as id or class specified by get does not exist. ";
}
}
get.prototype = {
r: function () {
return this.e.innerText;
},
w: function (elems, writing) {
return elems.forEach(x => x.innerText = writing);
},
a: function (writing) {
return this.e.innerText = this.e.innerText += writing;
}
};
var elems = get(".test")
get.prototype.w(elems, "html")
</script>
You can also do this with a newer ES2015 class method, that uses static methods. Static methods are called directly on the class, without creating an instance/object of the class. That way, you can call the static method and pass the class object back into those methods.
More info here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static
https://www.w3schools.com/JsrEF/jsref_class_static.asp
See snippet below:
`use strict`
class Get {
constructor(selector) {
if (selector) return Array.from(document.querySelectorAll(selector));
return [];
}
static Read(elements) {
return elements.map(e => e.innerText)
};
static Write(elements, string) {
elements.forEach(e => e.innerText = string)
};
static Append(elements, string) {
elements.forEach(e => e.innerText = `${e.innerText}${string}`)
};
}
console.log(Get.Read(new Get(`.test`)));
Get.Append(new Get(`.test`), `: Appended Text`);
js
<p href="#" class="test">html</p>
test
If I want to remove/add element on DOM I just use ng-if and the code under it does not compile into to DOM, can I do the same using pure js? I don't want the HTML code inside my js code.
Hiding it using CSS:
<div id = "infoPage" style="display: none;">
Will still insert the element to the DOM.
EDIT
The condition for showing or not is based on a flag like:
var show = false; //or true
You can try something like this:
Idea:
Create an object that holds reference of currentElement and its parent (so you know where to add).
Create a clone of current element as you want to add same element after its removed.
Create a property using Object.defineProperty. This way you can have your own setter and you can observe changes over it.
To toggle element, check
If value is true, you have to add element. But check if same element is already available or not to avoid duplication.
If false, remove element.
var CustomNGIf = function(element, callback, propertyName) {
var _value = null;
// Create copies of elements do that you can store/use it in future
this.parent = element.parentNode;
this.element = element;
this.clone = null;
// Create a property that is supposed to be watched
Object.defineProperty(this, propertyName, {
get: function() {
return _value;
},
set: function(value) {
// If same value is passed, do nothing.
if (_value === value) return;
_value = !!value;
this.handleChange(_value);
}
});
this.handleChange = function(value) {
this.clone = this.element.cloneNode(true);
if (_value) {
var index = Array.from(this.parent.children).indexOf(this.element);
// Check if element is already existing or not.
// This can happen if some code breaks before deleting node.
if (index >= 0) return;
this.element = this.clone.cloneNode(true);
this.parent.appendChild(this.element);
} else {
this.element.remove();
}
// For any special handling
callback && callback();
}
}
var div = document.getElementById('infoPage');
const propName = 'value';
var obj = new CustomNGIf(div, function() {
console.log("change")
}, propName);
var count = 0;
var interval = setInterval(function() {
obj[propName] = count++ % 2;
if (count >= 10) {
window.clearInterval(interval);
}
}, 2000)
<div class='content'>
<div id="infoPage"> test </div>
</div>
In my Javascript there is a parent Base class that will be extended by others.
I'd like to:
define in it a method getSubject() that could be common to all children, when it is not overridden.
make getSubject() rely on a Base property, that eventually could be overridden as well.
always call the getSubject() method in the context of the caller (the children classes or the Base class)
To clarify (hopefully) what I want to do..
I wrote (non-valid) PHP code as an example.
<?php
class Base
{
const SUBJ_SELECTOR = 'input';
public function init()
{
$this->wrapper = ....;
$this->subject = $this->getSubj();
if ($this->subject.attr('data-active')) {
// ... do stuff
}
}
public function getSubj() // One definition in parent
{
return $this->wrapper.find(self::SUBJ_SELECTOR);
}
}
class Select extends Base
{
const SUBJ_SELECTOR = 'select' // Override just the selector
}
class Textarea extends Base
{
const SUBJ_SELECTOR = 'textarea[name=foo]';
public function getSubj() // Eventual overriding
{
$subjs = $this->wrapper.find(self::SUBJ_SELECTOR);
foreach ($subjs as $subj) {
if ($subj.attr('multiline')) {
return $subj;
}
}
return $subjs;
}
}
I'd like to achieve the same result with Javascript (and JQuery eventually).
Actually I wrote some code (that I still didn't test) as a sketch:
var Base = function() {
this.options = {};
this.subject_selector = 'input';
this.wrapper = $('.container');
};
Base.prototype.getSubject = function() {
return this.wrapper.find(this.subject_selector);
}
Base.prototype.init = function() {
subj = this.getSubject();
if(subj.attr('data-active')) {
// ... do stuff
}
}
var Select = function() {
this.subject_selector = 'select';
}
Select.prototype = new Base();
Select.prototype.constructor = Select;
var Textarea = function() {
this.subject_selector = 'textarea';
}
Textarea.prototype.getSubject = function() {
subjs = this.wrapper.find(this.subject_selector);
for (var i = subjs.length - 1; i >= 0; i--) {
if(subjs[i].attr('multiline')) {
return subjs[i];
}
};
return subjs;
}
Textarea.prototype = new Base();
Textarea.prototype.constructor = Textarea;
Would it work correctly? Is this a proper use of the inheritance model?
Am I callling the method in the right way and will I get the expected result when executing the init() method?
According to the mustache RFC
A {{name}} tag in a basic template will try to find the name key in
the current context. If there is no name key, nothing will be
rendered.
I therefore expected this:
var template = '{{#anArray}}{{aString}}{{/anArray}}';
var json = {
"aString":"ABC",
"anArray": [1,{"aString":"DEF"}]
};
To give me once rendered:
"DEF"
However mustache.js looks for values in the parent's scope. Which gives me
"ABCDEF"
Do the context actually means including all the parents scopes ?
http://jsfiddle.net/ZG4zd/20/
Short answer: yes.
A bit longer answer. Context.prototype.lookup does a while loop, looking up a token in current context and it's parent contexts, while there is a parent context.
Relevant bit of code:
Context.prototype.lookup = function (name) {
var value = this._cache[name];
if (!value) {
if (name === ".") {
value = this.view;
} else {
var context = this;
//Iterate ancestor contexts
while (context) {
if (name.indexOf(".") > 0) {
var names = name.split("."), i = 0;
value = context.view;
while (value && i < names.length) {
value = value[names[i++]];
}
} else {
value = context.view[name];
}
if (value != null) {
break;
}
context = context.parent;
}
}
this._cache[name] = value;
}
if (typeof value === "function") {
value = value.call(this.view);
}
return value;
};