I want to use Angular Material library and build my own library with some custom designs. But facing some problems while splitting the material components. I think the problem is with shadow DOM. Here is the code that i want to achieve.
Code
custom-tabs-group.html -parent
<div class="custom-tabs">
<mat-tab-group disableRipple>
<ng-content></ng-content>
</mat-tab-group>
</div>
custom-tabs.html -child
<custom-tabs-group [tabContent]="tabContent">
<mat-tab *ngFor="let tab of tabContent" label="{{tab.title}}">{{tab.content}} </mat-tab>
</custom-tabs-group>
is it even possible? Please let me know
the code you shared got the ng-content usage backwards... the <custom-tabs-group> will be at the parent level and <ng-content> at the child level.
I tried 2 approaches:
strategy #1: pass the content to the custom child inside the <mat-tab>... this worked
strategy #2: pass the content to the custom child where <mat-tab> is inside the child... this didn't work
you can check the demo here
Actually i figured it out with some hack i don't know if its a good approch or not
custom-tabs.component.html
<div class="custom-tabs">
<mat-tab-group disableRipple>
<mat-tab *ngFor="let tab of tabsContentList" label="{{tab.label}}">
<div [innerHTML]="tab.htmlContent"></div>
</mat-tab>
</mat-tab-group>
</div>
custom-tabs-component.ts
import { DomSanitizer } from '#angular/platform-browser';
import { Component, OnInit, ViewEncapsulation, AfterContentInit, ContentChildren, Input, ViewChild, ElementRef, QueryList } from '#angular/core';
#Component({
selector: 'il-tabs-content',
template: `
<div #content>
<ng-content></ng-content>
</div>
`
,
})
export class TabsContentComponent implements OnInit {
#Input() label: String;
#ViewChild('content') set content(content: ElementRef) {
console.log("block three", content)
this.htmlContent = content;
if (this.htmlContent) {
this.htmlContent = this.htmlContent.nativeElement.innerHTML;
}
}
htmlContent: any;
constructor() { }
ngOnInit() {
}
}
#Component({
selector: 'il-tabs-group',
templateUrl: './tabs.component.html',
styleUrls: ['./tabs.component.css'],
encapsulation: ViewEncapsulation.None
})
export class TabsGroupComponent implements OnInit, AfterContentInit {
#ContentChildren(TabsContentComponent) tabsContentList: QueryList<TabsContentComponent>;
constructor(public sanitizer: DomSanitizer) { }
ngOnInit() {
}
ngAfterContentInit() {
this.tabsContentList.forEach((tabInstance) => {
var sanEle = this.sanitizer.bypassSecurityTrustHtml(tabInstance.htmlContent)
tabInstance.htmlContent = sanEle;
return tabInstance
})
}
}
usage
<il-tabs-group>
<il-tabs-content label="hello-1">
<h1>hello-1 content</h1>
</il-tabs-content>
<il-tabs-content label="hello-2">
<h1>hello-2 content</h1>
</il-tabs-content>
<il-tabs-content label="hello-3">
<h1>hello-3 content</h1>
<h2>extra content</h2>
</il-tabs-content>
</il-tabs-group>
i defined two components 'il-tabs-content' and 'li-tabs-group'. with this now i can use my own custom tabs build over angular material tabing with dynamic tabs. Anyone with better approch are welcome to share their ideas. thanks
Related
//This is my html file ,Here i declared "#bodyText" and #truncated to access the element in the component but showing undefined in onInit() hook, however it is working fine in ngAfterView() component.
<div class="note-card-content">
<h1 class="note-card-title">{{title}}</h1>
<div #bodyText class="note-card-body">
<p>{{body}}</p>
<div #truncator class="fade-out-truncation"></div>
</div>
</div>
<div class="x-button"></div>
// This is the component
import { Component, ElementRef, Input, OnInit, Renderer2, ViewChild } from '#angular/core';
#Component({
selector: 'app-note-card',
templateUrl: './note-card.component.html',
styleUrls: ['./note-card.component.scss']
})
export class NoteCardComponent implements OnInit {
//These two elements are showing undefined when I print on console
#ViewChild('truncator') truncator:ElementRef<HTMLElement>;
#ViewChild('bodyText') bodyText:ElementRef<HTMLElement>;
#Input() title:string;
#Input() body:string
constructor(private renderer:Renderer2) { }
ngOnInit(): void {
console.log(this.truncator)
// Work out if there is a text overflow and if so,then hide
let style = window.getComputedStyle(this.bodyText.nativeElement,null);
let viewableHeight = parseInt(style.getPropertyValue("height"),10)
if(this.bodyText.nativeElement.scrollHeight > viewableHeight){
// if there is no text overflow ,show the fade out truncator
this.renderer.setStyle(this.truncator.nativeElement,'display','block')
}else{
// else (there is a text overflow)
this.renderer.setStyle(this.truncator.nativeElement,'display','none')
}
}
}
And what's the problem? :)
This is a normal behaviour. ngOnInit(){} runs before Angular initializes the component's views and child views. You shouldn't use ngOnInit for this purpose. I suggest you to take a look at Lifecycle hooks, Angular.
I am trying to render my angular components dynamically using loops but I'm lost.
What I want to achieve is drag and re-arrange my components (I was able to achieve this using Dragula - ng2-dragula) but I want to save the arrangement to my local storage or session storage.
The tricky part is I am using child components
<section [dragula]="'VAMPIRES'">
<div class="first_element_class" id="first_element">
<my-first-component
[outputData]="outputData"></my-first-component>
</div>
<div class="second_element_class" id="second_element">
<my-second-component
[outputData]="outputData"></my-second-component>
</div>
</section>
I tried using the DOM sanitizer pipe to render them via for loops
import { Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer, SafeHtml } from '#angular/platform-browser';
#Pipe({
name: 'trustHtml'
})
export class TrustHtmlPipe implements PipeTransform {
constructor(readonly sr: DomSanitizer){}
transform(html: string) : SafeHtml {
return this.sr.bypassSecurityTrustHtml(html);
}
}
Updated HTML Code (Let's assume I added my html elements to an array of object)
objectHTML: any[] = [{
toBeRender: `<div class="first_element_class" id="first_element">
<my-first-component
[outputData]="outputData"></my-first-component>
</div>`
}];
<section [dragula]="'VAMPIRES'">
<div *ngFor="let element of objectHTML">
<div [innerHTML]="element.toBeRender | trustHtml" ></div>
</div>
</section>
My question is is it really possible to render the html child components/elements using ngFor? I tried using the DOMSanitizer but I'm only getting a blank page (No error on console)
I have a similar solution for you. Firstly, in your <my-first-component> ts file declare "outputData" as #Input and use ngFor in HTML. Here is my sample code below.
CUSTOM Control
HTML: event.component.html
<h2>Event {{ event.id }}</h2>
<button (click)="event.hidden = true">Close me</button>
TS:event.component.ts
import { Component, Input } from '#angular/core';
import { Event } from './event';
#Component({
selector: 'my-event',
templateUrl: './event.component.html'
})
export class EventComponent {
#Input() event: Event;
}
export interface Event
{
id: number;
hidden: boolean;
}
**HTML:** app.component.html (You can use that code where you need to.)
<button (click)="addEvent()">Add event</button>
<div *ngFor="let event of events">
<my-event *ngIf="!event.hidden" [event]="event"></my-event>
</div>
Note: Here [event] is the #Input for your custom control and *ngFor is the loop.
TS: app.component.ts
import { Component } from '#angular/core';
import { Event } from './event';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
events: Array<Event> = [1, 2, 3, 4, 5].map(i => ({id: i, hidden: true}));
addEvent() {
const event = this.events.find(e => e.hidden);
if (event) {
event.hidden = false;
}
}
}
Note: Please check the code and let me know. This code is also available in Stackblitz
LINK .
You can iterate with ngFor and render many times the child component by his selector.
components = [1, 2, 3, 4, 5];
<ng-container *ngFor="let component of components">
<child-component></child-component>
</ng-container>
I am using Angular Material Tabs to navigate different sections of a document. I've defined each tab in a TabItem class which looks like this:
class TabItem {
constructor(
public component: Type<any>,
public data: TabData,
public active: boolean
) {}
}
In the view, I loop through each TabItem and use *ngComponentOutlet to render the component of each TabItem.
<mat-tab-group>
<ng-container *ngFor="let tab of tabs">
<mat-tab>
<ng-template mat-tab-label>
<div class="mat-label-text" (click)="setActiveTab(tab)">{{ tab.data.label }}</div>
</ng-template>
<ng-container *ngComponentOutlet="tab.component"></ng-container>
</mat-tab>
</ng-container>
</mat-tab-group>
Everything works well... except I need access to the current TabItem in each of the resolved components to access its id, label, etc. The reason I am having trouble is because the examples online only show how to use ngComponentOutlet as a dynamic component. My components arent dynamic though... they are fixed, but created on the fly.
I dont know how I can use an injector since I am in a for loop... unless I create an injector for each individual item. I also dont want to subscribe to a service in every component... thats just ridiculous.
Here is a stackblitz of what I am trying to accomplish.
You can create a directive that will port desired data to your components:
data-provider.directive.ts
import { Directive, Input } from "#angular/core";
#Directive({
selector: '[dataProvider]'
})
export class DataProviderDirective {
#Input('dataProvider') data: any;
}
tabs.html
<ng-container *ngFor="let tab of tabs">
<mat-tab [dataProvider]="tab">
Now your dynamically generated component can read data from that directive:
tab-one.component.ts
import { Component, OnInit } from '#angular/core';
import { DataProviderDirective } from './data-provider.directive';
#Component({
selector: 'app-tab-one',
template: `
<p>I am tab one!</p>
<p>How can I access my respective TabItem?</p>
<pre>{{ dataProvider.data | json }}</pre>
`,
})
export class TabOneComponent implements OnInit {
constructor(public dataProvider: DataProviderDirective) { }
ngOnInit() {
console.log(this.dataProvider.data)
}
}
Forked Stackblitz
I have an Angular2 application and I want to use Semantic UI. However, there are some jQuery configurations like below that I have to run after a component loaded:
$('#app .ui.sidebar')
.sidebar({context:$('#app')})
.sidebar('setting', 'transition', 'overlay')
It is not working by importing the js file in the head of index.html or writing it in a <script> tag inside of a component template. Is there a "typescript way" to do that or how can I use a js file inside of a component?
I found this link about using jQuery in directives, then I created a sidebar directive:
import {Directive, ElementRef, OnDestroy, OnInit, Input} from '#angular/core';
import {HostListener} from "#angular/core/src/metadata/directives";
declare var $: any
#Directive({
selector: '.ui.sidebar'
})
export class SidebarDirective implements OnInit, OnDestroy {
#Input() context: string;
constructor(private el: ElementRef) {
}
public ngOnInit() {
$(this.el.nativeElement)
.sidebar({context: this.context})
.sidebar('setting', 'transition', 'overlay');
}
public ngOnDestroy() {
}
}
Then, I used it in the template:
<div id="app">
<div context="#app" class="ui left vertical menu sidebar"></div>
<div class="pusher"></div>
</div>
I have spent quite some time to get this working although it is rather simple in the end. Hope to save you some time ...
There is no need to create a directive, you can use the jQuery command as you would use with JavaScript (described at https://semantic-ui.com/modules/sidebar.html#/usage). However, "$" has to be declared and the command has to be located in a TypeScript function ("toggle()"):
import {Component} from '#angular/core';
declare var $: any;
#Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
toggle() {
$('.ui.sidebar').sidebar('toggle');
}
}
The corresponding section of the template may look like this:
<div class="ui fixed inverted main menu">
<a (click)="toggle()" class="launch icon item">
<i class="content icon"></i>
<p style="padding-left:1em">Menu</p>
</a>
</div>
Don't forget to add jQuery to the scripts section of .angular-cli.json:
"scripts": [
"../node_modules/jquery/dist/jquery.js",
"../node_modules/semantic-ui-css/semantic.min.js"
],
I'm using Semantic UI 2.2.12 which already depends on jQuery 3.2.1. Angular version is 4.4.4 running on node.js 6.11.2.
import { Component, OnInit } from '#angular/core';
declare var $:any;
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'app works!';
ngOnInit(){
$('#app .ui.sidebar')
.sidebar({context:$('#app')})
.sidebar('setting', 'transition', 'overlay') ;
}
}
I have the below OverlayComponent that's serving as a processing spinner during async calls. The overlay pops up without issue but when I try and pass a message to it, the message doesn't stick.
Child Component
import {Component, OnInit} from '#angular/core';
import {OverlayComponent} from "../../shared/app.mysite.overlay.component";
#Component({
moduleId: module.id,
selector: 'tracker-component',
templateUrl: '/public/app/templates/pages/racker/mysite.tracker.component.html',
styleUrls: ['../../../scss/pages/tracker/tracker.css'],
providers: [OverlayComponent]
})
export class TrackerComponent implements OnInit{
constructor(private overlayComponent: OverlayComponent) {
}
ngOnInit(): void {
this.overlayComponent.showOverlay("Testing 123"); //<-- shows overlay but doesn't show the message in the overlay
}
}
Child Component HTML
<div id="TrackerContainer">
<div class="col-lg-12" class="container">
<div class="jumbotron jumbotron-header">
<div>
<div id="pageTitle">Tracker</div>
</div>
</div>
<div class="content-container container">
<div *ngFor="let item of tracker.activeMenu.items">
<card-component [item]="item"></card-component>
</div>
</div>
</div>
</div>
<overlay-component [message]="overlayMessage"></overlay-component>
OverlayComponent.ts
import { Component } from '#angular/core';
#Component({
selector: 'overlay-component',
templateUrl: '/public/app/templates/shared/mysite.overlay.component.html',
styleUrls: ['../../app/scss/shared/overlay.css']
})
export class OverlayComponent {
message: string;
//message: string = 'testing...'; <-- this updated the message just fine
showOverlay(msg: string) {
this.message = msg; // <-- the right value comes through in the msg variable but doesn't update in the page
$('.overlay-component-container').show();
}
hideOverlay() {
$('.overlay-component-container').hide();
this.message = '';
}
}
OverlayComponent.html
<div class="overlay-component-container">
<div class="overlay-component">
<div class="overlay-message">{{message}}</div>
<div>
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
</div>
</div>
The issue could be caused by a few things:
You don't need this here, this is just for services, not for components
providers: [OverlayComponent]
This isn't the way to get a reference to another component, you may not have the correct instance.
constructor(private overlayComponent: OverlayComponent)
Is <overlay-component> actually inside the HTML of the Tracker component?
Also where is the overlayMessage property on the tracker component? (as used below)
<overlay-component [message]="overlayMessage"></overlay-component>
Just ensure that the over-component is inside the tracker component, remove the this.message = msg; from showOverlay() and use two-way data binding to change the message.
I.E <overlay-component [message]="overlayMessage"></overlay-component>
If you add overlayMessage: string; to your Tracker component (or parent component of the overlay) Then all you need to do is update it in your parent component and your overlay component will pick up the changes.