React - setState doesn't refresh component as expected - reactjs

I might be experiencing either a bug, or I misunderstand something about general javascript syntax.
Using ServiceNow UI Builder, I'm trying to refresh the datasource of a specific data visualization component. Which requires me to use setState and send in an entire JSON blob.
The following works as expected:
api.setState('intAssignedDonut', {
"header": "Interactions assigned to one of my groups",
"datasource": [{
"isDatabaseView": false,
"allowRealTime": true,
"sourceType": "table",
"label": {
"message": "Interaction"
},
"tableOrViewName": "interaction",
"filterQuery": "active=true^assignment_groupDYNAMICd6435e965f510100a9ad2572f2b47744",
"id": "intAssigned",
"elm": {}
}],
"metric": [{
"dataSource": "intAssigned",
"id": "intAssignedMetric",
"aggregateFunction": "COUNT",
"numberFormat": {
"customFormat": false
},
"axisId": "primary"
}],
"groupBy": [{
"maxNumberOfGroups": "ALL",
"numberOfGroupsBasedOn": "NO_OF_GROUP_BASED_ON_PER_METRIC",
"showOthers": false,
"groupBy": [{
"dataSource": "intAssigned",
"groupByField": "state",
"isRange": false,
"isPaBucket": false
}]
}]
});
However, I only need to alter a few properties, not the whole thing.
So I thought I'd just clone the thing into a temp object, change what I need, then pass the cloned object back.
let clientState_intAssignedDonut = api.state.intAssignedDonut;
clientState_intAssignedDonut.header = 'Interactions assigned to one of my groups';
clientState_intAssignedDonut.datasource[0].filterQuery = 'active=true^assignment_groupDYNAMICd6435e965f510100a9ad2572f2b47744';
api.setState("intAssignedDonut", clientState_intAssignedDonut);
This seems to update the header properly, but the component doesn't refresh the datasource.
Even if I console.log api.state.intAssignedDonut it looks identical to the whole JSON blob.
EDIT: I also tried using spread operators, but I can't figure out how to target the datasource[0]
api.setState("intAssignedDonut", {
...api.state.intAssignedDonut,
header: "Interactions assigned to one of my groups",
datasource[0].filterQuery: "active=true^assignment_groupDYNAMICd6435e965f510100a9ad2572f2b47744"
});

Javascript objects are passed by reference values, and react state is immutable:
let clientState_intAssignedDonut = api.state.intAssignedDonut;
api.setState("intAssignedDonut", clientState_intAssignedDonut);
This is mutating the state directly, and React will ignore your update if the next state is equal to the previous state, which is determined by an Object.is comparison to check if both objects are of the same value, see docs
Your second attempt is heading to the right direction using spread operator:
Update method one: first copy the nested object using: JSON.parse(JSON.stringify(object)), or you can find other method in this question: What is the most efficient way to deep clone an object in JavaScript?
let copied = JSON.parse(JSON.stringify(api.state.intAssignedDonut)); // copy a nested object
copied.header = "Interactions assigned to one of my groups";
copied.datasource[0].filterQuery = "active=true^assignment_groupDYNAMICd6435e965f510100a9ad2572f2b47744";
setState("intAssignedDonut",copied);
Update method two:
setState("intAssignedDonut",{
...api.state.intAssignedDonut,
header: "Interactions assigned to one of my groups",
datasource: [{ ...state.datasource[0], filterQuery: "active=true^assignment_groupDYNAMICd6435e965f510100a9ad2572f2b47744" }]
});
Check out sandbox

Related

How to add new field to mongodb schema?

I have a react application that performs CRUD operations on data stored in mongodb in the cloud.mongodb.com.
The schema of the data in my react looks like this:
const restaurantSchema = new Schema({
"uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"city": {
"type": "string"
}
}, {timestamps: true});
I would like to add a new field called "preference" of type number.
My questions are:
How do I add this new field of "preference"?
Can I give it a default value of say 1 when I add this new field? (There are 900 entries in the mongodb.)?
Can I give the "preference" value based on the order of the "name" field in ascending order?
thanks.
You can add and remove fields in the schema using option { strict: false }
option: strict
The strict option, (enabled by default), ensures that values passed to our model constructor that were not specified in our schema do not get saved to the db.
var thingSchema = new Schema({..}, { strict: false });
And also you can do this in update query as well
Model.findOneAndUpdate(
query, //filter
update, //data to update
{ //options
returnNewDocument: true,
new: true,
strict: false
}
)
You can check the documentation here

Adding an object to an array contained inside an object without overriding

I am currently trying to loop through an array of objects (each object is a task), where each task contains relevant information such as a name and date. Information from each task is then utilized in creating an object containing arrays, where each array contains objects that correspond to the date, or the array.
My current code is as follows:
contextTasks.forEach((taskItem) => {
taskItem["taskSchedule"].forEach((dateItem) => {
setItems((items) => ({
...items,
[dateItem["date"]]: [
{
name: taskItem["taskName"],
time: new Date(dateItem["time"]).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
type: "Task",
},
],
}));
});
});
However, if there are multiple tasks with the same date, they will override each other and I only end up with one task per date. How would I go about pushing further objects to the array if there are other entries for that specific date?
Finished object:
Object {
"2021-04-21": Array [
Object {
"name": "Test Class v1",
"type": "Class",
},
],
"2021-04-24": Array [
Object {
"name": "Test Task v2",
"type": "Task",
},
//I would like to add another object here without overriding existing contents of the array
],
}
Have you tried using reduce ?
the idea will be to have something like this inside your accumulator:
{"date1": [{val}, {val}, ...] , "date2": [{val}, {val}, ...]}
array.reduce((acc, val) => {
// test if your accumulator has the same date as your date from the val
if(acc[val.date]) {
acc[val.date] = [... acc[val.date], ...your val]
} else {
// no date found in the accumulator so make acc.date = ...acc.date, val
acc[val.date] = [ val ]
}
}, {})
Sorry if the code is not perfect but if you want provide your initial array of data and I will fix the response code
The cause of your issue is the fact you're executing an async method inside a synchronous loop. Also, modifying state forces a re-render, and you're attempting to do it presumably many times at once. It might, and will cause a bottleneck at some point.
The solution: build your new state first, and execute a setState once.

VueJS Array update bug

I have a JSFiddle below to explain my problem but basically I have an array called tiles which has a title. When the instance is created() I add a field to this array called active
I then output this array in an <li> element and loop through it outputting the title and active objects. My problem is as you can see in the fiddle when I run v-on:click="tile.active = true" nothing happens to the active state written in the <li> element
but if I run v-on:click="tile.title = 'test'" it seems to update the active object and the title object.
Its strange behaviour I can't seem to work out why. Does anyone have any ideas?
https://jsfiddle.net/jgb34dqo/
Thanks
It's to do with Vue not knowing about your properties, change your array to this:
tiles: [
{
title: 'tile one',
active: false
},
{
title: 'tile two',
active: false
},
{
title: 'tile three',
active: false
}
]
This allows Vue to know about the active property and in turn it knows to monitor that variable.
It's worth looking at this link about Vue Reactivity as it helps with understanding how and when data will change automagically.
If you must add the properties after
take a look at $set. It allows you to add props to an object that then get watched by vue. See this fiddle, notice the change:
this.tiles.forEach(function(tile) {
// Tell vue to add and monitor an `active` prop against the tile object
this.$set(tile, 'active', false);
}.bind(this))

Custom data unwrapping in ampersand.js model

I have a model - Configuration:
var Configuration = Model.extend({
props: {
name: 'string'
}
});
In the database, configuration model / table has 3 columns -> id, name and fields. The latter stores site config as a serialized array. When retrieving the entry from the database, I unserialize it and then pass it to the front end, so the front end receives this:
{
"id": 1,
"name": 'global',
"fields": {
"enabled": true,
"site_name": "Test"
}
};
What I want to do is to set whatever is inside fields object as properties on my model, or maybe session so that things get triggered throughout the site when they are updated. To visualize it, I want to achieve something like this:
var Configuration = Model.extend({
props: {
enabled: 'boolean',
site_name: 'string'
}
});
So basically, is there are a way to 'unwrap' stuff in fields object somehow?
The parse method is what you're looking for in this case. See https://github.com/AmpersandJS/ampersand-state/blob/master/ampersand-state.js#L93-L98 It allows you to transform incoming props.

angular-schema-form: Add custom html to form fields

I have just started to look into angular-schema-form, so this might be something I've missed in the docs or description.
What I am trying to do is to add an icon next to the label of generated form fields and next to the field itself. Like so:
But out of the box angular-schema-form will generate:
I know I can make my own custom field types, but is that the way to go? That would require me to redefine all field types in a custom variant, because I need these two icons and their functionality on all my form fields.
I was hoping there were an easier way to add this functionality to generated html, and an easy way to add functionality (ng-click function) on them.
Edit: After reading through the docs again, I've figured out that I need to define my own custom field type (https://github.com/Textalk/angular-schema-form/blob/development/docs/extending.md)
From what I gather, I need to add the following to my modules config block:
schemaFormDecoratorsProvider.addMapping(
'bootstrapDecorator',
'custominput',
'shared/templates/customInput.tpl.html',
sfBuilderProvider.builders.sfField
);
I have also added the contents of shared/templates/customInput.tpl.html to $templatesCache.
But when I try to render a form, with a schema like
"schema": {
"type": "object",
"properties": {
"firstName": {
"title": "First name",
"type": "string"
},
"lastName": {
"title": "Last name",
"type": "custominput"
},
"age": {
"title": "Age",
"type": "number"
}
}
}
I only see the first field (firstName) and age. The custom type is just ignored.
I have tried to debug my way to the problem, but as far as I can see, the custom field is correctly added to the decorator. I've tried to console.log the schemaFormDecoratorsProvider.decorator() and there I can see my custom field type.
I've also tried to fire off a $scope.$broadcast('schemaFormRedraw') in my controller, but I still only see the built in field types.
As a test, I've tried to define my own decorator, overwriting the default Bootstrap decorator:
schemaFormDecoratorsProvider.defineDecorator('bootstrapDecorator', {
'customType': {template: 'shared/templates/customInput.tpl.html', builder: sfBuilderProvider.stdBuilders},
// The default is special, if the builder can't find a match it uses the default template.
'default': {template: 'shared/templates/customInput.tpl.html', builder: sfBuilderProvider.stdBuilders},
}, []);
I would expect to see all fields to be rendered the same, since I only define a default type and my own custom type. But still, I only see built in types rendered, my custominput is till just ignored.
What am I missing?
I've had this same problem, the problem is that you should not confuse the JSON schema with the form definition.
To render a custom component you have to change the form definition. I.e in your controller your standard form defintion might look something like:
$scope.form = [
"*",
{
type: "submit",
title: "Save"
}
];
You'll have to change this to:
$scope.form = [
"firstName",
"age",
{
key:"lastName",
type:"customInput"
},
{
type: "submit",
title: "Save"
}
];

Resources