Angular 2 - NgFor not updating view - javascript

I am working on angular 2 project and I am having an issue when I am trying to change the list . NgFor not recognizing the changes , and displaying only the list loaded at first time .
here is an example code when I am loading all list and imminently after loading I reset it with null . the view still displaying all the list ...
this is my component constructor for example :
constructor( private songService : SongService)
this.songService.getSongs()
.subscribe(songsList => {
this.songs = songsList;
});
this.songs = null;
}
and this is the html :
<div class="row">
<div *ngFor= "let song of songs" class="col-md-4">
<app-song-item [song]="song"></app-song-item>
<br>
</div>
</div>

Loops in Angular sometimes screw up, in the way that they don't track your items the way you would want it to.
To prevent that, you can use a custom track by function like this
<div *ngFor="let song of songs; let i = index; trackBy: customTB" class="col-md-4">
In your TS
customTB(index, song) { return `${index}-${song.id}`; }
This way, you set up a custom trackBy function, that will update your view (the view wasn't getting updated because the tracking ID wasn't changing).

The reason why you are still seeing your list is because it is async. You can't be sure when the subscribe method is executed. It can be be direct, within seconds, take hours or not even at all. So in your case you are resetting the list before you are even getting one.
constructor( private songService : SongService)
this.songService.getSongs()
.subscribe(songsList => { //Might take a while before executed.
this.songs = songsList;
});
this.songs = null; //executed directly
}
The above explanation might be the cause of your problem, but there could also be another explanation. The constructor is only called when the component is created. Changing a router parameter doesn't necessarily create a component. Angular might re-use the component if it can.

Instead of null you should set an empty array, also have it inside a method, otherwise it never gets called
this.songService.getSongs()
.subscribe(songsList => {
this.songs = songsList;
});
clear(){
this.songs = [];
}

Try this
constructor(private songService: SongService) {
this.songService.getSongs()
.subscribe(songsList => {
this.songs = songsList;
this.reset();
});
}
reset() {
this.songs = [];
}

Related

Why are new rows not being added to ag-grid using vuejs?

I've got an ag-grid component in a vue.js application declared like this:
<ag-grid-vue :enable-sorting="false"
:enable-filtering="false"
:suppressDragLeaveHidesColumns="true"
:rowData="category.policyDocuments"
:gridOptions="policyGridOptions"
#cellValueChanged="savePolicyRow"
#grid-ready="onPolicyGridReady($event, category)"
class="w-100"
style="height: 200px">
</ag-grid-vue>
Bound to this typescript view model:
export class PolicyListViewModel {
status: PolicyDashboardStatus;
policyLists: DocumentWalletPolicyDocumentList[] = [];
gridApi: GridApi | null = null;
policyDocuments: DocumentWalletDocumentViewModel[] = [];
constructor(status: PolicyDashboardStatus) {
this.status = status;
}
get shouldShow(): boolean {
return this.policyDocuments.length > 0;
}
updateDocuments(): void {
this.policyDocuments = this.policyLists
.map(list => list.policyDocuments)
.reduce((a, b) => a.concat(b), []);
}
}
When my page first displays I get the right data. When I call updateDocuments with new data the grid does not update. I have verified in vue devtools that the rowData prop is being updated. ag-grid documentation would suggest that the component should react to the change. Any ideas what I am doing wrong?
Ah-ha. Finally figured it out, posting here in case it helps anyone else.
It looks like my problem was binding both gridOptions and rowData at the same time. Everything was working fine except for the the refresh when rowData was changed. I'm assuming this is because the grid options had its own rowData property and the ag-grid wrapper was using the data from one copy and detecting changes on the other (or something like that).
I replaced the gridOptions binding with a columnDefs binding (in this case the only options I needed to set) and everything started working exactly as it should.

Angular 2 components inside ngFor are not updated

I'm struggling for a few hours trying to update the view of my list of item.
I have a component called document-list.
This component goes through each document passed to it and include a component called document-list-item.
<div *ngFor="let document of documents">
<document-list-item [document]="document"></document-list-item>
</div>
When I update the list of document, the children views are not updated.
I tried those in the documents accessor of the document-list component :
public set documents(value: DocumentDetails[]) {
this._documents = value;
// used one by one
this.changeDetector.detectChanges(); => doesn't work
this.changeDetector.markForCheck(); => doesn't work
this.appRef.tick(); => throw an error about recursivity
}
I also tried to manually update the list in the setters (well placed console.log showed me that it goes in) :
private _documents: DocumentDetails[] = [];
#Input()
public set documents(value: DocumentDetails[]) {
this._documents = [];
for (let i = 0; i < value.length; i++) {
this._documents[i] = value[i];
}
}
public get documents() {
return this._documents;
}
The only thing that works was doing this in the accessor :
public set documents(value: DocumentDetails[]) {
this.documents = [];
setTimeout(() => {
this._documents = value;
}, 0);
}
It worked but it showed a blinking list each time the documents array were updated, which is not acceptable.
Any help is welcome
You have:
<document-list-item [document]="document"></document-list-item>
So you should have in your child class:
#Input()
document: any;

Angular Observable subscription fallback?

I'm using Angular 4 and I have a component that lives at the 'player/:id' route in my application. When the player navigates to this route from within the application I use a currentPlayerService to sync the current player within the app. This works great with the below code.
this.currentPlayerService.getPlayer()
.subscribe((player) => this.currentPlayer = player);
However, when the application loads directly to the 'player/:id' route (from an external link) I can comment out the above and use the code below to set the currentPlayer from the route params.
this.route.params.subscribe(params => {
this.playerService.getPlayer(params.id)
.subscribe((player) => {
this.currentPlayer = player;
this.currentPlayerService.setPlayer(player);
});
});
What I'd like to do (and am having difficulty finding the "reactive function programming" way of saying) is to load the player from the params only if this.currentPlayerService.getPlayer() doesn't have a value on load. I'm having a hard time conceiving of a way to use the Observable in some kind of logic control flow, so any help would be appreciated.
Thanks!
Observables itself is stateless. To keep track of a value (or current value), you will need to use either Subject or BehaviorSubject.
In your currentPlayerService, create a BehaviorSubject called playerBSubject:
export class CurrentPlayerService{
public playerBSubject: BehaviorSubject<any>;//can be type of Player if you have such type
constructor(){
this.playerBSubject = new BehaviorSubject({})//any value that you want to initialize
}
getPlayer(){
//do your logic here, whatever that is.
//I am using an Observable.of to mimic a http request
Observable.of({})
.subscribe((player)=>{
this.playerBSubject.next(player)//this will update the subject with the latest value
});
return this.playerBSubject.asObservable();
}
}
Note that the .next() method will update the values of your subject, and you can return it as an observable using .asObservable().
Now, in your component controller, you can check if your BehaviourSubject exist (or has the value you want), and only call playerService.getPlayer() if need to.
this.route.params.subscribe(params => {
//check if BehaviorSubject exist or not
if (this.currentPlayerService.playerBSubject.getValue() === {}) {
this.playerService.getPlayer(params.id)
.subscribe((player) => {
this.currentPlayer = player;
this.currentPlayerService.setPlayer(player);
});
}
});
Suggestions:
I am not sure why you need two service namely currentPlayerService and playerService. If your currentPlayerService is just to keep track of "current" value of the player, then you do not need it at all, should you use BehaviorSubject to keep track of your current player. All of them can boil down into one single service.
export class PlayerService {
public playerBSubject: BehaviorSubject<any>;
constructor() {
this.playerBSubject = new BehaviorSubject({})
}
getPlayer(id) {
Observable.of({id}) // implement your own logic
.subscribe((player) => {
this.playerBSubject.next(player)
});
return this.playerBSubject.asObservable();
}
setPlayer(id) {
return Observable.of({id})//implement your own logic
.subscribe(newPlayer => this.playerBSubject.next(newPlayer))
}
}
And in your controller if you want to get the current value you can just do:
this.currentPlayer = this.playerService.playerBSubject.getValue();
And with a little help of asObservable you can do this:
this.playerService
.asObservable()
.subscribe(player => {
if (player === {}) {
this.route.params.subscribe(params => {
this.playerService.getPlayer(params.id); //voila, your player is updated
})
}
//remember to update the value
this.currentPlayer = player
})

Rendering an array of html elements

I want to render an array of html elements in my component. The reason for storing the data/html in an array is because I want to be able to dynamically load a new element depending on a button-click.
This is how I want to display my array:
<div>
{this.state.steps}
</div>
This is how I initiate my component and array:
componentDidMount() {
this.createProcessStep().then(step => {
this.setState({steps: this.state.steps.concat(step)});
});
}
export function createProcessStep() {
this.setState({processStepCounter: this.state.processStepCounter += 1});
return this.addStepToArray().then(d => {
return this.reallyCreateProcessStep()
});
}
addStepToArray = () => {
const step = {
...Some variables...
};
return new Promise(resolve => {
this.setState({
stepsData: this.state.stepsData.concat(step)
}, resolve)
});
};
"stepsData" is another array that holds data (variables) belonging to each step. "steps" on the other hand, should only hold the html.
This is how one step/element looks like:
<div>
...Some Content...
<button label="+" onClick={ () => {
this.createProcessStep().then(step => {
this.setState({
steps: this.state.steps.concat(step)
});
})
}}/>
...other content...
</div>
This button within each step is responsible for loading/adding yet another step to the array, which actually works. My component displays each step properly, however react doesn't properly render changes to the element/step, which is
to say that, whenever e.g. I change a value of an input field, react doesn't render those changes. So I can actually click on the "+"-button that will render the new html element but whenever a change to this element occurs,
react simply ignores the phenotype of said change. Keeping in mind that the changeHandlers for those steps/elements still work. I can change inputfields, radioButtons, checkboxes etc. which will do exactly what it's
supposed to, however the "re-rendering" (or whatever it is) doesn't work.
Any ideas of what I'm doing wrong here? Thanks!
While you could certainly beat your approach into working, I would advise that you take more common react approach.
You make your components to correctly display themselves from the state . ie as many steps are in the state, your component will display. Than make your add button add necessary information (information, not formated html) to the state.
Here is an example how to use component N times:
const MyRepeatedlyOccuringComponent = (n) => (<p key={n}>There goes Camel {n}</p>)
const App = () => {
const camels = [1,22,333,4444,55555]
const caravan = camels.map((n) => MyRepeatedlyOccuringComponent(n))
return(<div>{caravan}</div>
}

How to dynamically append a record into an Array in flex?

I have an mxml view in flex, and I need to dynamically add data to a DataGrid component.
This is where the DataGrid is initialized:
<mx:DataGrid id="myGrid" width="100%"
dataProvider="{initDG}" >
<mx:columns>
<mx:DataGridColumn dataField="Identifier" />
<mx:DataGridColumn dataField="Name" />
</mx:columns>
</mx:DataGrid>
This is the script part:
private var DGArray:Array = new Array;
[Bindable]
public var initDG:ArrayCollection;
private function onCreation():void{
initData();
}
public function initData():void {
initDG=new ArrayCollection(DGArray);
}
private function onShow():void{
for (var child:Object in children) {
var details:Array = null;
if (child instanceof String) {
var test:String = children[child].toString();
details = test.split(",");
}
//Here I need to make an object like this one:
// record = {Identifier: details[0] , Name: details[1]};
this.DGArray.push(the record created);
}
}
I did this method because it's working if DGArray was a static Array:
private var DGArray:Array = [
{Identifier:'001', Name:'Slanted and Enchanted'},
{Identifier:'002', NAme:'Brighten the Corners'}];
Can anyone tell me how to create the record and add it to DGArray?
Thanks:)
In short
Add or remove items through the ArrayCollection instance instead of through the Array instance.
And here's why
ArrayCollection - as its name suggests - is in fact nothing but a wrapper around Array, adding some functionality to it that comes in handy when working with the Flex framework.
So when you do
initDG.addItem(theNewItem);
that new item will automatically also be added to the underlying Array.
Additionally this function will also dispatch a CollectionEvent, which will notify the DataGrid that the data in its dataProvider has changed and it should be redrawn to reflect those changes.
If on the other hand you do
DGArray.push(theNewItem);
like you did, you directly alter the underlying Array. This doesn't really break anything; you'll still be able to acces the new item through e.g. ArrayCollection.getItemAt() as well, but the DataGrid was never notified of the change, hence it didn't redraw and keeps displaying the old data.

Categories