Create repeatable Angular component with two rows for tables - html

I have two components. The first one represents a table of items and the second one represents one item. The first one is repeating the second one many times.
The List Component (app-list):
<table>
<tr *ngFor="let item of items" [item]="item" app-item></tr>
</table>
The Item Component (app-item):
<td>
<img src="https://someimage.com/{{item.img}}.jpg">
</td>
<td>
<h3>{{item.name}}</h3>
</td>
<td>
{{item.description}}
</td>
In order for this to work, I had to use an attribute selector for the app-item component:
#Component({
selector: '[app-item]'
})
This works perfectly.
Now I want to improve it and add a second row in each app-item. My problem is that the tr tag lies in the app-list component instead of the app-item component. I thought that if I move it to the app-item component, I could add another tr and be able to show two rows per one item. So this is what I did. After that I used ng-container to repeat the items in my app-list, in order to avoid adding a wrapper tag around my two rows:
<ng-container *ngFor="let item of items" [item]="item" app-item></ng-container>
This solution did not work. I got the following error:
ERROR TypeError: el.setAttribute is not a function
at EmulatedEncapsulationDomRenderer2.push../node_modules/#angular/platform-browser/fesm5/platform-browser.js.DefaultDomRenderer2.setAttribute (platform-browser.js:1089)
at EmulatedEncapsulationDomRenderer2.push../node_modules/#angular/platform-browser/fesm5/platform-browser.js.EmulatedEncapsulationDomRenderer2.applyToHost (platform-browser.js:1157)
at DomRendererFactory2.push../node_modules/#angular/platform-browser/fesm5/platform-browser.js.DomRendererFactory2.createRenderer (platform-browser.js:1015)
Can you help me resolve this error or suggest another implementation?
EDIT: SOLUTION
The better version #Serhiy is suggesting
The table:
<table>
<app-item *ngFor="let item of items" [item]="item" remove-component-tag></app-item>
</table>
The directive:
import { Directive, ElementRef } from '#angular/core';
#Directive({
selector: '[remove-component-tag]'
})
export class RemoveComponentTagDirective {
constructor(private el: ElementRef) {
let element = el.nativeElement;
let children = el.nativeElement.childNodes;
setTimeout(()=>{
let reversedChildren = [];
children.forEach(child => {
reversedChildren.unshift(child);
});
reversedChildren.forEach(child => {
element.parentNode.insertBefore(child, element.nextSibling);
});
element.remove(element);
}, 0);
}
}
The timeout is necessary for some reason and works even with 0.

I can't see the right "angular" way to do it, but you should be able to use directives to clear your html during render.
Saw this approach in comments here: Angular2 : render a component without its wrapping tag
I tried that and it worked for me:
Parent component:
<table>
<div *ngFor="let item of items">
<app-item [item]="item" remove-wrapper></app-item>
</div>
</table>
Child component:
<tr>
<td>
<img src="https://someimage.com/{{item.img}}.jpg">
</td>
<td>
<h3>{{item.name}}</h3>
</td>
<td>
{{item.description}}
</td>
</tr>
<tr>
<td>
<img src="https://someimage.com/{{item.img}}.jpg">
</td>
<td>
<h3>{{item.name + ' 2'}}</h3>
</td>
<td>
{{item.description + ' 2'}}
</td>
</tr>
Directive:
#Directive({
selector: '[remove-wrapper]'
})
export class RemoveWrapperDirective {
constructor(private el: ElementRef) {
let parent = el.nativeElement.parentElement;
let children = el.nativeElement.childNodes;
setTimeout(()=>{
parent.parentNode.insertBefore(children[1], parent.nextSibling);
parent.parentNode.insertBefore(children[0], parent.nextSibling);
parent.remove(parent);
}, 10);
}
}
Without a timeout, it crashed for me. The code can be improved, but you can start from here.

thank you for that solution.
I want to add a correction for your code to avoid the usage of the setTimeout function.
Implementing the OnInit interface for the directive and move the code from the constructor to the ngOnInit method will keep the code clean.
import { Directive, OnInit, ElementRef } from '#angular/core';
#Directive({
selector: '[remove-component-tag]'
})
export class RemoveComponentTagDirective implements OnInit{
constructor(private el: ElementRef) { }
ngOnInit() {
let element = this.el.nativeElement;
let children = this.el.nativeElement.childNodes;
let reversedChildren = [];
children.forEach(child => {
reversedChildren.unshift(child);
});
reversedChildren.forEach(child => {
element.parentNode.insertBefore(child, element.nextSibling);
});
element.remove(element);
}
}
Take a look at Angular lifecycle hooks

Would using component syntax instead of directive syntax help here?
Instead of:
<ng-container *ngFor="let item of items" [item]="item" app-item></ng-container>
Try:
<ng-container *ngFor="let item of items">
<app-item [item]="item"></app-item>
</ng-container>

Related

Using selector in HTML (Angular) doesn't follow structure of table [duplicate]

I am experimenting with angular2 2.0.0-beta.0
I have a table and the line content is generated by angular2 this way:
<table>
<tr *ngFor="#line of data">
.... content ....
</tr>
</table>
Now this works and I want to encapsulate the content into a component "table-line".
<table>
<table-line *ngFor="#line of data" [data]="line">
</table-line>
</table>
And in the component, the template has the <tr><td> content.
But now the table does no more work. Which means, the content is no longer shown in columns.
In the browser, the inspector shows me that the DOM elements look like this:
<table>
<table-line ...>
<tbody>
<tr> ....
How can I make this work?
use existing table elements as selector
The table element doesn't allow <table-line> elements as children and the browser just removes them when it finds them. You can wrap it in a component and still use the allowed <tr> tag. Just use "tr" as selector.
using <template>
<template> should be allowed as well but doesn't yet work in all browsers. Angular2 actually never adds a <template> element to the DOM, but only processes them internally, therefore this can be used in all browsers with Angular2 as well.
Attribute selectors
Another way is to use attribute selectors
#Component({
selector: '[my-tr]',
...
})
to be used like
<tr my-tr>
I found the example very usefull but it didn't work in the 2,2.3 build, so after much head scratching made it work again with a few small changes.
import {Component, Input} from '#angular/core'
#Component({
selector: "[my-tr]",
template: `<td *ngFor='let item of row'>{{item}}</td>`
})
export class MyTrComponent {
#Input("line") row:any;
}
#Component({
selector: "my-app",
template: `<h1>{{title}}</h1>
<table>
<tr *ngFor="let line of data" my-tr [line]="line"></tr>
</table>`
})
export class AppComponent {
title = "Angular 2 - tr attribute selector!";
data = [ [1,2,3], [11, 12, 13] ];
constructor() { console.clear(); }
}
Here's an example using a component with an attribute selector:
import {Component, Input} from '#angular/core';
#Component({
selector: '[myTr]',
template: `<td *ngFor="let item of row">{{item}}</td>`
})
export class MyTrComponent {
#Input('myTr') row;
}
#Component({
selector: 'my-app',
template: `{{title}}
<table>
<tr *ngFor="let line of data" [myTr]="line"></tr>
</table>
`
})
export class AppComponent {
title = "Angular 2 - tr attribute selector";
data = [ [1,2,3], [11, 12, 13] ];
}
Output:
1 2 3
11 12 13
Of course the template in the MyTrComponent would be more involved, but you get the idea.
Old (beta.0) plunker.
Adding 'display: contents' to the component style worked out for me.
CSS:
.table-line {
display: contents;
}
HTML:
<table>
<table-line class="table-line" [data]="line">
</table-line>
</table>
Why this works?
When instancing a component, angular (after compiling) wraps the content of the component in the DOM as follows:
<table>
<table-line>
<tr></tr>
</table-line>
</table>
But in order for the table to display properly, the tr tags can't be wrapped by anything.
So, we add display: contents, to this new element. As I understand, what this does is to tell the explorer that this tag should not be rendered, and display the inner content as if there was no wrapping around. So, while the tag still exists, it doesn't affect visually to the table, and the tr tags are treated as if they were direct children of the table tag.
If you'd like to investigate further on how contents works:
https://bitsofco.de/how-display-contents-works/
try this
#Component({
selecctor: 'parent-selector',
template: '<table><body><tra></tra></body></table>'
styles: 'tra{ display:table-row; box-sizing:inherit; }'
})
export class ParentComponent{
}
#Component({
selecctor: 'parent-selector',
template: '<td>Name</td>Date<td></td><td>Stackoverflow</td>'
})
export class ChildComponent{}

Angular Input value doesn't change

I have a problem. In my Angular project I have 2 components with an two-way bound parameter. The parameter is an object id. Both components use the same service which stores the list with objects. The parent contains the list where you can select the object and the child shows the selected object details. Here is the code of the parent:
<div>
<table>
<tr>
<th scope="col">Title</th>
</tr>
<tr *ngFor="let offer of service1.findAll();"
[style.background-color]="offer.rowClicked ? '#ed9b82' : ''"
(click)="highlightClickedRow(offer)">
<td>{{offer.title}}</td>
</tr>
</table>
</div>
<div>
<app-detail3 [editedOfferId]="offerSelectedId" (editedOfferChanged)="offerSelectedId=$event"></app-detail3>
</div>
with the typescript:
#Component({
selector: 'app-overview3',
templateUrl: './overview3.component.html',
styleUrls: ['./overview3.component.css']
})
export class Overview3Component implements OnInit {
public offerSelectedId: number = -1;
constructor(public service1: OffersService) { }
ngOnInit(): void {
}
public highlightClickedRow(offer :Offer) {
let offers: Offer[] = this.service1.findAll();
for (let i = 0; i < offers.length; i++) {
if (offers[i] != offer) {
offers[i].rowClicked = false;
}
else {
offers[i].rowClicked = true;
this.offerSelectedId = offers[i].id;
}
}
}
}
And here is the detail component:
<div id="content" *ngIf="editedOfferId != -1">
<div id="data-table">
<table>
<tr>
<th scope="row" colspan="2">Selected offer details(id = {{service.findById(editedOfferId)!.id}})</th>
</tr>
<tr>
<th scope="row">Title:</th>
<td><input type="text" id="txtTitle" (input)="checkObjectChanged()"></td>
</tr>
<tr>
<th scope="row">Description:</th>
<td><input type="text" id="txtDescription" (input)="checkObjectChanged()" value="{{service.findById(editedOfferId)!.description}}"></td>
</tr>
<tr>
<th scope="row">Status:</th>
<td>
<select (input)="checkObjectChanged()" id="txtStatus">
<option *ngFor="let key of keys" [ngValue]="key" [value]="status[key]" [label]="status[key]" [selected]="service.findById(editedOfferId)!.auctionStatus === key"></option>
</select>
</td>
</tr>
<tr>
<th scope="row">Highest Bid:</th>
<td><input id="txtHighestBid" (input)="checkObjectChanged()" value="{{service.findById(editedOfferId)!.valueHighestBid}}"></td>
</tr>
</table>
</div>
</div>
<div *ngIf="editedOfferId == -1">
<label id="lblNothingSelected">Nothing has been selected yet</label>
</div>
With the typescript:
#Component({
selector: 'app-detail3',
templateUrl: './detail3.component.html',
styleUrls: ['./detail3.component.css']
})
export class Detail3Component implements OnInit {
#Input() editedOfferId: number = -1;
#Output() editedOfferChanged = new EventEmitter<number>();
public selectedOffer: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);
status = AuctionStatus
keys: Array<number>;
constructor(public service: OffersService) {
this.keys = Object.keys(this.status).filter(k => !isNaN(Number(k))).map(Number);
this.selectedOffer = service.findById(this.editedOfferId)!;
}
ngOnInit(): void {
}
}
Now when I click on an object in the parent component, the details will be loaded in de detail component, but when I edit for example the title input field and then change the object in the parent, I would expect that the data of the new selected object will be loaded. This happens, but only the fields that were not edited at that moment, so when I edit the title, everything will be loaded correctly, but the value of the title will remain the same. Even tough the object has a different title, the value that I was typing stays in the input field. Why is this happening and how can I fix this?
First of all , you did not implement your save changes business logic of your details component , so , when you make some changes in your object , all changes you made will not save then will not be loaded in your parent component using service1. so recommend implementing a save change service request to apply changes .
to do that exactly how you want by your code logic , Use one of those solutions:
Solution 1:
Create an update method for each offer field in your OffersService.
class OffersService {
...
updateTitle(offerId:number,newTitle:string){ ... }
updateDescription(offerId,newDesc:string){ ... }
....
}
In your edit component use onFocuseOut event ( angular focus Out event depend to your angular version ) to each control field , example for title:
<tr>
<th scope="row">Title:</th>
<td><input type="text" id="txtTitle" (focusout)="service1.updateTitle(editedOfferId,$event.target.value)"></td>
</tr>
please do the same thing for each other fields and make sure your update.. methods save offer values as well .
Solution 2:
create a service method in service1 that saves all changes of a specific Offer object. example:
class OffersService {
...
updateOfferById(id:number,newOffer:Offer) { ... }
...
}
make submit changes button in your details component .html , and in its handler implement change detection and call :
service1.updateOfferById(editedOfferId,changedOffer).subscribe( ... );
the difference between the two methods , the first apply changes without using user action to do like submit change button, the input focusOut event do that for each field instead of saving all changes at once .
Instead of using the offerSelectedId in your Overview3Component, change your variable to store the entire object reference.
So instead of public offerSelectedId: number = -1;,
use something like public offerSelected: Offer = new Offer(...)
Then set the selected item (this.offerSelected = offers[i];)
and pass the reference (offerSelected) into the your child component Detail3Component.
#Component({
selector: 'app-overview3',
templateUrl: './overview3.component.html',
styleUrls: ['./overview3.component.css']
})
export class Overview3Component implements OnInit {
public offerSelected: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);
allOffers: Offer[] = [];
constructor(public service1: OffersService) { }
ngOnInit(): void {
this.allOffers = this.service1.findAll();
}
public highlightClickedRow(offer :Offer) {
for (let i = 0; i < this.allOffers.length; i++) {
if (offers[i] != offer) {
offers[i].rowClicked = false;
}
else {
offers[i].rowClicked = true;
this.offerSelected = offers[i]; // set with entire object
}
}
}
}
Remember to make the required changes in the child component as well, changing from just the object ID #Input() editedOfferId: number = -1;
to the object #Input() selectedOffer: Offer;.
#Component({
selector: 'app-detail3',
templateUrl: './detail3.component.html',
styleUrls: ['./detail3.component.css']
})
export class Detail3Component implements OnInit {
// NOTE: This object would be binded back to the source
#Input() selectedOffer: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);
constructor() {
}
ngOnInit(): void {
}
}
Passing the entire object reference like this will allow you to change the values and have them reflected in the parent, in this case the allOffers array.
There should be no need for the EventEmitter #Output() editedOfferChanged = new EventEmitter<number>(); unless you want to know if they changed something.
Just note that this will only update the data in the current state and will not be saved on refresh unless you implement some saving logic.
Also this is by no means the best solution but it is a solution.
You've not implemented save changes logic in the detail3 component and values are not linked to any object or variable. It can be done in multiple ways. You should pass an offer object to the child component instead of an id so that you don't have to find selected offer from all the offers. Bind selected offer object to your forms using ngModel or use reactive forms.
Since data passed to child component is object and any change in object will be reflected to main array (you can prevent that by using spread operator when passing value to child component if you don't want these change to be reflected to main array and fire an event to manually to save data on change).
I would prefer reactive forms and submit/save button to save change. I've implemented an example in stackblitz.
"the value of the title will remain the same"
<tr>
<th scope="row">Title:</th>
<td><input type="text" id="txtTitle" (input)="checkObjectChanged()"></td>
</tr>
Your child component don't assign value for title.
value="{{service.findById(editedOfferId)!.title}}"
It seem the parent component doesn't reload the list offers after child component already updated it.
I suggest you change the function:
(editedOfferChanged)="offerSelectedId=$event"
To
(editedOfferChanged)="onEditedOffer($event)"
For easily debug and do few tasks at parent component after offer was updated at child component.
For Example
onEditedOffer(event){
console.log('onEditedOffer',event;
offerSelectedId = event;
//do something to update list or single offer.
//i choose easy way "reload list offers"
offers: Offer[] = this.service1.findAll();
}
it is possible to simply use Ngmodel with your variable.
[(ngModel)]="YourVar"
in the tag you want and then on the .ts side you can reuse this same variable by using the name of the variable

Angular 6: Making events equally dynamic on *ngFor-dynamically-generated-template-elements

Normally in Angular to show and hide things I create a variable in the component, create a mouseover-mouseout event on the element that toggles that variable, and place an ngIf on any element that I want effected by that event (display/hide).
You can't approach it like this if the template is within an ngFor though. When you hover over any of the dynamically generated elements it will trigger EVERY show/hide. Thus my question is, using Angular star directives how can I make events equally dynamic on *ngFor-dynamically-generated-template-elements so that When I hover over one of those template elements on its corresponding event is emitted? More specifically, (see stackblitz below) how do I get ONE tooltip to appear when I am hovering over its corresponding template event?
How do people get around this. Can you create dynamically generated variable names? Would this even be a scalable approach whne you have hundreds of thousands of rows? Probably not. There must be a way.
Here's my Stackblitz demonstrating what I am talking about.
Template:
<h1>Tool tip example</h1>
<p>
Events on dynamically generated template from *ngFor :)
</p>
<p>
Requirements: Make a tooltip appear with the rest of the information on hover.
</p>
<table>
<tr>
<th>id</th>
<th>name</th>
<th>gender</th>
</tr>
<tr *ngFor="let object of this.data.arrayOfObjects">
<td (mouseenter)="tooltipHover=!tooltipHover" (mouseleave)="tooltipHover=!tooltipHover" class="id-pointer">
{{object.friends.length}}
<div *ngIf="this.tooltipHover" class="tooltip">
Tooltip:
more info here
</div>
</td>
<td>{{object.name}}</td>
<td>{{object.gender}}</td>
</tr>
</table>
Component:
import { Component } from '#angular/core';
import {Data} from './../../data'
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
tooltipHover:boolean =false;
constructor(public data: Data){}
}
You have single variable for entire list, that's why it is triggered for each row.
Instead you should have a list of each row tooltip visibility. Check this stackblitz
I changed your html to following
<tr *ngFor="let object of this.data.arrayOfObjects; let i = index">
<td (mouseenter)="onMouseEnter(i)"
(mouseleave)="onMouseLeave(i)" class="id-pointer">
{{object.friends.length}}
<div *ngIf="tooltipHover[i]" class="tooltip">
Tooltip:
more info here
</div>
</td>
<td>{{object.name}}</td>
<td>{{object.gender}}</td>
</tr>
And your component file to this
tooltipHover: boolean[] = [];
constructor(public data: Data){}
onMouseEnter(index) {
this.tooltipHover[index] = true;
}
onMouseLeave(index) {
this.tooltipHover[index] = false;
}
Try something like this
create empty array
tooltipHover=[]
<tr *ngFor="let object of this.data.arrayOfObjects; let i = index">
<td (mouseenter)="tooltipHover[i]=!tooltipHover[i]" (mouseleave)="tooltipHover[i]=!tooltipHover[i]" class="id-pointer">
{{object.friends.length}}
<div *ngIf="this.tooltipHover[i]" class="tooltip">
Tooltip:
more info here
</div>
</td>
<td>{{object.name}}</td>
<td>{{object.gender}}</td>
</tr>
Example:https://stackblitz.com/edit/angular-xbwxrw

Angular: how to change image in a specific row of table created by *ngFor loop

So, I have a generated by ngFor table.
In each row I have button with image as background:
<tbody>
<tr *ngFor='let movie of movies let i = index'>
<td>...some values here...</td>
<td>
<span>
<button name="watchLater_butom">
<img [src]='watchLaterIcon' alt="icon (click)='AddToWatchLater(i)'>
</button>
</span>
</td>
</tbody>
And my component:
export class MyComponent{
watchLaterIcon: string = '/assets/watchLater_icon.png';
emptyWatchLaterIcon: string = '/assets/emptyWatchLater_icon.png';
AddToWatchLater(i : number) : void {
var tempWatchLaterIcon = this.watchLaterIcon;
this.watchLaterIcon = this.emptyWatchLaterIcon;
this.emptyWatchLaterIcon = tempWatchLaterIcon;
}
}
By now, all the images are changed by clicking any button. What I need is to change only one image src for a button which I clicked.
I suppose it has to be done something in AddToWatchLater() method. I guess, that it might work via index of row which I pass to the method, but have no idea how it works.
In your .ts file, firstly create a new array, each element of this array is going to have watchLaterIcon string, and its length should be the same as the movies array, so add a for loop in the constructor:
imageSources: string[] = [];
constructor()
{
for(let i=0; i<this.movies.length; i++)
{
this.imageSources[i] = watchLaterIcon;
}
}
Then make a new component for the button 'ButtonComponent', and edit your .html file so the ngFor iterates through button component and pass in image sources as inputs to the component:
<table>
<tbody>
<tr *ngFor='let imageSource of imageSources>
<td>...some values here...</td>
<td>
<span>
<app-button [inputImageSrc]="imageSource"><app-button>
</span>
</td>
</tr>
</tbody>
</table>
Button component .HTML file:
<button (click)="getSelected()">
<img src={{imgSrc}} alt="icon">
</button>
Button component .Ts file:
emptyWatchLaterIcon: string = '/assets/emptyWatchLater_icon.png';
#Input('inputImageSrc') imgSrc: string; // get input and use as 'src' in the img tag
getSelected() // Change the image source, applies only to the clicked button of the ngFor loop.
{
this.imgSrc = emptyWatchLaterIcon;
}
You can change dynamically depending of the position of the array data managing it in object per movie, a quick example:
Ts file
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
watchLaterIcon: string = 'https://www.materialui.co/materialIcons/action/watch_later_black_192x192.png';
emptyWatchLaterIcon: string = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT53nVsfr2bXkVsrEZ8Mjsbwk4-dQzfPL1XuVWNPVy5Ex52AQAM';
movies:{} = [{ name: 'Movie 1' }, { name: 'Movie 2' }]
AddToWatchLater(index){
const actualsrc = this.movies[index].src;
if(actualsrc){
this.movies[index].src = actualsrc === this.watchLaterIcon ? this.emptyWatchLaterIcon : this.watchLaterIcon;
}else {
this.movies[index].src = this.emptyWatchLaterIcon;
}
}
}
Html file :
<table>
<tbody>
<tr *ngFor='let movie of movies let i = index'>
<td>...some values here...</td>
<td>
<span>
<button name="watchLater_butom">
<img [src]="movie.src ? movie.src : watchLaterIcon" alt="icon" (click)="AddToWatchLater(i)" >
</button>
</span>
</td>
</tr>
</tbody>
</table>

Angular 2: Remove the wrapping DOM element in a component

I am writing an HTML table component using data that is nested, such that the output might look like this:
<table>
<tr><td>Parent</td></tr>
<tr><td>Child 1</td></tr>
<tr><td>Child 2</td></tr>
<tr><td>Grandchild 1</td></tr>
</table>
I would like to create this using a recursive component as follows:
<table>
<data-row *ngFor="let row of rows" [row]="row"></data-row>
</table>
data-row:
<tr><td>{{row.name}}</td></tr>
<data-row *ngFor="let child of row.children" [row]="child"></data-row>
However, this adds a wrapping element around the table row which breaks the table and is invalid HTML:
<table>
<data-row>
<tr>...</tr>
<data-row><tr>...</tr></data-row>
</data-row>
</table>
Is it possible to remove this data-row wrapping element?
One Solution:
One solution is to use <tbody data-row...></tbody> which is what I'm currently doing, however this leads to nested tbody elements which is against the W3C spec
Other thoughts:
I've tried using ng-container but it doesn't seem to be possible to do <ng-container data-row...></ng-container> so that was a non starter.
I have also considered ditching the use of tables, however using an HTML table is the ONLY way to allow simple copying of the data into a spreadsheet which is a requirement.
The final option I've considered would be to flatten the data before generating the table, however, since the tree can be expanded and collapsed at will, this leads to either excessive rerendering or a very complicated model.
EDIT: Here's a Plunk with my current solution (which is against spec): http://plnkr.co/edit/LTex8GW4jfcH38D7RB4V?p=preview
Just use a class or attribute as the selector for the component and apply it to a table row element.
#Component({
selector: [data-row],
with
<tr data-row> </tr>
or
#Component({
selector: .data-row,
with
<tr class="data-row"></tr>
EDIT - i can only get it to work using content projection in the child component, and then including the td elements inside the components element in the parent. See here - https://plnkr.co/edit/CDU3Gn1Fg1sWLtrLCfxw?p=preview
If you do it this way, you could query for all the rows by using ContentChildren
import { Component, ContentChildren, QueryList } from '#angular/core';
import { DataRowComponent } from './wherever';
somewhere in your component...
#ContentChildren(DataRowComponent) rows: QueryList<DataRowComponent>;
That will be defined in ngAfterContentInit
ngAfterContentInit() {
console.log(this.rows); <-- will print all the data from each component
}
Note - you can also have components that recurse (is that a word?) themselves in their own templates. In the template of data-row component, you have any number of data-row components.
I found a solution from another stackoverflow thread, so I can't take credit, but the following solution worked for me.
Put :host { display: contents; } into the data-row component .css file.
If you wrap row components in a ng-container you should be able to get it done
<tbody>
<ng-container *ngFor="let row of rows; let i = index">
<data-row [row]="row"></data-row>
</ng-container>
</tbody>
#Component({
selector: 'my-app',
template: `
<table>
<ng-container *ngFor="let row of table">
<tbody data-table-row [row]="row"></tbody>
</ng-container>
</table>
`,
})
export class App {
table = [
{
name: 'Parent',
children: [
{
name: 'Child 1'
children: []
},
{
name: 'Child 2'
children: [
{
name: 'Grandchild 1'
children: []
}
]
}
]
}
]
}
#Component({
selector: 'tbody[data-table-row]',
template: `
<tr><td>{{row.name}}</td></tr>
<tbody *ngFor="let child of row.children" data-table-row [row]="child"></tbody>
`
})
export class DataTableRowComponent {
#Input() row: any;
}
posting another answer just to show what i was talking about ... I'll leave you alone after this, promise. Heh.
http://plnkr.co/edit/XcmEPd71m2w841oiL0CF?p=preview
This example renders everything as a flat structure, but retains the nested relationships. Each item has a reference to its parent and an array of its children.
import {Component, NgModule, VERSION, Input} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
#Component({
selector: 'my-app',
template: `
<table *ngIf="tableReady">
<tr *ngFor="let row of flatList" data-table-row [row]="row"> </tr>
</table>
`,
})
export class App {
tableReady = false;
table = [
{
name: 'Parent',
age: 70,
children: [
{
name: 'Child 1',
age: 40,
children: []
},
{
name: 'Child 2',
age: 30,
children: [
{
name: 'Grandchild 1',
age: 10,
children: []
}
]
}
]
}
];
flatList = [];
ngOnInit() {
let flatten = (level : any[], parent? :any ) => {
for (let item of level){
if (parent) {
item['parent'] = parent;
}
this.flatList.push(item);
if (item.children) {
flatten(item.children, item);
}
}
}
flatten(this.table);
this.tableReady = true;
}
}
#Component({
selector: '[data-table-row]',
template: `
<td>{{row.name}}</td><td>{{row.age}}</td>
`
})
export class DataTableRowComponent {
#Input() row: any;
}