I have these two methods defined in my React component:
handleAddMetric() {
const metricKey = prompt('Name/key of metric');
const newMetricItem = {
name: metricKey,
value: 100
}
let newMetrics = {};
newMetrics[metricKey] = newMetricItem;
const updatedMetrics = Object.assign({}, this.state.metrics, newMetrics);
this.setState({ metrics: updatedMetrics });
}
handleRemoveMetric(keyName) {
let updatedMetrics = this.state.metrics;
delete updatedMetrics[keyName];
console.log('handleRemoveMetric', this, keyName, updatedMetrics);
this.setState({ metrics: updatedMetrics });
}
Adding new values to this.state.metrics works fine, but deleting:
<button onClick={this.handleRemoveMetric.bind(this, key)}>Delete</button>
...calls my handleRemoveMetric function but doesn’t update the collection.
I first it was some issue with this but that doesn’t seem to be the case.
Any ideas?
Update: The console output is:
handleRemoveMetric Metrics {props: Object, context: Object, refs: Object, updater: Object, state: Object…}componentWillUnmount: function ()context: Objectprops: Objectref: Objectrefs: ObjectsetState: function (data, cb)state: Objectupdater: Object_reactInternalInstance: ReactCompositeComponentWrapperisMounted: (...)replaceState: (...)__proto__: ReactComponent
"myMetricKey"
Object {MRR: Object, moneyInBank: Object, wow: Object}
...so at least the collection is updated locally.
You need to copy it over to a new object.
const metrics = {
...this.state.metrics,
[keyName]: null
};
this.setState({ metrics });
should work.
Related
I use the function changeCheck to check and uncheck specific components.
When I use the function, it works correctly.
this.props.team is a list of all of the teams.
The goal of changeAllTeams is to be able to check and uncheck all of the teams that have a specific league.
In this example I want to change all of the teams that have a league acronym of NFL:
this.state = {
checked: [],
checkedTeams: [],
teamObject: [],
queryString: [],
accordionStatus: [true, true, true]
}
changeAllTeams = (leagueType) => {
this.props.team.map(
(v, i) => {
if(v.league.acronym === 'NFL'){
this.changeCheck(i, v.team_name, v)
}
}
)
}
componentDidUpdate(){
console.log('checked', this.state.checked)
console.log('team object', this.state.teamObject)
console.log('props team object', this.props.teamObject)
this.props.changeLeagues(this.props.league, this.props.checkedLeagues, this.state.checkedTeams, this.state.queryString, this.state.teamObject, this.state.checked)
}
changeCheck = (index, name, teamObject) => {
//updates checked team state
if(!this.state.checkedTeams.includes(name)){
this.state.checkedTeams[this.state.checkedTeams.length] = name
this.setState({ checkedTeams: [...this.state.checkedTeams] })
//sets team object with new team object
this.state.teamObject[this.state.teamObject.length] = teamObject
this.setState({ teamObject: this.state.teamObject })
} else {
console.log(name)
newChecked = this.state.checkedTeams.filter(v => { return v !== name})
this.setState({ checkedTeams: newChecked })
//removes team object and sets new state
newObjectChecked = this.state.teamObject.filter(v => { return v.team_name !== teamObject.team_name})
this.setState({ teamObject: newObjectChecked })
}
//updates checkbox for specific space
this.state.checked[index] = !this.state.checked[index]
this.setState({ checked: this.state.checked })
this.forceUpdate()
}
When I map over the array in changeAllTeams, only the last object in the array takes effect.
The state for checked updates for everything, but the state for checkedTeams and teamObject does not.
This video may help to understand further:
https://streamable.com/q4mqc
Edit:
This is the structure of the objects in this.props.team:
I don't have your code but I'm pretty sure that the problem is that you didn't provide a unique id for each item (remember that it's most of the time a bad idea to use map index for your items). The thing that you should do is to give each item a unique key and call the function based on that id.
There are a few places where you mutate the contents of this.state. That could cause React to be unable to detect changes in the state because the new and old state are referencing the same object. I would recommend that you don't mutate any state and instead create clones of the data object before passing the new data to setState()
So I'm taking a course in web programming and in it we've gotten this assignment to design some simple front end for ordering salads, to get all the components etc. it was previously stored in a .js file in the following fashion
let inventory = {
Sallad: {price: 10, foundation: true, vegan: true},
Pasta: {price: 10, foundation: true, gluten: true},
'Salad + Pasta': {price: 10, foundation: true, gluten: true},
'Salad + Matvete': {price: 10, foundation: true, vegan: true, gluten: true},
'Kycklingfilé': {price: 10, protein: true},
'Rökt kalkonfilé': {price: 10, protein: true},
'Böngroddar': {price: 5, extra: true, vegan: true},
'Chèvreost': {price: 15, extra: true, lactose: true},
Honungsdijon: {price: 5, dressing: true, vegan: true},
Kimchimayo: {price: 5, dressing: true},
.
.
.
};
export default inventory;
This is then imported into my App.js that was created when creating the react project and sent as a prop to another component that took care of the composing of a salad that was eventually sent back to a function also sent with as a prop.
So what we're supposed to do now is to get this inventory from a local rest(?) server instead. So if I go to
http://localhost:8080/proteins
it will open a page that just displays an array with all the different choices of proteins
["Kycklingfilé","Rökt kalkonfilé","Norsk fjordlax","Handskalade räkor från Smögen","Pulled beef från Sverige","Marinerad bönmix"]
And then going to
http://localhost:8080/proteins/Kycklingfilé
Will give you another page with the properties of that ingredient
{"price":10,"protein":true}
And my attempt at recreating that inventory object with all the ingredients as properties inside state is this
class App extends Component {
constructor(props) {
super(props);
this.state = {
salads: [],
inventory: {
}
};
}
componentDidMount() {
const base = "http://localhost:8080/";
const pURL = base + "proteins/";
const fURL = base + "foundations/";
const eURL = base + "extras/";
const dURL = base + "dressings/";
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + e).then(response => response.json()).then(data => {
Object.assign(this.state.inventory, {e : data})
})
})
});
fetch(pURL).then(response => response.json()).then(data => this.setState({data}));
fetch(eURL).then(response => response.json()).then(data => this.setState({data}));
fetch(dURL).then(response => response.json()).then(data => this.setState({data}));
}
I've been using
{JSON.stringify(this.state)}
to try and look at whats going on and with this code it comes out as this
{"salads":[],"inventory":{},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So the fetch works fine for getting all the ingredients of a certain type, I guess it's only the dressings since it overwrites data each time on those last three fetches. But the problem is that inventory is completely empty.
If I instead write it like this
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
Object.assign(this.state.inventory, {e: fetch(fURL + e).then(response => response.json().then())})
})
});
The output becomes
{"salads":[],"inventory":{"e":{}},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So it adds the 'e' object, which is another problem since I want it to be the value of the current element, but it's completely empty, and I dont know how to get the data from that seconds fetch when I write it like that. So that's why it now looks like it does in the first code snippet, where it doesn't even get an empty 'e' inside inventory.
Finally, if I write it like that second example but just e: e like this
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
Object.assign(this.state.inventory, {e: e})
})
});
The output becomes
{"salads":[],"inventory":{"e":"Salad + Quinoa"},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So it seems like everything is working up until the .forEach on the array of strings that represents a certain type of ingredient since it manages to put that into 'e' inside inventory with one of the array elements as it's value. It's only the last one in the list though but I guess that stems from the problem that it just makes the object 'e' and not the value of the current element and overwrites it for every item.
Sorry if all the rambling made the problem unclear, but what I'm trying to achieve is inventory {} inside state that looks like it did when it was in a seperate file, so that when we create the component we can send this.state.inventory instead of the imported inventory as prop. And to create that using what we can fetch from the different pages.
When you write
{e : data}
you create a new Object with a single entry. That sets the value of the key 'e' as the current value of the variable 'data'. A variable named 'e' is not involved:
const e = 'hello';
console.log(e); // "hello"
console.log({ asd: e }); // { asd: "hello" }
console.log({ e: "asd" }); // { e: "asd" }
console.log({ e: asd }); // ReferenceError: asd is not defined
What you are trying to do is using the value of the variable e as the key that you want to set. In javascript this is done using [ and ] like so:
const e = 'hello';
console.log({ [e]: "world" }); // { hello: "world" }
// this is necessery whenever you want a key that is not a simple word
console.log({ ["key with spaces"]: "world" }); // { "key with spaces": "world" }
console.log({ [e + e]: "world" }); // { hellohello: "world" }
EDIT:
there is another issue with your code above that you might encounter sooner or later:
In React you should never ever modify this.state directly. Always go through this.setState()!
https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
In your case this is a bit more difficult, since you are making multiple requests which each affect the same key in your state (inventory).
Because you cannot know in what order the requests arrive, and whether React will actually do the setState each time new data comes, or do them all at the same time, you cannot simply use this.setState({ inventory: newInventory }). Instead you should use the function version as described here. Unfortunately this can be a bit complex to grasp in the beginning :(
in your case I would solve it like this:
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + e)
.then(response => response.json())
.then(data => this.setState((prevState) => ({
inventory: Object.assign({}, prevState.inventory, {[e]: data}),
})));
})
})
});
A couple of things to note here:
note the ({ in (prevState) => ({ ... }): this is an arrow function that returns an object
we are passing a function to this.setState (see the link above for details). This function receives the current state as an argument (prevState) and should return the new State. (although it can omit keys of the old state that remain unchanged). This is better than directly passing the new state to this.setState because when multiple setState happen at the same time, React can apply the functions you pass in the right order so that all changes happen, but if you passed objects it has to decide on one of them to 'win' so changes can get lost.
In Object.assign({}, prevState.inventory, {[e]: data}), instead of modifying prevState.inventory we create a new object that contains the updated inventory. You should never modify the old state, even in this.setState.
Hope this helps :)
So with #sol's advice to use [e] to create the objects for each ingredient, this code
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + [e]).then(response => response.json()).then(data => {
Object.assign(this.state.inventory, {[e] : data})
})
})
});
now works. I think why it didn't look successful with my "troubleshooting" of just printing that JSON.stringify of the entire state in render was that is just didn't render properly when react refreshed after saving the code. Updating the page makes it all blank, but clicking onto another page through a link and then back fixes it. Dont know why, but I'll take it.
how can i observe an array of interfaces. So I have declared an interfaace:
export interface IBox {
Id: number;
Name: string;
Show: boolean;
}
And in my componentWillMount():
public componentWillMount() {
this.data = observable(this.props.boxes as IBox[]);
}
how can I evoke render if the value of Show is changed?
By default mobx observers all properties in an object when object is turned into observable: https://mobx.js.org/refguide/object.html
So when you do:
this.data.push({
Id: 0,
Name: '',
Show:false
});
this.data[0].Show = true; // show is now observed.
what does not work?
var obj = {
Id: 0,
Name: '',
Show:false
};
this.data.push(obj);
obj.Show = true; // show is not observed!.
also this does not work, see: https://mobx.js.org/best/pitfalls.html
this.data.push({});
this.data[0].Show = true; // since property didn't exist when added, it is not observed.
The array should trigger that a model inside was changed and thus update/re-render the right component.
make sure that the component that uses the data has #observer. (Otherwise it wont trigger re-render). another solution would be to mobx.toJS(this.data) in the observed component and give this result as props to non-observed components.
I'm working on using a kendo inside of an angular 2 project.
Getting the widget set up correctly is no problem:
ngOnInit() {
let options = inputsToOptionObject(KendoUIScheduler, this);
options.dataBound = this.bound;
this.scheduler = $(this.element.nativeElement)
.kendoScheduler(options)
.data('kendoScheduler');
}
When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.
Example:
public views:kendo.ui.SchedulerView[] = [{
type: 'month',
title: 'test',
dayTemplate: (x:any) => {
let date = x.date.getDate();
let count = this.data[date];
return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
}
}];
The monthly-scheduler-day class:
#Component({
selector: 'monthly-scheduler-day',
template: `
<div>{{date}}</div>
<div class="badge" (click)=dayClick($event)>Available</div>
`
})
export class MonthlySchedulerDayComponent implements OnInit{
#Input() date: number;
#Input() count: number;
constructor() {
console.log('constructed');
}
ngOnInit(){
console.log('created');
}
dayClick(event){
console.log('clicked a day');
}
}
Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.
I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223
I whipped this service up to handle binding my components:
import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '#angular/core';
declare var $:JQueryStatic;
#Injectable()
export class JQueryBinder {
constructor(
private resolver: ComponentResolver,
private injector: Injector
){}
public bindAll(
componentType: any,
contextParser:(html:string)=>{},
componentInitializer:(c: ComponentRef<any>, context: {})=>void):
void
{
let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
return a instanceof ComponentMetadata
}).selector;
this.resolver.resolveComponent(componentType).then((factory)=> {
$(selector).each((i,e) => {
let context = contextParser($(e).html());
let c = factory.create(this.injector, null, e);
componentInitializer(c, context);
c.changeDetectorRef.detectChanges();
c.onDestroy(()=>{
c.changeDetectorRef.detach();
})
});
});
}
}
Params:
componentType: The component class you want to bind. It uses reflection to pull the selector it needs
contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
componentInitializer - callback that initializes the created component with the context you parsed
Example usage:
let parser = (html: string) => {
return {
date: parseInt(html)
};
};
let initer = (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
let d = context.date;
c.instance.count = this.data[d];
c.instance.date = d;
}
this.binder.bindAll(GridCellComponent, parser, initer );
Well your solution works fine until the component needs to change its state and rerender some stuff.
Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side)
the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.
So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:
let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
componentRef = componentFactory.create(this.injector, null, selectorOrNode);
componentRef.changeDetectorRef.detectChanges();
After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:
let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
properties = enumerateProperties(injector.get(NgZone))
.filter(p => p instanceof EventEmitter);
let subscriptions = Observable.merge(...properties)
.subscribe(_ => changeDetectorRef.detectChanges());
Of course don't forget to unsubscribe on destroy:
componentRef.onDestroy(_ => {
subscriptions.forEach(x => x.unsubscribe());
componentRef.changeDetectorRef.detach();
});
UPD after stackoverflowing once more
Forget all the words above. It works but just follow this answer
I have a simple question, but I'm not sure how to proceed.
Here is my initial state:
getInitialState: function() {
return {
selector:{
params:{
platform:'BITSTAMP',
currency:'USD',
item:'BTC',
interval:'15m',
range:'1d'
}
},
platforms: [],
pairs: [],
allPlatforms: {},
range: ['12h','1d','3d','1w','2w','1m','3m','6m','1y','Max', 'Custom'],
interval: ['1m','15m','1h','6h','12h','24h']
}
},
Let's say I need to change selector.params.platform. If I write:
this.setState({
selector: {
params: {
platform: "somenewplatform"
}
}
});
It will destroy my other params keeping only platform. So what I'm doing is
var newState = this.state;
newState.selector.params.platform = "newplatform";
Then I setState({selector:newState});
Is it the right way to do this? Knowing that newState.selector.params.platform = "newplatform"; means modifying the state directly without going through setState directly, I'm not sure this is the right thing to do.
So if anyone has a better solution I'll be really curious to read it.
You don't want to do
var newState = this.state;
newState.selector.params.platform = "newplatform";
this.setState({selector:newState});
This will set this.state.selector to be your entire state object. It looks like what you want is this:
var newSelector = this.state.selector;
newSelector.params.platform = "newplatform";
this.setState({selector: newSelector});