How to create a tree like structure with different views in angular - javascript

I am trying to create a tree view using recursive html. Everything works fine if all the nodes of the tree look similar but in my case the parent and child look different but child and grand child look same. So, I have nested a child component in the parent component and to generate grand children I just call the same child component recursively as shown below:
parent.component.html
<div *ngFor="let div of divsArray; index as mainIdx" class="main-container" #mainDiv>
<button (click)="addChild(mainIdx)"><i class="fa fa-plus"></i></button>
<button (click)="addSibling(mainIdx)"><i class="fa fa-plus"></i></button>
<button (click)="removeSibling(mainIdx)"><i class="fa fa-minus"></i></button>
<app-child *ngFor="let child of div.childArray" [childData]="child"></app-
child>
</div>
parent.component.ts
divsArray: any[]=[{'childArray':[]}];
addSibling(){
this.divsArray.push({'childArray':[]});
}
ngAfterViewInit(){
}
removeSibling(divIndex: number){
this.divsArray.splice(divIndex,1);
}
addChild(pIndex: number){
this.divsArray[pIndex].childArray.push({'grandChild':[]});
}
child.component.html
<div class="child-container">
<button (click)="addChild()"><i class="fa fa-plus"></i></button>
<button (click)="removeChild()"><i class="fa fa-minus"></i></button>
<app-child *ngFor="let child of childData.grandChild;"></app-child>
</div>
child.component.ts
#Input() childData: any;
addChild(){
this.childData.grandChild.push({'greatGrandChild':[]})
}
The issue which I face here is when I try to add a child for the grandchild, I get an error saying 'cannot read property grandchild of undefined'. This happens when I click the addChild button a third time (that is to add a child for the grand child).This 'grandchild' is an array in the childData object which I pass as an input property from the parent. Can someone please help me in fixing this issue or maybe suggest some way to implement a tree like structure with different views for the parent and child node . Thanks!

child.component.html
<app-child *ngFor="let child of childData.grandChild;" [childData]="child"></app-child>
Need to set the childData input
child.component.ts
addChild() {
this.childData.grandChild.push({ grandChild: [] });
}
It's extra work to access childData's children with "greatGrandChild", "greatGreatGrandChild", since you'll also have to figure out what the right field is before accessing it, and the get create the next field name and uppercase the first letter, etc.
Edit: If you want to delete the current node and it's children when clicking the '-'
child.component.html
<app-child *ngFor="let child of childData.grandChild;" [childData]="child" (remove)="removeChildren()"></app-child>
child.component.ts
#Output() remove = new EventEmitter<string>();
addChild() {
this.childData.grandChild.push({ grandChild: [] });
}
removeMe() {
this.remove.emit(null);
}
removeChildren() {
this.childData.grandChild = [];
}

Related

React- Lodash without() method rendering empty after state change?

I am new to React and I have run into some difficulty I cannot seem to figure out.
I am using Lodash to remove a single object from an array, via an onClick eventHandler. However once I select to delete a specific item, the display is rendered as empty, However When I console.log the returned Array it shows the elements are present but wrapped in a lodashWrapper.
Also the element I selected for removal is still in the Array.
I am using the lodash method without and I have included all relevant snippets.
class App extends Component
{
constructor()
{
super();
this.state =
{
appointments : [],
idx : 0,
}
this.deleteAppointment = this.deleteAppointment.bind(this)
}
deleteAppointment(apt)
{
let temp = this.state.appointments;
console.log(temp);
temp = without(temp,apt)
console.log(temp);
this.setState({
appointments : temp
});
}
<ListAppointment appointments = {this.state.appointments}
deleteAppointment={this.deleteAppointment} />
class ListAppointment extends Component
{
render()
{
return (
<div className="appointment-list item-list mb-3">
{this.props.appointments.map(item =>(
<div className="pet-item col media py-3" key={item.aptID}>
<div className="mr-3">
<button className="pet-delete btn btn-sm btn-danger"
onClick={()=> this.props.deleteAppointment(item)} >
<FaTimes />
</button>
</div>
Any insight as to why this is occurring is greatly appreciated. I have tried different formats to Bind this but i still end with the same result, which is an empty array being rendered.
Thanks in Advance

accessing *ngFor last in its component in angular 6

In angular 6 I want to access *ngFor last value as I want to operation if last value is set
eg
<li [ngClass]="list.mydata==1?'replies a':'sent a'" *ngFor="let list of chatlist; let last=last;">
<span [last]="last"></span>
<img src="{{list.profile_img}}" alt="" />
<div *ngIf="list.sender_type==0">
<p>{{list.message}}{{last}}</p>
</div>
<div *ngIf="list.sender_type==1">
<p style="background-color: burlywood;">{{list.message}}</p>
</div>
</li>
I want to do is [(myvar)]=last in place of let last=last
I want to bind the last variable so, I can access it is set or not in its component.
you can create a custom directive:
import { Directive, Output, EventEmitter, Input } from '#angular/core';
#Directive({
selector: '[onCreate]'
})
export class OnCreate {
#Output() onCreate: EventEmitter<any> = new EventEmitter<any>();
constructor() {}
ngOnInit() {
this.onCreate.emit('dummy');
}
}
and then you can use it in your *ngFor to call the method in your component:
<li [ngClass]="list.mydata==1?'replies a':'sent a'" *ngFor="let list of chatlist; let last=last;">
<span (onCreate)="onCreate(last)"></span>
<img src="{{list.profile_img}}" alt="" />
<div *ngIf="list.sender_type==0">
<p>{{list.message}}{{last}}</p>
</div>
<div *ngIf="list.sender_type==1">
<p style="background-color: burlywood;">{{list.message}}</p>
</div>
</li>
then in your component:
myvar: boolean = false;
onCreate(last) {
this.myvar = last;
}
checkout this DEMO.
Angular provides certain local variables when using *ngFor, one is for example last, which will (not as you expect currently) be a boolean value, being true if it is the last item. This is meant for adding specific stylings for the example to the last element of a list.
If you want that boolean you already use it correctly, but obviously the element using it should be a component. So instead of
<span [last]="last"></span>
it should be something like
<my-component [last]="last"></my-component>
where in my-component you define
#Input last: boolean;
and thus have access to it.
for example
<li [ngClass]="list.mydata==1?'replies a':'sent a'" *ngFor="let list of chatlist; let index=i;">
</li>
now you excess last elemnt like
<anyTag *ngIf="i === chatlist.length-1"></anyTag>

Vue component as child of another component

I am working on converting an existing theme into reusable components.
I currently have a button component like so:
<template>
<a :href="link" class="button" :class="styling"><slot></slot></a>
</template>
<script>
export default {
props: {
link: {},
styling: {
default: ''
}
}
}
</script>
And, in my HTML, I use it like so:
<vue-button link="#" styling="tiny bg-aqua">Button 1</vue-button>
Now, I am attempting to create a "button group" using the existing button component.
What I would like to be able to do is something like this:
<vue-button-group styling="radius tiny">
<vue-button link="#" styling="tiny bg-aqua">Button 1</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 2</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 3</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 4</vue-button>
</vue-button-group>
I am very new to VueJS, and am a bit confused on the proper way to handle such a thing. I would like to be able to pass as many button components into the group as is needed.
Here is what I have so far for the button group:
<template>
<ul class="button-group" :class="styling">
<li><slot></slot></li>
</ul>
</template>
<script>
export default {
props: {
styling: {
default: ''
}
}
}
</script>
This would, of course, work with a single button being passed in, but I cannot seem to figure out how to allow for more than that, while encasing each button within its own list item.
Any suggestions or corrections to the way I am going about this would be very much appreciated. Thank you.
Since you want to do advanced things with the output of your component, this might be the time to go to render functions, as they allow far more flexibility:
Vue.component('button-group', {
props: {
styling: {
default: ''
}
},
render(createElement) { // createElement is usually called `h`
// You can also do this in 1 line, but that is more complex to explain...
// const children = this.$slots.default.filter(slot => slot.tag).map(slot => createElement('li', {}, [slot]))
const children = [];
for(let i = 0; i < this.$slots.default.length; i++) {
if(!this.$slots.default[i].tag) {
// Filter out "text" nodes, so we don't make li elements
// for the enters between the buttons
continue;
}
children.push(createElement('li', {}, [
this.$slots.default[i]
]));
}
return createElement('ul', {staticClass: "button-group",class: this.styling}, children);
}
})
var app = new Vue({
el: '#app',
})
.rainbow-background {
/* todo: implement rainbow background */
border: red 1px solid;
}
.button-group {
border-color: blue;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app">
<button-group styling="rainbow-background">
<button>hi</button>
<button>How are you?</button>
<button>I'm fine</button>
<button>Have a nice day!</button>
</button-group>
</div>
A render function works by returning an virtual html structure, this structure is generated by repeated calls to a createElement functions. createElement accepts 3 parameters, a tag name (like ul or li), an options object, and an list of children.
We first start by generating a list of children with our incoming slots, who are stored inside this.$slots.default.
Then we loop over this list, filtering out incoming slot data that is basically text, this is because the way HTML considers whitespace between tags as text.
We are now almost complete with our final structure, we now wrap the slot element in a freshly generated li tag, and then we finish the generating of with wrapping everything in a new ul tag with the proper class names.
The Vue way to do this is use names slots, and provide the data the child uses to the parent. The data will propagate to the child via the slot-scope.
The whole key to this is the data flows through the parent to the child.
Here's a working codepen of the process: https://codepen.io/Flamenco/pen/ZMOdYz
This example uses the default slot, so name attribute is not needed on parent or child.
Parent
<template>
<ul class="button-group" :class="styling">
<li v-for='item in items'><slot :item='item'></slot></li>
</ul>
</template>
<script>
...
props:{
items: Array
}
</script>
Child
<vue-button-group class="radius tiny" :items='items'>
<template slot-scope='scope'>
<vue-button link="#" styling="tiny bg-aqua">{{scope.item.text}}</vue-button>
</template>
</vue-button-group>
<script>
...
data:{
items:[{text:'Button 1'},{text:'Button 2'},{text:'Button 3}]
}
</script>
You can also try to use a named slot
<template>
<ul class="button-group">
<li><slot name="buttons"></slot></li>
</ul>
</template>
And than:
<vue-button-group class="radius tiny">
<template slot="buttons">
<vue-button link="#" styling="tiny bg-aqua">Button 1</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 2</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 3</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 4</vue-button>
</template>
</vue-button-group>
A possible solution for that is to use v-for.
<button v-for="button in buttons" :key="button">
Button{{button}}
</button>
Here is the fiddle.
From there you could build your <buttongroup>-component yourself; passing "meta-infos" into your <buttongroup> as props (in my fiddle the array in the data-part).
Slots would make sense, if you want to render something else besides the buttons and you want to inject components for that. Otherwise you would gain nothing with slots.

Angular 5 w/Angular Material - Extracting an object property from ngFor to use in component

<div *ngFor="let player of players">
<h4 mat-line>{{player.firstName}} {{player.lastName}} - {{player.id}}</h4>
</div>
I'm doing a HTTP get call from my player.service.ts file, and then looping through the player object that gets returned, printing out the firstName, lastName and id properties in a massive player list.
I need to extract a specific player ID at a given point in the loop so that I can pass that down to a child Edit Player component that opens a modal with that specific player's information pre-filled in the form (using NgModel and a getbyId call to the API to get the player object). How would I go about doing this?
It looks like you're using #angular/material. If so, you should be able to use a click handler that loads the player data and opens up a dialog with their provided dialog service.
eg:
Template:
<div *ngFor="let player of players">
<h4 (click)="handlePlayerClick(player.id)"
mat-line>
{{player.firstName}} {{player.lastName}} - {{player.id}}
</h4>
</div>
Component:
constructor(private dialogService: MatDialog, private playerApi: PlayerApiService) { }
handlePlayerClick(playerId: string): void {
// potentially open a MatDialog here
this.playerApi.getById(playerId).subscribe((playerData: PlayerInterface) => {
const dialogConfig = {
data: {
playerData: playerData
}
} as MatDialogConfig;
this.dialogService.open(EditPlayerComponent, dialogConfig);
});
}
Documentation: https://material.angular.io/components/dialog/api
You'd want your child component to have a property like #Input() playerId: any; and then simply pass it in square brackets into the child tag like so:
<div *ngFor="let player of players">
<h4 mat-line>{{player.firstName}} {{player.lastName}} - {{player.id}}</h4>
<edit-player [playerId]="player.id"></edit-player>
</div>

sharing event driven variable across component in angular 2

I have a component which has a data table which I filter using a pipe,
The way I trigger and sent new argument to the pipe is on input-event on a input tag , I capture the input in 'targetInput' variable,
The above setup works, here is how it looks like:
<tr >
<td *ngFor="let column of currentView.columns">
<div *ngIf="column.label">
<input placeholder="{{column.label}}" id="{{column._devName}}" type="text"
(input)="targetInput = {targetValue:$event.target.value,targetId:$event.target.id,currentFilterMap:currentFilterMap}">
</div>
</td>
</tr>
<ng-container *ngFor="let task of (currentView.tasks | countryPipe:targetInput); let i=index">
<tr class="worktask" (click)="setCurrentTask($event, task)" (dblclick)="openWindowNewTab(getOpenTaskURL(task, currentView.process))"
id="workspace_table_wo_{{ task.workOrderId }}_task_{{ task.taskId }}"
[class.table-active]="isSelected(task)">
<td *ngFor="let column of currentView.columns">{{task[column.devName]}}</td>
</tr>
Now I decide , that I want a separate component for the input tag , so I split the html and make a parent-child setup and pass the shared variable using #Input decorator,
This is how the new setup looks ,
Parent html:
<tr >
<td *ngFor="let column of currentView.columns">
<filterTagBox [taskCol] = "column" [currentFilterMap] = "currentFilterMap"></filterTagBox>
</td>
</tr>
<ng-container *ngFor="let task of (currentView.tasks | countryPipe:targetInput); let i=index">
<tr class="worktask" (click)="setCurrentTask($event, task)" (dblclick)="openWindowNewTab(getOpenTaskURL(task, currentView.process))"
id="workspace_table_wo_{{ task.workOrderId }}_task_{{ task.taskId }}"
[class.table-active]="isSelected(task)">
<td *ngFor="let column of currentView.columns">{{task[column.devName]}}</td>
</tr>
Now I can't seem to pass the targetInput from the child component back to the parent on the input event, Not sure if this is the way I should implement this or if there is a better way.
I think in your case parent is Parent html and child is filterTagBox. if you want transfer value from parent to child you need use #input
if you want transfer value from child to parent you need use EventEmitter and #Output
more info.
https://angular.io/docs/ts/latest/cookbook/component-communication.html
I use BehaviorSubject to notify any component (the parent in your situation) that subscribes it. It's a special type of observables. A message service can do it for you. Define a message model (you can even use a simple string if you prefer) and create a message service:
import {Observable, BehaviorSubject} from 'rxjs/Rx'; //
import {Message} from "../../models/message"; // Your model
... inside your message service class:
private _newMessage = new BehaviorSubject<Message>(new Message);
getMessage = this._currentUser.asObservable();
sendMessage(message: Message) { this._newMessage.next(message) }
In a component (e.g. in a parent), you can subscribe getMessage subject like this:
this.messageService.getMessage.subscribe(
message => {
// a message received, do whatever you want
if (message == "so important message")
this.list = newList;
// ... so on
});
This way, multiple components can subscribe to this BehaviorSubject, and any trigger in any component/service that uses sendMessage method can change these subscribed components immediately. For you, that can be a child component:
... you successfully made something in your
... child component, now use the trigger:
this.messageService.sendMessage(new Message("so important message", foo, bar));
Thanks for the answers , I did figure out how I could do this ,and although I found using behaviour service interesting I decided to use a output variable to sent data form the child component to the parent which would ultimatedly sent to the pipe,
Here is what I did :
Child component HTML:
<div *ngIf="taskCol.label">
<div id="{{taskCol._devName}}_tagBox"></div>
<input placeholder="{{taskCol.label}}" id="{{taskCol._devName}}" type="text"
<!-- Call childComponent.onInput passing event parameters -->
(input)="onInput({targetValue:$event.target.value,targetId:$event.target.id})">
</div>
Child component.ts :
#Component({
selector: 'filterTagBox',
template: require('./filterTagBox.component.html')
})
export class FilterTagBox{
private colValues:string[];
public containsQueries:boolean;
private regex:RegExp;
#Input() public taskCol:TaskColumn;
#Output() onItemInput = new EventEmitter<any>(); // bound event to the parent component
// constructor and other hidden methods...
onInput(targetInput : any){
this.onItemInput.emit(targetInput); //trigger onItemInput event on inputBox input
}
}
Parent component html :
<tr >
<td *ngFor="let column of currentView.columns">
<!-- Catch custom onItemInput event which was triggered in the child -->
<filterTagBox (onItemInput)="filterBoxPipeData = {targetValue:$event.targetValue,targetId:$event.targetId,currentFilterMap:currentFilterMap}" [taskCol] = "column" ></filterTagBox>
</td>
</tr>
<!--sent the data retrieve from the input i.e filterBoxPipeData to the pipe i.e tagBoxFilterPipe along with data to be filtered i.e currentView.task -->
<ng-container *ngFor="let task of (currentView.tasks | tagBoxFilterPipe:filterBoxPipeData); let i=index">
<tr>
<!--hidden html -->
</tr>

Categories

Resources