Angular 2: Remove the wrapping DOM element in a component - html

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;
}

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 no access to object.id

hey i just started with angular and my problem is, why i have no access to my .id,.name and .color, the first code is my oberteile-names.ts where i have defined my const array
import { Oberteil } from './oberteile/oberteil';
export const OBERTEILE: Oberteil[] = [
{id:11, name: 'flannelshirt', color: 'vintagebrown'},
{id:12, name: 'flannelshirt', color: 'vintagegreen'},
{id:13, name: 'flannelshirt', color: 'vintagewhite'},
{id:14, name: 'cordhemd', color: 'black'},
{id:15, name: 'cordhemd', color: 'vintagebrown'},
{id:16, name: 'cordhemd', color: 'vintagegreen'},
{id:17, name: 'basicshirt', color: 'black'},
{id:18, name: 'basicshirt', color: 'white'}
];
here is my simple oberteil.ts
export interface Oberteil {
id: number;
name: string;
color : string;
}
so now i go into my oberteile.component.ts and initialize oberteil with the const OBERTEILE
import { Component, OnInit } from '#angular/core';
import { OBERTEILE } from '../oberteil-names';
#Component({
selector: 'app-oberteile',
templateUrl: './oberteile.component.html',
styleUrls: ['./oberteile.component.css']
})
export class OberteileComponent implements OnInit {
oberteil = OBERTEILE;
constructor() { }
ngOnInit(): void {
}
}
now i want to display them on my oberteile.component.html but i have no access to id
<h2>{{oberteil.id}}</h2>
i think it is very easy to be solved but i can not find any answers for it, even when i just want to display {{oberteile}}, it just display [object,Object]
oberteil is an array of multiple elements. You need to select a specific element of the array, or iterate over the array with *ngFor:
<h2>{{oberteil[0].id}}</h2><!-- display the ID of the first element -->
<h2 *ngFor="let element of oberteil">{{element.id}}</h2><!-- display IDs of all elements -->
oberteil is an array. You can't access it like that. You need to specify the index of the array.
Or if you are trying to show the id from each element of oberteil array, you can use *ngFor directive.
<h1 *ngFor="let o of oberteil">{{o.id}}</h1>
See *ngFor
oberteil is array, no Object, so you cannot bind the value with <h2>{{oberteil.id}}</h2>
It should be like that in *ngFor iteration to get all values
<div *ngFor="let item of oberteil">
<p>{{item.id}}</p>
<p>{{item.name}}</p>
<p>{{item.color}}</p>
</div>
It is just example to use your oberteil variable in html to bind the data.

In Angular how do I dynamically wrap certain words in another html element?

I have this simple Angular Component:
#Component({
selector: 'my-component',
template: '<p>{{someString}}</p>',
})
export class MyComponent {
#Input() someString: string;
}
someString could be any string of any length. As an example, imagine that someString's value is :
"If you want my body and you think I'm sexy, come on, sugar, tell me so."
In that case the HTML generated by Angular would essentially be equivalent to:
<p>If you want my body and you think I'm sexy, come on, sugar, tell me so.</p>
How would I modify MyComponent so that it detects each occurrence of the word sexy in someString and have Angular wrap that word in another HTML element such as <b>. So in this example case it would generate something like:
<p>If you want my body and you think I'm <b>sexy</b>, come on, sugar, tell me so.</p>
What if I wanted to wrap every occurrence of the word sexy in an Angular Component instead of a native HTML Element? Would that necessitate a different approach?
You can try this :D
#Component({
selector: 'app-test',
template: `
<p [innerHTML]="stringFormatted()"></p>
`,
styles: []
})
export class TestComponent {
someString = "If you want my body and you think I'm sexy, come on, sugar, tell me so.";
stringFormatted() {
return this.someString.replace(/sexy/g, '<b>sexy</b>');
}
}
You can use the something like below - wherein after rendering the main sentence, you can replace the special word with a span element and apply a CSS class, say .special to that span tag.
import { Component, Input, ElementRef, AfterViewInit } from '#angular/core';
#Component({
selector: 'my-component',
template: '<p>{{sentence}}</p>'
})
export class MyComponent implements AfterViewInit {
#Input() sentence: string;
#Input() specialWord: string;
constructor(private el: ElementRef) {
}
ngAfterViewInit() {
this.el.nativeElement.innerHTML = this.el.nativeElement.
innerHTML.replace(new RegExp(`${this.specialWord}`, 'g'),
`<span class="special">${this.specialWord}</span>`);
}
}
To keep your code generic, you can use additional #Input() for special word.
In your application's styles.scss, you can define the CSS class .special.
.special {
font-weight: bold;
}
If you wonder why you can't use similar logic to replace the content of sentence by something like below:
this.sentence = this.sentence.replace(new RegExp(`${this.specialWord}`, 'g'),
`<span class="special">${this.specialWord}</span>`);
then, note that Angular will escape the HTML tags and they will appear as is in the output. So you will see something like this in the browser, instead of styled spans.
Hello, it's a <span class="special">beautiful</span> day and I am in a very <span class="special">beautiful</span> city
That's why, I had to resort to manipulating the innerHTML so that replacement is done after Angular has rendered the sentence to DOM.
This solution avoids using innerHTML and relies strictly on Angular constructs.
#Component({
selector: 'my-component',
template: `
<p>
<ng-container *ngFor="let segment of segments">
<span *ngIf="!segment.shouldBeBold()">segment.text</span>
<b *ngIf="segment.shouldBeBold()">segment.text</b>
</ng-container>
</p>
`,
})
export class MyComponent {
#Input() someString: string;
private wordsToBold = new Set(['sexy', 'body', 'sugar']);
get segments() {
const regex = this.makeRegex();
const segments = this.someString.split(regex);
return segments.map(segment => {
return { text: segment, shouldBeBold: wordsToBold.has(segment.toLowerCase()) };
});
}
private makeRegex() {
const expression = [...this.wordsToBold].join('\\b|\\b');
return new RegExp(`(\\b${expression}\\b)+`, 'gi');
}
}

Create repeatable Angular component with two rows for tables

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>

Rendering TRs in Vue using a custom component

While I can render an HTML table fine using v-for and inline mustache syntax, I cannot achieve the same result using a component.
Vue / the browser removes the wrapping TABLE tag and inserts TRs outside a TABLE context, so they do not render properly:
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<h2>Vue table rows inline (works)</h2>
<table border="1">
<tr>
<th>Name</th>
<th>Value</th>
</tr>
<tr v-for="(row, index) in mydata">
<td>{{row.name}}</td>
<td>{{row.value}}</td>
</tr>
</table>
<h2>Vue table rows using component (broken)</h2>
<table border="1">
<tr>
<th>Name</th>
<th>Value</th>
</tr>
<my-row v-for="(row, index) in mydata" :key="row.name" :name="row.name" :val="row.value"></my-row>
</table>
</div>
<script>
Vue.component('my-row', {
props: ['name', 'val'],
template: '<tr><td>{{name}}</td><td>{{val}}</td></tr>'
})
new Vue({
el: "#app",
data: {
mydata: [{
name: "A",
value: 1
},
{
name: "B",
value: 2
},
{
name: "C",
value: 3
}
]
}
})
</script>
You can see this also at https://jsfiddle.net/MCAU/eywraw8t/128217/
What do I need to do to get the component version to work? (Adding a TBODY doesn't make any difference.)
Oh I have now found https://v2.vuejs.org/v2/guide/components.html#DOM-Template-Parsing-Caveats which explains that TRs are a special case and require the following syntax instead:
<tr is="my-row" v-for="(row, index) in mydata" :key="row.name" :name="row.name" :val="row.value"></tr>
using functionnal component as suggested in : Vue js error: Component template should contain exactly one root element may do the trick.
copy/paste here :
if, for any reasons, you don't want to add a wrapper (in my first case it was for <tr/> components), you can use a functionnal component.
Instead of having a single components/MyCompo.vue you will have few files in a components/MyCompo folder :
components/MyCompo/index.js
components/MyCompo/File.vue
components/MyCompo/Avatar.vue
With this structure, the way you call your component won't change.
components/MyCompo/index.js file content :
import File from './File';
import Avatar from './Avatar';
const commonSort=(a,b)=>b-a;
export default {
functional: true,
name: 'MyCompo',
props: [ 'someProp', 'plopProp' ],
render(createElement, context) {
return [
createElement( File, { props: Object.assign({light: true, sort: commonSort},context.props) } ),
createElement( Avatar, { props: Object.assign({light: false, sort: commonSort},context.props) } )
];
}
};
And if you have some function or data used in both templates, passed them as properties and that's it !
I let you imagine building list of components and so much features with this pattern.