Angular ngFor not displaying changed array - javascript

in my application I have a Dashboard where the user can add and delete widgets. To do so I am using angular-gridster2. This works well, but when I am adding or deleting a widget from the dashboard first no widgets are displayed anymore and then only after a refresh the correct changes occur. My Widget list the ngFor directive is iterating through is build from an observable. These values change correctly.
This is my html:
<gridster #dashboardgrid [options]="options" class="widget-container">
<gridster-item *ngFor="let widget of widgetList()" (mouseup)="setCurrentWidgetId(widget.id)" [item]="widget.position" class="gridster-design drag-handler">
<!-- some stuff-->
</gridster-item>
</gridster>
In my ngOnInit I am subscribing from the observable:
this.dataService.currentDashboardId.subscribe(dashboardId => this.currentDashboardId = dashboardId);
this.dataService.currentSheetId.subscribe(sheetId => this.currentSheetId = sheetId);
this.dataService.projectData
.subscribe((project: Project) => {
this.project = project;
});
And this is the method returning the list of widgets which should be displayed:
widgetList(): Array<Widget> {
return this.project.dashboards
.find(x => x.id === this.currentDashboardId).sheets
.find(x => x.id === this.currentSheetId).widgets;
}
I really can't find the reason for this behaviour so if anybody does I appreciate that. Thanks in advance.

Did you try to debug it?
Try to change the foreach loop to a list, instead of a function
*ngFor="let widget of widgetList()"
*ngFor="let widget of list"
and in your ngOnInit:
this.list = this.widgetList()
this way you can debug it and see if your list contains any items. If it does, then I would look deeper into your gridster-item component and make sure that the
[item]="widget.position" has value according to the component logic

store the array in some variable let say widgetList
widgetList : any;
widgetList(): Array<Widget> {
return this.widgetList = this.project.dashboards
.find(x => x.id === this.currentDashboardId).sheets
.find(x => x.id === this.currentSheetId).widgets;
}
and use ngfor in html like this
*ngFor="let widget of widgetList"

Related

How do I preload all the movies when all the tags are unselected?

I am building a small functionality for my project, basically i have an array of movie object that contains name of movies and tags assigned for that movies. I want to display the movies on the basis of tags selected, i.e if the tag “comedy” is selected , then all the movies that have “comedy” as one of their tags will be displayed
For this purpose I’m maintaining a map which consist of key as “tagname” and its “status” i.e (seleted/unselected) as value in form of state. I have written the logic for it, but my doubt is that whenever the page is loaded at first all the movies should be displayed because no tag is selected on load by default, how shall i implement it as i am initialising all the values in state(map) as false(unselected).
Please help, Here is the link for sandbox that i have implemented till now -
https://codesandbox.io/s/movie-tags-exp-4mur0?file=/src/Tags.js
You can make the following changes to your code,
Instead of maintaining the state as an object , You can maintain a list where you can add the tags and remove them
const [selectedTags, setSelectedTags] = useState([]);
with #1 in place now you can add and remove the tags as below
const manipulate = (tag) => {
if (selectedTags.includes(tag)) {
setSelectedTags((prevSelectedTags) =>
prevSelectedTags.filter((existingTag) => existingTag !== tag)
);
} else {
setSelectedTags((prevSelectedTags) => [...prevSelectedTags, tag]);
}
};
Updating the style for the selected tag will be changed as
backgroundColor: selectedTags.includes(tagname) ? "green" : "white"
You can remove the check as your filtered data is the derived state based on the selected Tags. Now you can filter as
const filteredData =
selectedTags.length > 0
? data.filter((movie) =>
movie.tags.some((tag) => selectedTags.includes(tag))
)
: data;
Once filtered you can use the filteredData to render your movies list
{filteredData.map((movie, index) => {
return (
<div key={index}>
<h3>{movie.name}</h3>
<p>{movie.desc}</p>
<br />
</div>
);
})}
Working Sandbox
Filter Movies Based on Tags

ng2-dragula after adding new item it's getting displayed at the top

I am using ng2-dragula for drag and drop feature. I am seeing issue when I drag and drop first element(or any element) at the end and then try to add new item to the array using addNewItem button, new item is not getting added to the end. If i don't drop element to the end, new item is getting added at the end in UI.
I want new items to be displayed at the bottom in any scenario. Any help is appreciated.
This issue is not reproducible with Angular 7. I see this happening with Angular 9
JS
export class SampleComponent {
items = ['Candlestick','Dagger','Revolver','Rope','Pipe','Wrench'];
constructor(private dragulaService: DragulaService) {
dragulaService.createGroup("bag-items", {
removeOnSpill: false
});
}
public addNewItem() {
this.items.push('New Item');
}
}
HTML
<div class="container" [dragula]='"bag-items"' [(dragulaModel)]='items'>
<div *ngFor="let item of items">{{ item }}</div>
</div>
<button id="addNewItem" (click)="addNewItem()">Add New Item
I edited the stackblitz from the comment to help visualize the issue. This seems to be triggered when a unit is dragged to the bottom of the list. Updated stackblitz : https://stackblitz.com/edit/ng2-dragula-base-ykm8fz?file=src/app/app.component.html
ItemsAddedOutOfOrder
You can try to restore old item position on drop.
constructor(private dragulaService: DragulaService) {
this.subscription = this.dragulaService.drop().subscribe(({ name }) => {
this.dragulaService.find(name).drake.cancel(true);
});
}
Forked Stackblitz
Explanation
There is some difference between how Ivy and ViewEngine insert ViewRef at specific index. They relay on different beforeNode
Ivy always returns ViewContainer host(Comment node)ref if we add item to the end:
export function getBeforeNodeForView(viewIndexInContainer: number, lContainer: LContainer): RNode|
null {
const nextViewIndex = CONTAINER_HEADER_OFFSET + viewIndexInContainer + 1;
if (nextViewIndex < lContainer.length) {
const lView = lContainer[nextViewIndex] as LView;
const firstTNodeOfView = lView[TVIEW].firstChild;
if (firstTNodeOfView !== null) {
return getFirstNativeNode(lView, firstTNodeOfView);
}
}
return lContainer[NATIVE]; <============================= this one
}
ViewEngine returns last rendered node(last <li/> element)ref
function renderAttachEmbeddedView(
elementData: ElementData, prevView: ViewData|null, view: ViewData) {
const prevRenderNode =
prevView ? renderNode(prevView, prevView.def.lastRenderRootNode!) : elementData.renderElement;
...
}
The solution might be reverting the dragged element back to original container so that we can let built-in ngForOf Angular directive to do its smart diffing.
Btw, the same technique is used in Angular material DragDropModule. It remembers position of dragging element and after we drop item it inserts it at its old position in the DOM which is IMPORTANT.

Ionic: change ion-list items on click

Lets say I have 3 lists
list: 1 ) bus , plane
list: 2 ) [related to bus] slow , can't fly
list: 3) [related to plane] fast, can fly
In my Ionic Angular project I have successfully made the 1st ion-list. But how can I change the whole ion-list by clicking on the item inside it?
[I get it, its something to do with (click) function, but how I can affect the whole list using typescript]
Edit: I get what you want to achieve. You can do this by creating an intermediary list and using that list in your ngFor. That way you can just simply change the reference of the intermediary list to whatever list you like onClick
export class ListPage {
transportationTypes: string[] = ['bus', 'plane'];
busSpecs: string[] = ['slow', "can't fly"];
planeSpecs: string[] = ['fast', 'can fly'];
currentList: string[] = this.transportationTypes;
itemClicked(type): void {
if (type === 'bus') {
this.currentList = this.busSpecs;
} else if(type === 'plane') {
this.currentList = this.planeSpecs;
} else {
this.currentList = this.transportationTypes;
}
}
}
And in your HTML just call the itemClicked function
<ion-list *ngIf="currentList">
<ion-item *ngFor="let item of currentList" (click)="itemClicked(item)">
{{item}}
</ion-item>
</ion-list>

Make all filter options 'active' on initialisation - Angular

https://stackblitz.com/edit/timeline-angular-7-wbff3f
The above stackblitz will demo a few pipe filters. If you click on locations of "North, South or East", it will populate the *ngFor loop - these filters can be selected on/off and use a "multifilter pipe". By default I want all these filter buttons "active" or "on" to show the complete populated list to begin with, allowing you to then click the filters off. Otherwise, the list if blank on first load!
.html file
<button [class.active]="entry.isLocationActive" (click)="toggle(entry.location); entry.isLocationActive = !entry.isLocationActive" class="btn btn-primary" type="button" *ngFor="let entry of timeLine | filterUnique">{{entry.location}}</button>
<my-timeline-entry *ngFor="let entry of timeLine | filter:filteredYear:'year'| multifilter:filteredLocations:'location' " timeEntryHeader={{entry.year}} timeEntryContent={{entry.detail}} timeEntryPlace={{entry.place}} timeEntryLocation={{entry.location}}></my-timeline-entry>
.ts file
filteredLocations: string[] = [];
toggle(location) {
let indexLocation = this.filteredLocations.indexOf(location);
if (indexLocation >= 0) {
this.filteredLocations = this.filteredLocations.filter((i) => i !== location);
} else {
this.filteredLocations.push(location);
}
}
I've played around with pre-populating the filteredLocations object with all the values, so it starts off with them all therefore active.
I managed to get the 'active' class to be active on first load but that did not carry through to the pipe filter. I'm sure its not a big change just can't see it, any help would be appreciated greatly!
filteredLocations = this.timeLine.map(a => a.location);
Adding this to the .ts file and replacing the empty filteredLocations: string[] = [];
I wasn't far off initially in the end. Hope it helps others.

How do I target all items in a list, when a change occurs in Vue.js?

I'm building a site that uses Vue for to power the majority of the UI. The main component is a list of videos that is updated whenever a certain URL pattern is matched.
The main (video-list) component looks largely like this:
let VideoList = Vue.component( 'video-list', {
data: () => ({ singlePost: '' }),
props: ['posts', 'categorySlug'],
template: `
<div>
<transition-group tag="ul">
<li v-for="(post, index) in filterPostsByCategory( posts )">
<div #click.prevent="showPost( post )">
<img :src="post.video_cover" />
/* ... */
</div>
</li>
</transition-group>
</div>`,
methods: {
orderPostsInCategory: function ( inputArray, currentCategory ) {
let outputArray = [];
for (let i = 0; i < inputArray.length; i++) {
let currentCategoryObj = inputArray[i].video_categories.find( (category) => {
return category.slug === currentCategory;
});
let positionInCategory = currentCategoryObj.category_post_order;
outputArray[positionInCategory] = inputArray[i];
}
return outputArray;
},
filterPostsByCategory: function ( posts ) {
let categorySlug = this.categorySlug,
filteredPosts = posts.filter( (post) => {
return post.video_categories.some( (category) => {
return category.slug === categorySlug;
})
});
return this.orderPostsInCategory( filteredPosts, categorySlug );
}
}
});
The filterPostsByCategory() method does its job switching between the various possible categories, and instantly updating the list, according to the routes below:
let router = new VueRouter({
mode: 'history',
linkActiveClass: 'active',
routes: [
{ path: '/', component: VideoList, props: {categorySlug: 'home-page'} },
{ path: '/category/:categorySlug', component: VideoList, props: true }
]
});
The difficulty I'm having is transitioning the list in the way that I'd like. Ideally, when new category is selected all currently visible list items would fade out and the new list items would then fade in. I've looked at the vue transitions documentation, but haven't been able to get the effect I'm after.
The issue is that some items have more than one category, and when switching between these categories, those items are never affected by whatever transition I try to apply (I assume because Vue is just trying to be efficient and update as few nodes as possible). It's also possible that two or more categories contain the exact same list items, and in these instances enter and leave methods don't seem to fire at all.
So the question is, what would be a good way to ensure that I can target all current items (regardless of whether they're still be visible after the route change) whenever the route patterns above are matched?
Have you noticed the special key attribute in the documentation?
Vue.js is really focused on performance, because of that, when you modify lists used with v-for, vue tries to update as few DOM nodes as possible. Sometimes it only updates text content of the nodes instead of removing the whole node and then append a newly created one. Using :key you tell vue that this node is specifically related to the given key/id, and you force vue to completely update the DOM when the list/array is modified and as a result the key is changed. In your case is appropriate to bind the key attribute to some info related to the post and the category filter itself, so that whenever the list is modified or the category is changed the whole list may be rerendered and thus apply the animation on all items:
<li v-for="(post, index) in filterPostsByCategory( posts )" :key="post.id + categorySlug">
<div #click.prevent="showPost( post )">
<img :src="post.video_cover" />
/* ... */
</div>
</li>

Categories