How to handle an object instance in the state? - javascript

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")
});

Related

Why won't a boolean object property update?

I have an array of objects. Each object has a method that should update a boolean property in the same object called 'found'.
When I call the function, the property does not update. I am not sure why.
I thought that the 'found' property would be accessible but it isn't??
I have created a minimal version of the problem here:
https://codepen.io/sspboyd/pen/XWYKMrv?editors=0011
const gen_s = function () { // generate and return the object
let found = false;
const change_found = function () {
found = true;
};
const update = function () {
change_found();
};
return {
change_found,
found,
update
};
};
const s_arr = []; // initialize an array
s_arr.push(gen_s()); // add a new s object to the array
console.log(s_arr[0].found); // returns 'false'
s_arr.forEach((s) => {
s.update();
});
console.log(s_arr[0].found);
When your change_found function changes the value of found, it's changing the value pointed to by your let found variable, but the object returned by your gen_s function still points to the old value.
You can fix your code using the 'holder' pattern, like this:
const gen_s = function () { // generate and return the object
let foundHolder = {value: false};
const change_found = function () {
foundHolder.value = true;
};
const update = function () {
change_found();
};
return {
change_found,
foundHolder,
update
};
};
const s_arr = []; // initialize an array
s_arr.push(gen_s()); // add a new s object to the array
console.log(s_arr[0].foundHolder.value); // returns 'false'
s_arr.forEach((s) => {
s.update();
});
console.log(s_arr[0].foundHolder.value);
Or even better, use a class:
class S {
constructor() { this.found = false; }
change_found() { this.found = true; }
update() { this.change_found(); }
}
const s_arr = [];
s_arr.push(new S());
console.log(s_arr[0].found);
s_arr.forEach(s => s.update());
console.log(s_arr[0].found);

JavaScript/TypeScript Array interface[] , group by type to send 1 of many functions

I have an array of Question (interface) that I need to send to 1 of many functions based on Question type. I think my series of if statements is very ugly and am hoping there is a way of doing this that adheres to SOLID. I believe I am violating O (Open for extension, closed for modification).
renderQuestionList(contents: Question[]): HTMLElement {
return yo`
<div>${contents.map(q => {
if (q.type == 'passfailna') { return this.renderQuestionPassfailna(q) };
if (q.type == 'yesno') { return this.renderQuestionYesno(q) };
if (q.type == 'numeric') { return this.renderQustionNumeric(q) };
})}
</div>`;
}
Then,
renderQuestionPassfailna(q: Question): any {
return yo`<div>Stuff</div>`;
}
renderQuestionYesno(q: Question): any {
return yo`<div>Other Stuff</div>`;
}
renderQustionNumeric(q: Question): any {
return yo`<div>I'm Helping!</div>`;
}
it is ugly. How about building a map of functions? Perhaps something like
constructor() {
this.questions = {
passfailna: q => this.renderQuestionPassfailna(q),
yesno: q => this.renderQuestionYesno(q),
numeric: q => return this.renderQustionNumeric(q)
};
}
renderQuestionList(contents: Question[]): HTMLElement {
return yo`<div>${contents.map(q => this.questions[q.type](q))}</div>`;
}
If the logic inside the template is too large, then it can be moved to a function, such as
renderQuestionList(contents: Question[]): HTMLElement {
return yo`
<div>${contents.map(q => renderQuestion(q))}
</div>`;
}
renderQuestion(q):HTMLElement {
if (q.type == 'passfailna') { return this.renderQuestionPassfailna(q) };
if (q.type == 'yesno') { return this.renderQuestionYesno(q) };
if (q.type == 'numeric') { return this.renderQustionNumeric(q) };
}
However, I would question the wisdom of generating such a large tree all at once. When I use YO I prefer to generate small items, and insert them using appendChild. For example,
renderQuestionList(contents: Question[]): HTMLElement {
let div = yo`<div> </div>`;
contents.forEach(q => {
div.appendChild(renderQuestion(q));
});
return div;
}

How to implement more than one method in JS es6 class?

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.

updating an array in React

I have an array of 16 objects which I declare as a state in the constructor:
this.state = {
todos:[...Array(16)].map((_, idx) => {
return {active: false, idx}
}),
}
Their status will get updated through an ajax call in ComponentDidMount.
componentDidMount()
{
var newTodos = this.state.todos;
axios.get('my/url.html')
.then(function(res)
{
newTodos.map((t)=>{
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
}
this.setState({
todos:newTodos,
})
}
}
and then finally, I render it:
render(){
let todos = this.state.todos.map(t =>{
if(t.active === true){
console.log('true'}
else{
console.log('false')
}
})
return (
<div></div>
)
}
They all appear as active = false in the console, they never go into the if condition. When
I print out the entire state it appears not to be updated in the render method. In the console it says "value below was just updated now".
I thought changes to the state in ComponentWillMount will call the render function again?
How do I make that React will accept the new values of the state?
componentDidMount()
{
var newTodos = []; // <<<<<<<<<<<<<
axios.get('my/url.html')
.then(function(res)
{
newTodos = this.state.todos.map((t)=>{ //<<<<<<<<<<<<<<<
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
return t; //<<<<<<<<<<<<<<<<<<
} // <<<<< are you missing a semi-colon?
this.setState({
todos:newTodos,
})
}
}
The map() argument (in your code) is a function, not an expression, so an explicit return must be provided. I.E.:
xxx.map( t => ( "return t is implicit" ) );
xxx.map( t => { "return t must be explicit" } );
And, as #DanielKhoroshko points out, your new variable points to this.state. And of course never, never, ever alter this.state directly. Since map() returns a new array, not the original as altered, that's why we use map() and not forEach()
That is because you are actually not providing any new state, but mutating it instead.
React uses shallow comparison be default (where to objects are equal if they reference the same memory address). And that's exactly what's happening here:
var newTodos = this.state.todos; // newTodos === this.state.todos
this.setState({ todos:newTodos }) // replaces two equal addresses, so it simply doesn't change anything
The easiest solution, though probably not the most performant would be to clone your todos array:
var newTodos = [...this.state.todos]; // newTodos !== this.state.todos

Protractor: wait for an element and then perform action

I have written below code to check for locator type and based on whether element is visible or not, I am returning element. I am receiving error on call type(locatorType, value,text) method with appropriate values.
this.type = function(locatorType,value,text){
this.getElement(locatorType,value).sendKeys(text)
};
this.getElement = function(locatorType,value){
if(locatorType=='model'){
console.log(locatorType)
console.log(value)
return this.waiterFunc(element(by.model(value)));
}
else if(locatorType=='xPath'){
return this.waiterFunc(element(by.xPath(value)));
}
else if(locatorType=='buttonText'){
return this.waiterFunc(element(by.buttonText(value)));
}
};
this.waiterFunc = function(element){
console.log('In waiterfunc')
//console.log(element.getText())
browser.wait(function() {
return this.isVisible(element).then(function(){
return element;
})
})
};
this.isVisible = function(element){
return EC.visibilityOf(element);
};
Below is the error being received:
WebDriver is not able to find the element and perform the actions on it. Please suggest where I am wrong.
Separate the waiting function with the element return:
this.getElement = function(locatorType, value) {
var elm;
if (locatorType == 'model') {
elm = element(by.model(value));
this.waiterFunc(elm);
}
else if (locatorType == 'xPath') {
elm = element(by.xpath(value)); // also renamed xPath -> xpath
this.waiterFunc(elm);
}
else if (locatorType == 'buttonText') {
elm = element(by.buttonText(value));
this.waiterFunc(elm);
}
return elm;
};
In this case the waiterFunc would become simpler:
this.waiterFunc = function(element){
browser.wait(this.isVisible(element));
};

Categories