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.
Related
Can Anyone help me please. Basically I am using React with Typescript for a project for school and all of a sudden this error happens and I don't understand why... I tried solving it with a boolean state and a simple Work[], two states and it always spits out the same error: Too many re-renders and I can't imagine why. To the kind soul that saves me, you have my thanks
This is my component
export default function WorkList(props :any) {
const id = props.id
const [works, setWorks] = useState<Work[]>()
useEffect(()=> {
if(!works){
Api.fetchFromAPI(
HTTP_METHOD.GET,
`/artist/${id}/worksofart?token=${AuthService.getToken()}`,
new Headers()
).then((listOfWorks) => {
setWorks(listOfWorks)
})
}
}, [works])
function renderWorks(work: Work) {
return <WorkPost work={work}/>
}
return !works ? (
<div>
<h3>Loading works...</h3>
</div>
) : (
<div className={"work-panel"}>
<h3>Works Of Art:</h3>
<div>
{works.map(renderWorks)}
</div>
</div>
)
}
This is my work object
export class Work {
public id: string;
public work_name: string;
public owner: string;
public description: string;
public reviews: number;
public tags: Array<string>
public content: string
public fileExtension: string
public comments: Array<string>;
public ups: Array<string>;
constructor(id: string, work_name: string, owner: string, description: string, reviews: number, tags: Array<string>, content: string, fileExtension: string, commments: Array<string>, ups: Array<string>) {
this.id = id
this.work_name = work_name
this.owner = owner
this.description = description
this.reviews = reviews
this.tags = tags
this.content = content
this.fileExtension = fileExtension
this.comments = commments
this.ups = ups
}
}
My current thought on it is that my works array is very large and as a consequence takes a while to finish the setWorks and as it hasn't finished yet, it renders again and again and again. I might be absolutely wrong...
The output is this: Error message
Error Output - Console
UPDATE: The error was in the WorkPost component and not here, even tho it said it was here. I had a state that was updating without control or review so it was rendering, updating, rendering, etc.
Thanks for all your help!
Too many re-renders means you have an infinite loop somewhere. What I would do is: add a default value for works =>
const [works, setWorks] = useState<Work[]>([])
Can you also check the listOfWorks response? Because this could be empty, change the reference of work, retrigger the useEffect callback and start the infinite loop.
Why is it needed to have works as dependency in the useEffect? You could remove this, have an empty list as dependency and have this code run once, after mount.
useEffect(()=> {
if(!works.length){
Api.fetchFromAPI(
HTTP_METHOD.GET,
`/artist/${id}/worksofart?token=${AuthService.getToken()}`,
new Headers()
).then((listOfWorks) => {
setWorks(listOfWorks)
})
}
}, [])
I have my app.component with a list of objects
class Hero {
alias: string;
constructor(public firstName: string,
public lastName: string) {
}
}
class AppComponent {
...
heroes: Hero[] = [
new Hero("foo", "bar")
];
...
onHeroChange($event: Hero, index: number): void {
this.heroes[index] = $event;
}
<div *ngFor="let hero of heroes; let index=index">
<hero [hero]="hero" (heroChange)="onHeroChange($event, index)"></hero>
</div>
The HeroComponent is
export class HeroComponent {
#Input()
set hero(newValue: Hero) {
this._hero = newValue;
this.showAlias = !!newValue.alias;
}
get hero(): Hero {
return this._hero;
}
#Output() heroChange: EventEmitter<Hero> = new EventEmitter<Hero>();
showAlias: boolean = !1;
private _hero: Hero;
//
#HostListener('click')
onHeroClick(): void {
this.hero.alias = `alias_${+new Date()}`;
console.info('HERO TO EMIT', this.hero);
this.heroChange.emit(this.hero);
}
}
My problem is that even by assigning the changed hero in app.component, the set hero inside hero.component is not called, so showAlias in the example is not updated and I don't see the alias in the hero.component.
Do I need to force the ngFor by assigning the entire array?
Maybe a workaround could be removing the object from the array and then inserting again?
Sounds like useless computation though.
Note: this is just an example, it's not what I'm really working on, so something like
Update the showAlias prop in the onHeroClick method
or
Assign hero in the hero.component
unfortunately don't solve the issue. I need the changes to be on the outside because other stuff happens.
Could be another option changing the detection to onPush and marking for check manually?
Blitz ==> https://stackblitz.com/edit/angular-ivy-kpn3ds
You're not setting a new hero, you're just modifying a property on the existing one:
this.hero.alias = `alias_${+new Date()}`;
That doesn't fire the setter. Change the line like this:
this.hero = {...this.hero, alias: `alias_${+new Date()}`};
I am trying to make $onChanges hook work by using immutable way.
Chat Service
class ChatService {
constructor() {
this.collection = {
1: [
{
chat: 'Hi',
},
{
chat: 'Hello',
},
{
chat: 'How are you?',
},
],
};
}
getCollection() {
return this.collection;
}
getChatById(id) {
return this.collection[id];
}
addChat(id, chat) {
// this.collection[id].push(chat);
this.collection[id] = this.collection[id].concat(chat);
}
}
Chat Component
const Chat = {
bindings: {},
template: `<chat-list chats="$ctrl.chats" add-msg="$ctrl.addMsg(chat)"></chat-list>`,
// template: `<chat-list chats="$ctrl.chats[$ctrl.id]" add-msg="$ctrl.addMsg(chat)"></chat-list>`,
controller: class Chat {
constructor(ChatService) {
this.ChatService = ChatService;
this.id = 1;
// if i get the all the chat collection by
// this.chats = ChatService.getCollection()
// and then use like above in the commented out template,
// and it works and triggers $onChanges
this.chats = ChatService.getChatById(this.id);
}
addMsg(msg) {
this.ChatService.addChat(this.id, { chat: msg });
}
},
};
Chat List Component
const ChatList = {
bindings: {
chats: '<',
addMsg: '&',
},
template: `
<div>
<li ng-repeat="chat in $ctrl.chats">{{chat.chat}}</li>
<form ng-submit="$ctrl.addMsg({chat: chatmodel})">
<input ng-model="chatmodel">
</form>
</div>
`,
controller: class ChatList {
$onChanges(changes) {
console.log(changes);
if (changes.chats && !changes.chats.isFirstChange()) {
// this.chats = changes.chats.currentValue;
}
}
},
};
However, $onChanges hook doesn't fire. I know that in order to make the $onChanges fire, need to break the reference of binding chats in chat-list component from the chat component.
Also I could re-fetch the chats after adding on the addMsg method, it would work and trigger $onChanges but if the msg was from the another user and lets say if I was using Pusher service, it would only update the chats collection on the Chat Service not the chat-list component.
One way $onChanges seems to fire is when I get all the chat collection and then use ctrl.id to get particular chats when passing via the bindings like <chat-list chats="$ctrl.chats[$ctrl.id]" instead of <chat-list chats="$ctrl.chats. However, this will update chat list without doing anything on the $onChanges.
Ideally, I would like to update the chat list on the view by <chat-list chats="$ctrl.chats and then using the currentValue from the $onChanges hook and not use like $watch and $doCheck. I am not sure how to do it. Any help is appreciated. Thanks and in advance.
Here's very basic example of it on the plunkr.
Let's walk trough what your code is doing for a minute to ensure we understand what's going wrong:
The constructor in ChatServices creates a new object in memory (Object A), this object has a property 1 which holds an array in memory (Array 1)
constructor() {
this.collection = {
1: [
{
chat: 'Hi',
},
{
chat: 'Hello',
},
{
chat: 'How are you?',
},
],
};
}
In your component's constructor, you use the ChatService to retrieve Array 1 from memory and store it in the this.chats property from your component
this.chats = ChatService.getChatById(this.id);
So currently, we have two variables pointing to the same array (Array 1) in memory: The chats property on your component and the collection's 1 property in the ChatService.
However, when you add a message to the ChatService, you are using the following:
addChat(id, chat) {
this.collection[id] = this.collection[id].concat(chat);
}
What this is doing is: It updates collection's 1 property to not point towards Array 1, but instead creates a new array by concatenating both the current Array 1 and a new message, store it in memory (Array 2) and assign it to collection[id].
Note: This means the Object A object's 1 property also points to Array 2
Even tho the collection's 1 property has been updated properly when it comes to immutability, the chats property on your component is still pointing towards Array 1 in memory.
There's nothing indicating it should be pointing to Array 2.
Here's a simple example demonstrating what's happening:
const obj = { 1: ['a'] };
function get() {
return obj['1'];
}
function update() {
obj['1'] = obj['1'].concat('b');
}
const result = get();
console.log('result before update', result );
console.log('obj before update', obj['1']);
update();
console.log('result after update', result );
console.log('obj after update', obj['1']);
As you can see in the above snippet, pointing obj['1'] towards a new array doesn't change the array result points to.
This is also why the following is working correctly:
One way $onChanges seems to fire is when I get all the chat collection
and then use ctrl.id to get particular chats when passing via the
bindings like <chat-list chats="$ctrl.chats[$ctrl.id]" instead of
<chat-list chats="$ctrl.chats.
In this case you are storing a reference to Object A. As mentioned above, the 1 property on the ChatService's collection is updated correctly, so this will reflect in your component as it's also using that same Object A.
To resolve this without using the above way (which is, passing Object A to your component), you should ensure the component is aware of the changes made to Object A (as it can not know this when not having access to it).
A typical way these kind of things are done in Angular (I know this is AngularJS, but just pointing out how you can resolve this in a way Angular would do and works fine with Angular JS) is by using RXjs and subscribe to the chats changes in your component.
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.
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