Rendering TRs in Vue using a custom component - html

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.

Related

How to display Markdown files containing HTML syntax in Gatsby?

I am using Markdown file to generate pages for gatby. In order to control the style of pictures, I use html syntax. However, the page generated by gatsby does not display the html part.
This is my markdown file:
---
......frontmatter......
---
......content......
<table>
<tr>
<td><img src="./images/2018/zotero/ZoteroWebDAV.png"></td>
<td><img src="./images/2018/zotero/ZoteroExts.png" width=100%></td>
</tr>
</table>
......content......
Everything else is rendered normally, however, neither the table nor the pictures in it are displayed. Here is my gatsby-config.js.
{
resolve: `gatsby-transformer-remark`,
options: {
excerpt_separator: `<!-- endexcerpt -->`,
plugins: [
// 'gatsby-remark-relative-images',
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: 1200,
},
},
{
resolve: `gatsby-remark-image-attributes`,
options: {
dataAttributes: true
}
},
],
},
},
What can I do to make the html part in Markdown render normally?
You can use as well the built-in dangerouslySetInnerHtml property or any markdown parser like markdown-to-jsx.
Using the first approach, following Gatsby's guides:
import React from "react"
import { graphql } from "gatsby"
export default function Template({data}) {
const { markdownRemark } = data // data.markdownRemark holds your post data
const { frontmatter, html } = markdownRemark
return (
<div className="blog-post-container">
<div className="blog-post">
<h1>{frontmatter.title}</h1>
<h2>{frontmatter.date}</h2>
<div
className="blog-post-content"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
</div>
)
}
export const pageQuery = graphql`
query($id: String!) {
markdownRemark(id: { eq: $id }) {
html
frontmatter {
date(formatString: "MMMM DD, YYYY")
slug
title
}
}
}
`
Because you haven't shared your query I've used the one in the guide but tweak it as you wish. As you can see, everything that is in the end of the frontmatter is html:
<div
className="blog-post-content"
dangerouslySetInnerHTML={{ __html: html }}
/>
Using the second approach, and following the previous query structure, the html should be rendered as:
import Markdown from 'markdown-to-jsx';
import React from 'react';
<Markdown>{html}</Markdown>
If there's any hindrance I'd say that the second approach is better because, as the dangerouslySetInnerHTML name suggests, you are potentially exposing your site to XSS (Cross-Site Scripting), while the second approach sanitizes the implementation.

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>

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

How to add button or images to dojo grid

I have a dojo grid with a json datastore (mysql resultset converted into json format). Currently my grid show 5 columns as shown below in the figure:
I have column named 'Action'. The rows under this 'Action' column should contain buttons or images(edit icon, delete icon) with hyperlinks such as edit.php?id=1 for edit, or delete.php?id=1 for delete.
Here is my dojo grid code:
<span dojoType="dojo.data.ItemFileReadStore" data-dojo-id="studentsStore" url="http://localhost/newsmis/public/studentManagement/student/fetchdata/data/1"></span>
<table dojoType="dojox.grid.DataGrid" id="studentsGrid" data-dojo-id="studentsGrid" columnReordering="true" sortFields="['idstudents','first_name','middle_name','last_name']" store="studentsStore" clientSort="true" selectionMode="single" rowHeight="25" noDataMessage="<span class='dojoxGridNoData'>No students found</span>">
<thead>
<tr>
<th field="idstudents" width="20%">Student ID</th>
<th field="first_name" width="20%">First Name</th>
<th field="middle_name" width="20%">Middle Name</th>
<th field="last_name" width="20%">Last Name</th>
<th field="action" width="20%">Action</th>
</tr>
</thead>
</table>
My json data format is
{"identifier":"idstudents","items":[{"idstudents":"11","first_name":"Pradip","middle_name":"Maan","last_name":"Chitrakar"}]}
How can i do it? Please suggest me some ideas
The one way I know is, that defining formatting method for that column in grid structure. So instead of defining the structure of the grid declaratively, define in JavaScript object like below
var structure = [
{
name: "First Name",
field: "first_name"
},
{
name: "Action",
field: "_item",
formatter: function(item){
var btn = new dijit.form.Button({
label: "Edit"
});
return btn;
}
}
]
and set this structure to the grid
<table dojoType="dojox.grid.DataGrid" id="studentsGrid" data-dojo-id="studentsGrid" columnReordering="true" sortFields="['idstudents','first_name','middle_name','last_name']" store="studentsStore" clientSort="true" selectionMode="single" rowHeight="25" noDataMessage="<span class='dojoxGridNoData'>No students found</span>" structure=structure >
Here is the working example,
Image in dojo Grid
As documentation of dojox.grid.DataGrid stays:
Beginning with Dojo 1.7, you should use dgrid or gridx, next-generation grid components that take full advantage of modern browsers and object stores.
piece of code example:
columns: [
{
field: "id",
label: "ID"
},
{
field: "name",
label: "Name"
},
{
field: "options",
label: "Options",
renderCell: function (obj) {
var cellContent = domConstruct.create("div",{});
var btn = new Button({
label: "Cell " + obj.id,
name: "idBtn"
})
btn.placeAt(cellContent);
on(btn, "click", function (evt) {
console.log(obj);
});
return cellContent;
}
}
]
This is JSfiddle example how to do it in dgrid by using function renderCell in column properties of dgrid.
renderCell(object, value, node, options) - An optional function that will be called to render the value into the target cell. object refers to the record from the grid’s store for the row, and value refers to the specific value for the current cell (which may have been modified by the column definition’s get function). node refers to the table cell that will be placed in the grid if nothing is returned by renderCell; if renderCell returns a node, that returned node will be placed in the grid instead. (Note: if formatter is specified, renderCell is ignored.)
If you don't want a button, but only an image icon, you can return a span-element from the formatter function like this:
formatter: function(value) {
var myValueClass = "dijitNoValue";
...
myValueClass = "ValueClass1";
...
return "<span class='dijitReset dijitInline dijitIcon " + myValueClass + "'></span>";
}
An css class has to be defined like
.ValueClass1 {
background-image: url('val_image1.png');
background-repeat: no-repeat;
width: 18px;
height: 18px;
text-align: center;
background-position: 1px;
}