I have a class like this:
class Outcome {
constructor(name) {
this.name = name;
this.inProgress = "";
this.success = null;
this.messages = [];
}
addMessage(type, text) {
this.messages.push({
type,
text
});
}
getMessagesByType(type) {
return this.messages.filter((message) => message.type === type);
}
}
In my React component I imported it, and I would like to use it like this:
submit() {
let outcome = new Outcome("submit");
outcome.inProgress = true;
this.setState({
outcome // Save in state so I can show a spinner
});
if (!formsDataValid) {
outcome.inProgress = false;
outcome.success = false;
outcome.addMessage("error", "Data are not valid");
this.setState({
outcome
});
return;
}
fetch().then((response) => {
outcome.inProgress = false;
if (response.ok) {
outcome.success = true;
outcome.addMessage("success", "Operation correctly performed");
} else {
outcome.success = false;
outcome.addMessage("error", response.error);
}
this.setState({
outcome
});
});
}
then in render I can check the result in this way:
render() {
{this.state.outcome?.inProgress ?
"Spinner here"
: this.state.outcome?.messages.length > 0 ?
"Here render the messages"
: null}
<button type="submit" disabled={this.state.outcome?.success || false}>Submit button</button>
}
This should works, but the problem is that in handle submit, when I'm doing for example outcome.success = false; it will edit the state directly, because the object is a reference.
Is there a clean way to do that without edit the state directly? I tried
this.setState({
outcome: { ...outcome }
});
but in this way it will remove the methods of the class in the object it clone in to the state.
I know we should use React Hooks, but the components is an old component and we have no time to change that.
One option would be to create a method that can produce a copy of an Outcome. Essentially when you do {...outcome} you're losing the prototype chain from outcome and only copying its members.
class Outcome {
constructor(name) {
this.name = name;
this.inProgress = "";
this.success = null;
this.messages = [];
}
addMessage(type, text) {
this.messages.push({
type,
text
});
}
getMessagesByType(type) {
return this.messages.filter((message) => message.type === type);
}
clone() {
const result = new Outcome(this.name);
result.inProgress = this.inProgress;
result.success = this.success;
result.messages = this.messages;
return result;
}
}
You could then use it like this:
this.setState({
outcome: outcome.clone();
});
There's also a generic way to do this that'll work for (most) classful objects.
function cloneInstance(obj) {
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
}
If you want to be able to easily mutate the result too for more of a functional style, I'd probably use something like this:
function immutableModify(obj, cb = o => o) {
const result = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
cb(result);
return result;
}
which can then be used like
this.setState({
outcome: immutableModify(outcome, outcome => outcome.name = "test")
});
I have a class of validations that I have created in JS:
let test = new Validator(req.body);
Now I want to test something, maybe that a specific key in this object is 2-5 char length, I would do it like this:
let myBoolean = test.selector("firstName").minLength(2).maxLength(5);
// firstName is like: req.body.firstName
And how this could be done in the class?
EDIT
I made something like this:
audit.isLength({selector: "from", gte: 2, lte: 35})
class Validator {
constructor(obj) {
this.obj = obj;
this.isValid = true;
}
isExists(sel) {
if (typeof this.obj[sel] === "undefined") return false;
return true;
}
isLength(info) {
let sel = this.obj[info.selector];
if (typeof sel === "undefined") return false;
if (info.gte) {
if (sel.length<info.gte) return false;
}
if (info.lte) {
if (sel.length>info.lte) return false;
}
if (info.gt) {
if (sel.length<=info.gt) return false;
}
if (info.lt) {
if (sel.length>=info.lt) return false;
}
return true;
}
}
Try something like this - assign the object to validate to a property on the instantiation, return this from each validating call, and when validating, assign to an isValid property on the object (if it isn't already false). Note that you need to access the isValid property finally in order to retrieve the boolean.
class Validator {
constructor(obj) {
this.obj = obj;
this.isValid = true;
}
selector(sel) {
this.sel = sel;
return this;
}
minLength(min) {
if (this.isValid) this.isValid = this.obj[this.sel].length >= min;
return this;
}
maxLength(max) {
if (this.isValid) this.isValid = this.obj[this.sel].length <= max;
return this;
}
}
const test = new Validator({firstName: 'foobar'}); // 6 chars: invalid
console.log(test.selector("firstName").minLength(2).maxLength(5).isValid);
const test2 = new Validator({firstName: 'fooba'}); // 5 chars: valid
console.log(test2.selector("firstName").minLength(2).maxLength(5).isValid);
const test3 = new Validator({firstName: 'f'}); // 1 char: invalid
console.log(test3.selector("firstName").minLength(2).maxLength(5).isValid);
Create a class with fluent methods/chainable methods, that return this, which is an instance of the class itself and when you finally run validation according to the rules, call .validate(), which will act as a final method to return the result:
class Validator {
constructor (body) {
this._body = body;
}
selector(str) {
this._selector = str;
return this;
}
minLength(num) {
this._minLength = num;
return this;
}
maxLength(num) {
this._maxLength = num;
return this;
}
validate() {
// run your validation logic here and return true or false accordingly
return true
}
}
const req = { body: 'body' };
const test = new Validator(req.body);
const myBoolean = test
.selector('firstName')
.minLength(2)
.maxLength(5)
.validate();
console.log('rules:');
console.log(test);
console.log(`result: ${myBoolean}`);
This is the builder pattern (sort of). You'll probably want to define a separate class that has a minLength and maxLength function. Those functions will set some state on the builder, and return either this (the builder its self), or a new builder that's a copy of this. Then you'd have some finalize function on the builder, which looks at the state, handles all the logic based on the min/max, and returns a boolean.
I try to create chaining function using vanilla javascript, its work if just chaining, but if inside other function its stop working.
var doc = document,
M$ = function(el) {
var expr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/;
var m = expr.exec(el);
if(m[1]) {
return doc.getElementById(m[1]);
} else if(m[2]) {
return doc.getElementsByTagName(m[2]);
} else if(m[3]) {
return doc.getElementsByClassName(m[3]);
}
},
$ = function (el) {
this.el = M$(el);
// event function
this.event = function(type,fn) {
this.el.addEventListener(type,fn,false);
return this;
}
// forEach function
this.forEach = function(fn,val) {
for(var i = this.el.length - 1; i >= 0; i--) {
fn.call(val, i, this.el[i]);
}
return this;
}
if(this instanceof $) {
return this.$;
} else {
return new $(el);
}
};
//use
$("button").forEach(function(index, el)
// when i use function event, its not work
el.event("click", function() {
alert("hello");
});
// if i'm using addEventListener its work, but i want use event function
});
My question is, how to be event function working inside forEach function?
Thanks for help!
First off, there is an issue with brackets in your code after $("button").forEach(function(index, el) you are missing {;
Then the problem is that when you try to call click-callback on your elements (buttons), in fact, due to the this issues the elements (buttons) don't have event() property. They are not even defined themselves since this.el = M$(el); goes outside forEach(). I tweaked and cleaned a little your code, check it out. I guess now it does what you want:
var doc = document,
M$ = function(el) {
var expr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/;
var m = expr.exec(el);
if(m[1]) return doc.getElementById(m[1]); else if(m[2]) return doc.getElementsByTagName(m[2]); else if(m[3]) return doc.getElementsByClassName(m[3]);
},
$ = function(el) {
this.forEach = function(fn,val) {
// assign this.el and this.el[i].event inside forEach(), not outside
this.el = M$(el);
for(var i = this.el.length - 1; i >= 0; i--) {
this.el[i].event = function(type,fn) { this.addEventListener(type,fn,false); };
fn.call(val, i, this.el[i]);
}
}
return this;
};
$("button").forEach(function(index, el) {
el.event("click", function() { alert("hello, " + this.textContent); });
});
<button>btn1</button>
<button>btn2</button>
UPDATE
While the previous solution is fine for the particular purpose of setting click handlers on buttons, I think what you really want is to emulate Jquery and chain function calls. I improved your attempt right in this way:
var doc = document,
M$ = function(el) {
var expr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/;
var m = expr.exec(el);
if(m[1]) return doc.getElementById(m[1]);else if(m[2]) return doc.getElementsByTagName(m[2]); else if(m[3]) return doc.getElementsByClassName(m[3]);
},
$ = function (el) { //console.log(this);
this.el = M$(el);
this.event = function(type,fn) {
for(var i = this.el.length - 1; i >= 0; i--) this.el[i].addEventListener(type,fn,false);
}
this.forEach = function(fn) {
fn.call(this);
}
return this;
};
$("button").forEach(function() {
this.event("click", function() {
alert("hello, " + this.textContent);
});
});
<button>btn1</button>
<button>btn2</button>
The key to understanding here is that your this object should always be equal to $ {el: HTMLCollection(2), event: function, forEach: function}. So,
calling $("button") you initially set it to $ {el: HTMLCollection(2), event: function, forEach: function} - with HTML Collection and event&forEach functions;
calling $("button").forEach(fn) you keep forEach's context equal to this from previous step;
calling fn.call(this); inside forEach() you call your callback fn and pass the same this to it;
inside the callback fn you call this.event() - it works because your this is always the one from the first step.
in this.event() which is just $.event() we just traverse our HTMLCollection and set handlers for click event on buttons. Inside $.event() this will be equal to a button element because we call it in such a context on click event, so, this.textContent takes the buttons' content.
Thanks, really good question!
First things first.
1.
this.el = M$(el);
M$ = function(el) {
var expr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/;
var m = expr.exec(el);
if(m[1]) {
return doc.getElementById(m[1]);
} else if(m[2]) {
return doc.getElementsByTagName(m[2]);
} else if(m[3]) {
return doc.getElementsByClassName(m[3]);
}
}
As you defined M$ you can either have a HtmlCollection if you get elements by tag name or by class name or just one element if you get element by id.
Then you suppose that your el is one when it can be a collection.
this.event = function(type,fn) {
this.el.addEventListener(type,fn,false);
return this;
}
You probably receive a collection if you try to get all buttons.
2.
If you try to run posted code you will receive an Unexpected identifier error because you missed a { after forEach(function(index, el).
3.
If you put that { in there you will receive a el.event is not a function error because you don't have an event function on el, but you have that on $(el).
4.
If you change your code to:
$("button").forEach(function(index, el)
{
// when i use function event, its not work
$(el).event("click", function() {
alert("hello");
});
// if i'm using addEventListener its work, but i want use event function
});
You'll receive an error because you didn't handled multiple elements. See 1 problem.
Have a look at this.
var doc = document,
M$ = function(el) {
var expr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/;
var m = expr.exec(el);
if(m[1]) {
return Array.apply([],[doc.getElementById(m[1])]);
} else if(m[2]) {
return Array.apply([],doc.getElementsByTagName(m[2]));
} else if(m[3]) {
return Array.apply([],doc.getElementsByClassName(m[3]));
}
},
$ = function (el) {
if(! (this instanceof $)) {
return new $(el);
}
this.els = M$(el);
// event function
this.event = function(type,fn) {
this.forEach(function(index, el){
el.addEventListener(type,fn,false);
});
return this;
}
// forEach function
this.forEach = function(fn,val) {
for(var i = this.els.length - 1; i >= 0; i--) {
fn.call(val, i, this.els[i]);
}
return this;
}
return this;
};
//use
$("button").event("click", function() {
alert("hello");
});
Here the M$ function is made to return an array to keep things consistent.
So, the $().event function is changed to iterate through all the elements in this.els.
Hence, you could simply call $("button").event function instead of $("button").forEach function to register event listeners.
Refer: Demo
This one works. But, Is this what you want? I am not sure.
I am having some trouble getting the text of elements using querySelectorAll. I already tried using querySelector but it only gives me the value of the first element. What I want to achieve is to get the text of the clicked element. Here's my code:
function __(selector){
var self = {};
self.selector = selector;
if(typeof selector == 'undefined'){
self.element = [self.selector];
}else{
self.element = document.querySelectorAll(self.selector);
}
// creating a method (.on)
self.on = function(type, callback){
self.element.forEach(function(elements){
elements['on' + type] = callback;
});
}
self.text = function(elems){
[].slice.call(self.element).forEach(function(el,i){
return el.innerHTML;
});
}
return self;
}
And the HTML File:
<script src="selector.js"></script>
<script>
window.onload = function(){
__('p').on('click', function(){
alert(__(this).text());
});
}
</script>
<p>Hello</p>
<p>World</p>
The code above gives me an undefined value.
Your code actually throws an error: SyntaxError: '[object HTMLElement]' is not a valid selector. This is because at __(this) you’re passing the selected element itself to the function, not a selector. You could account for this by including this as a third case:
if(typeof selector == "undefined"){
self.element = [self.selector];
}
else if(typeof selector == "string"){
self.element = document.querySelectorAll(self.selector);
}
else if(selector instanceof HTMLElement){
self.element = [selector];
}
Now, the actual problem you’ve described occurs because you don’t return anything in the self.text function. The return is for the forEach function argument, which is ignored. Return the texts in the self.text function itself and use map instead of forEach. Something like this:
self.text = function(elems){
return [].slice.call(self.element).map(function(el,i){
return el.innerHTML;
});
}
This will return an array with the inner HTML texts for each element.
Code in action
function __(selector) {
var self = {};
self.selector = selector;
if (typeof selector == "undefined") {
self.element = [self.selector];
} else if (typeof selector == "string") {
self.element = document.querySelectorAll(self.selector);
} else if (selector instanceof HTMLElement) {
self.element = [selector];
}
// creating a method (.on)
self.on = function(type, callback) {
self.element.forEach(function(elements) {
elements['on' + type] = callback;
});
}
self.text = function(elems) {
return [].slice.call(self.element).map(function(el, i) {
return el.innerHTML;
});
}
return self;
}
console.log(__("p").text()); // Logs texts of all <p>s
__("p").on("click", function() {
console.log(__(this).text()); // Logs text of clicked <p>
});
<p>Hello</p>
<p>World</p>
I'm trying to select a the parent form element of an input. The form element isn't necessarily the direct parent node. Currently this outputs "undefined" to my log.
var anInputElement = document.querySelector(...);
var formElement = getFormElement(anInputElement);
console.log(formElement);
function getFormElement(elem) {
//if we've traversed as high as the `body` node then
//we aint finding the `form` node
if(elem.nodeName.toLowerCase() !== 'body') {
var parent = elem.parentNode;
if(parent.nodeName.toLowerCase() === 'form') {
return parent;
} else {
getFormElement(parent);
}
} else {
return false;
}
}
Why am I getting undefined in my console log?
not just
getFormElement(parent);
but
return getFormElement(parent);
and simplified, just for fun:
function getFormElement(elem) {
if(elem.nodeName.toLowerCase() !== 'body') {
var parent = elem.parentNode;
return parent.nodeName.toLowerCase() === 'form' ? parent : getFormElement(parent);
}
return false;
}