Change component's template Angular 2 - javascript

I'm using mdl-select component. It's a drop-down list. When you press it there are focusin event fired. But it doesn't when you press an arrow-dropdown icon, so I needed to change a template a bit to have a desired behavior. But it's a library component. Is there a way to override it's template?
The thing I need to change just to add tabindex=\"-1\" to element. I can do it with js, but I use component a lot in app, and I don't want to use document.getElement... every time I use MdlSelectComponent in the views of my own components.
I tried to use #Component decorator function on MdlSelectComponent type, however it requires to declare this class once again and anyway have done nothing.
Update
main.browser.ts
/*
* Angular bootstraping
*/
import { Component } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { decorateModuleRef } from './app/environment';
import { bootloader } from '#angularclass/hmr';
import {MdlSelectComponent} from '#angular2-mdl-ext/select';
/*
* App Module
* our top level module that holds all of our components
*/
import { AppModule } from './app';
/*
* Bootstrap our Angular app with a top level NgModule
*/
export function main(): Promise<any> {
console.log(MdlSelectComponent)
MdlSelectComponent.decorator.template = "<div class=\"mdl-textfield is-upgraded\" [class.is-focused]=\"this.popoverComponent.isVisible || this.focused\" [class.is-disabled]=\"this.disabled\" [class.is-dirty]=\"isDirty()\"> <span [attr.tabindex]=\"!this.disabled ? 0 : null\" (focus)=\"open($event);addFocus();\" (blur)=\"removeFocus()\"> <!-- don't want click to also trigger focus --> </span> <input #selectInput tabindex=\"-1\" [readonly]=\"!autocomplete\" class=\"mdl-textfield__input\" (click)=\"toggle($event)\" (keyup)=\"onInputChange($event)\" (blur)=\"onInputBlur()\" [placeholder]=\"placeholder ? placeholder : ''\" [attr.id]=\"textfieldId\" [value]=\"text\"> <span class=\"mdl-select__toggle material-icons\" (click)=\"toggle($event)\"> keyboard_arrow_down </span> <label class=\"mdl-textfield__label\" [attr.for]=\"textfieldId\">{{ label }}</label> <span class=\"mdl-textfield__error\"></span> <mdl-popover [class.mdl-popover--above]=\"autocomplete\" hide-on-click=\"!multiple\" [style.width.%]=\"100\"> <div class=\"mdl-list mdl-shadow--6dp\"> <ng-content></ng-content> </div> </mdl-popover> </div> ";
return platformBrowserDynamic()
.bootstrapModule(AppModule)
.then(decorateModuleRef)
.catch((err) => console.error(err));
}
// needed for hmr
// in prod this is replace for document ready
bootloader(main);
APP.COMPONENT.TS
import { Component, OnInit, ViewEncapsulation } from '#angular/core';
import { Router } from '#angular/router';
require('../../../styles/styles.scss');
import {MdlSelectComponent} from '#angular2-mdl-ext/select';
//
declare let Reflect: any;
Reflect.getOwnMetadata('annotations', MdlSelectComponent)[0].template = 'Hooray';
#Component({
selector: 'app',
encapsulation: ViewEncapsulation.None,
styleUrls: [],
template: `
<div>
<mdl-select [(ngModel)]="personId">
<mdl-option *ngFor="let p of people" [value]="p.id">{{p.name}}</mdl-option>
</mdl-select>
<router-outlet></router-outlet>
</div>`
})
export class AppComponent implements OnInit {
constructor(
router: Router,
) {
router.events.subscribe(data => {
scrollTo(0, 0);
});
}
public ngOnInit() {
}
}

As #angular2-mdl-ext/select uses Reflect to define decorators then you do the following
declare let Reflect: any;
Reflect.getOwnMetadata('annotations', MdlSelectComponent)[0].template = 'Hooray';
Plunker Example

Related

Angular how to hide a global component when a specific route is opened? [duplicate]

This question already has answers here:
How to Update a Component without refreshing full page - Angular
(7 answers)
Closed 3 years ago.
I'm not sure whether this is possible or not in angular but I wanted to hide a global component when a specific route is opened.
Say for example I have the following:
app.component.html
<app-header></app-header>
<app-banner></app-banner> <!-- Global Component I want to hide -->
<div class="body-container">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>
app-routing.module.ts
import {NgModule} from '#angular/core';
import {Route, RouterModule} from '#angular/router';
import { StudentListComponent } from './Components/StudentList/StudentList.component';
import { SchoolMottoComponent } from './Components/SchoolMotto/SchoolMotto.component';
const routes: Routes = [
{path: 'StudentList', component: StudentListComponent },
{path: 'SchoolMotto', component: SchoolMottoComponent }
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
export const routingComponents = [StudentListComponent, SchoolMottoComponent]
With this, its a given that when I want to view the StudentList Component, then the url by default becomes localhost4200:/StudentList and the same with SchoolMotto it becomes localhost4200:/SchoolMotto.
Within the StudentListComponent, is an ag-grid that displays list of students, and when you click one of those students the url becomes something like this: localhost4200:/StudentList/view/cf077d79-a62d-46e6-bd94-14e733a5939d and its another sub-component of SchoolList that displays the details of that particular student.
I wanted to hide the Global banner component when the url has something like that: localhost4200:/StudentList/view/cf077d79-a62d-46e6-bd94-14e733a5939d. Only when the user views the specific details of a student.
Something like this:
app.component.html
<app-header></app-header>
**<app-banner *ngIf="router != '/StudentList/view/'"></app-banner> <!-- Global Component I want to hide -->**
<div class="body-container">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>
Is this doable or not? If it is, how?
You could use event emitter or subject to emit an event when you're in StudentList/view and use ngIf to hide the banner.
In your StudentList component.ts :
export class StudentList {
bannerSubject: Subject<any> = new Subject<any>();
ngOnInit() {
bannerSubject.next(true);
}
}
subscribe to this in your parent component and you can easily hide the banner.
You can acheieve that with the help of component interation using a service
You will use the help of Rxjs Observables here
You will emit an event when you reach the student view component, then recieve that event in app component then change the view condition
New Service:
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
#Injectable()
export class RouteService {
private routeChangedSource = new Subject<string>();
// Observable string streams
routeChanged$ = this.routeChangedSource.asObservable();
// Service message commands
changeRoute(mission: string) {
this.routeChangedSource.next(mission);
}
}
Student View Component.
import { Component } from '#angular/core';
import { routeService } from './mission.service';
#Component({
})
export class MissionControlComponent implements ngOnInit{
constructor(private routeService: routeService) {}
ngOnInit() {
this.routeService.changeRoute(mission);
}
}
App Component:
import { Component, Input, OnDestroy } from '#angular/core';
import { RouteService } from './route.service';
import { Subscription } from 'rxjs';
export class AppComponent implements OnDestroy {
studentView = false;
constructor(private routeService: RouteService) {
this.subscription = routeService.routeChanged$.subscribe(
value => {
this.studentView = true;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Now, your App Component can be:
<app-header></app-header>
<app-banner *ngIf="!studentView"></app-banner>
<div class="body-container">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>
<app-header></app-header>
<app-banner *ngIf="myService.hideGlobalComp"></app-banner> <!-- Global Component I want to hide -->
<div class="body-container">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>
in the ts file:
onCellClicked($event) { // place into your method there you want.
this.route.parent.url.subscribe(urlPath => {
this.url = urlPath[urlPath.length - 1].path;
});
if(this.url === 'StudentList/view') {
this.myService.hideGlobalComp = true;
}
}
}
In you ts file do like this.
add new variable router: string;
add in construction add this
constructor(private _router: Router){
this.router = _router.url;
}
Then in HTML use same code.
Let me know if this does not work.

Angular list reference not getting updated on item deletion

I am implementing a feature which displays the selected items from a hierarchy structure, on the right.
slice.component.ts :
import { Component, Input, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '#angular/core';
import * as API from '../../shared/api-routes';
import { DataService } from '../../shared/service/data.service';
import { TreeNode } from '../../shared/dto/TreeNode';
import { Subject } from 'rxjs/Subject';
import html from './slice.component.html';
import css from './slice.component.css';
#Component({
selector: 'slice-component',
template: html,
providers: [DataService],
styles: [css],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SliceComponent {
selections: TreeNode<string>[] = [];
newList: TreeNode<string>[];
constructor(dataService:DataService, cd:ChangeDetectorRef) {
super(dataService, cd);
}
public onSliceChange(event:TreeNode<string>):void {
if(event.selected) {
this.selections.push(event);
}
else {
var index = this.selections.indexOf(event);
if(index > -1) {
this.selections.splice(index, 1);
}
}
this.newList = this.selections.slice();
}
}
slice.component.html :
<p>Slices</p>
<mat-input-container>
<input #searchInput matInput placeholder="Search for Slices">
</mat-input-container>
<div class="flex-container">
<div class="SliceCheck" *ngIf="isDataLoaded">
<fortune-select
(sliceSelected)="onSliceChange($event)">
</fortune-select>
</div>
<div class="sendToRight">
<rightside-component
[sliceTreeNode]="newList">
</rightside-component>
</div>
</div>
rightside.component.ts :
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '#angular/core';
import { TreeNode } from '../../shared/dto/TreeNode';
import html from './rightside.component.html';
import css from './rightside.component.css';
#Component({
selector: 'rightside-component',
template: html,
providers: [DataService],
styles: [css],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RightSideComponent {
#Input() sliceTreeNode: TreeNode<string>[];
constructor(private cd: ChangeDetectorRef) {}
getSlices() : TreeNode<string>[] {
if (typeof(this.sliceTreeNode) == "undefined" || (this.sliceTreeNode) === null) {
return [];
}
return this.sliceTreeNode;
}
deselect(item: TreeNode<string>): void {
if((item.children) !== null) {
item.children.forEach(element => {
this.deselect(element);
});
}
var index = this.sliceTreeNode.indexOf(item);
if(index > -1) {
this.sliceTreeNode.splice(index, 1);
}
item.selected = false;
}
}
rightside.component.html :
<ul class="selection-list" >
<li *ngFor="let item of getSlices()">
<button class="btn" (click)="deselect(item)" *ngIf="item.selected">
<i class="fa fa-close"> {{ item.displayName }} </i>
</button>
</li>
</ul>
In my implementation, everything works as expected until the following happens :
You delete an item from the rightside list, it gets deselected correctly from the hierarchy. But when you select it again from the hierarchy, it shows up twice in the side-view list now.
Somehow the list instance in the right-side component does not get updated when a node is selected again which has been previously deselected.
Any inputs on how to fix this? It is something similar to this plunkr I found online : http://next.plnkr.co/edit/1Fr83XHkY0bWd9IzOwuT?p=preview&utm_source=legacy&utm_medium=worker&utm_campaign=next&preview
The most likely issue is that you're using ChangeDetectionStrategy.OnPush and performing mutations within onSliceChange of the SliceComponent without an explicit call to cd.markForCheck().
Either remove changeDetection: ChangeDetectionStrategy.OnPush from SliceComponent or add cd.markForCheck() to the end of onSliceChange
When you set the change detection to OnPush, Angular only guarantees that the component will be updated when the references passed to it's Inputs are changed. onSliceChange doesn't replace any Input, so the slice component won't be updated.
You should also consider replacing deselect in RightSideComponent with an Output and handling the changes in SliceComponent. The convention of one-way data flow is to only modify common state in the common parent. This prevents conflicts when two components both want to modify some shared state.

How can I access an already transcluded ContentChild?

I have a angular component app-b that is used within a component app-a that is used in the app-component. The app-component has some content in app-a, app-a transcludes this with ng-content into app-b, app-b shows it with another ng-content - but how can I access this content within the component (and not it's template)?
I would think that ContentChild is the correct approach but appears to be wrong.
Example:
https://stackblitz.com/edit/angular-ddldwi
EDIT: Updated example
You cannot query by tag name with #ContentChild decorator. You can query either by template variable, component or directive selector.
app-a.component.html
<app-b>
<ng-content></ng-content>
<p #myContent>This is a content child for app-b.</p>
</app-b>
app-b.component.ts
import { Component, AfterContentInit, ContentChild } from '#angular/core';
#Component({
selector: 'app-b',
templateUrl: './b.component.html',
styleUrls: ['./b.component.css']
})
export class BComponent implements AfterContentInit {
#ContentChild('myContent') contentchild;
ngAfterContentInit() {
console.log(this.contentchild);
}
}
Live demo
I recommend sharing the data between components. For example, move your data (E.G. dummyName) into a service. Then add the service to each component (where you need the shared data).
Service:
import { Injectable } from '#angular/core';
#Injectable()
export class DataShareService {
public dummyName: String = 'one';
constructor() { }
}
Add the new service to app.module.ts:
providers: [DataShareService],
Child Component:
import { DataShareService } from './data-share.service';
import { Component } from '#angular/core';
#Component({
selector: 'app-child',
templateUrl: './child.component.html'
})
export class ChildComponent {
constructor(public ds: DataShareService) { }
toggle() {
this.ds.dummyName = this.ds.dummyName === 'one' ? 'two' : 'one';
}
}
Child Component template (html):
<p> {{ds.dummyName}}</p>
<button (click)="toggle()">Click Me to Toggle Value</button>
Parent Component:
import { Component, OnInit } from '#angular/core';
import { DataShareService } from './data-share.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(public ds: DataShareService) {}
displayNumber() {
return this.ds.dummyName === 'one' ? 1 : 2;
}
}
Parent Component template (html):
<p> this is the parent component:</p>
<div> value as string: {{ ds.dummyName }} </div>
<div> value as number: <span [textContent]="displayNumber()"></span></div>
<hr>
<p> this is the child component:</p>
<app-child></app-child>
Note! The child component toggle() function demonstrates how you can change the data. The parent component displayNumber() function demonstrates how to use the data, independent of it's display (I.E. as just pure data).
This appears to be impossible due to a bug in Angular:
https://github.com/angular/angular/issues/20810
Further reference:
https://www.reddit.com/r/Angular2/comments/8fb3ku/need_help_how_can_i_access_an_already_transcluded/

How to make angular click bindings work using string to create DOM elements?

How should I make angular binding work on dynamically added DOM elements? I am using ag-grid (ng2) for datatables.
Based on certain conditions, i am using different column rendering.
columnDef.cellRenderer = function (params) {
return `<div><i class='fa ${params.value}'></i></div>`;
};
In this, i want to add a click function to the icon like this:
columnDef.cellRenderer = function (params) {
return `<div><i (click)='iconClicked()' class='fa ${params.value}'></i></div>`;
};
How do I make these click bindings work in angular 2 ?
You can build components dynamically like explained in Equivalent of $compile in Angular 2 to be able to pass HTML with Angular event and value bindings.
or you can use imperative event binding
export class MyComponent {
constructor(private elRef:ElementRef) {}
someMethod() {
columnDef.cellRenderer = (params) => {
return `<div><i id="addClick" class='fa ${params.value}'></i></div>`;
this.elRef.nativeElement.querySelector('#addClick')
.addEventListener('click', this.iconClicked.bind(this));
};
}
iconClicked(e) {
}
}
As micronyks said, you can create components dynamically by using ComponentFactoryResolver (NOT DynamicComponentResolver, it no longer exists) and you can find examples on S.O. (e.g. https://stackoverflow.com/a/36566919/1153681).
But it won't work in your situation because:
You don't want to create a whole component, but only add a piece of markup to an existing component.
You're not the one creating the markup, ag-grid is.
Since you're in ag-grid context, why don't you use ag-grid's API instead of Angular's? A quick look at their docs shows that a grid has a property onCellClicked(params) that takes a function callback which gets called when a cell is clicked.
Then hopefully you can trigger some Angular code from that callback.
You don't need to use ComponentFactoryResolver directly if you want dynamic Angular 2 components in ag-grid - you can use ag-grid's provided Angular 2 interface for this.
Let's say you have the following simple Components:
// CubeComponent
import {Component} from '#angular/core';
import {AgRendererComponent} from 'ag-grid-ng2/main';
#Component({
selector: 'cube-cell',
template: `{{valueCubed()}}`
})
export class CubeComponent implements AgRendererComponent {
private params:any;
private cubed:number;
// called on init
agInit(params:any):void {
this.params = params;
this.cubed = this.params.data.value * this.params.data.value * this.params.data.value;
}
public valueCubed():number {
return this.cubed;
}
}
// Square Component
import {Component} from '#angular/core';
import {AgRendererComponent} from 'ag-grid-ng2/main';
#Component({
selector: 'square-cell',
template: `{{valueSquared()}}`
})
export class SquareComponent implements AgRendererComponent {
private params:any;
agInit(params:any):void {
this.params = params;
}
public valueSquared():number {
return this.params.value * this.params.value;
}
}
// from-component.component.html
<div style="width: 200px;">
<button (click)="changeComponentType()">Change Component Type</button>
<ag-grid-ng2 #agGrid style="width: 100%; height: 350px;" class="ag-fresh"
[gridOptions]="gridOptions">
</ag-grid-ng2>
</div>
// from-component.component.ts
import {Component} from '#angular/core';
import {GridOptions} from 'ag-grid/main';
import {SquareComponent} from "./square.component";
import {CubeComponent} from "./cube.component";
#Component({
moduleId: module.id,
selector: 'ag-from-component',
templateUrl: 'from-component.component.html'
})
export class FromComponentComponent {
public gridOptions:GridOptions;
private currentComponentType : any = SquareComponent;
constructor() {
this.gridOptions = <GridOptions>{};
this.gridOptions.rowData = this.createRowData();
this.gridOptions.onGridReady = () => {
this.setColumnDefs();
}
}
public changeComponentType() {
this.currentComponentType = this.currentComponentType === SquareComponent ? CubeComponent : SquareComponent;
this.setColumnDefs();
}
private createRowData() {
let rowData:any[] = [];
for (var i = 0; i < 15; i++) {
rowData.push({
value: i
});
}
return rowData;
}
private setColumnDefs():void {
this.gridOptions.api.setColumnDefs([
{
headerName: "Dynamic Component",
field: "value",
cellRendererFramework: this.currentComponentType,
width: 200
}
])
}
}
// app.module.ts
import {NgModule} from "#angular/core";
import {BrowserModule} from "#angular/platform-browser";
import {RouterModule, Routes} from "#angular/router";
// ag-grid
import {AgGridModule} from "ag-grid-ng2/main";
// application
import {AppComponent} from "./app.component";
// from component
import {FromComponentComponent} from "./from-component.component";
import {SquareComponent} from "./square.component";
import {CubeComponent} from "./cube.component";
const appRoutes:Routes = [
{path: 'from-component', component: FromComponentComponent, data: {title: "Using Dynamic Components"}},
{path: '', redirectTo: 'from-component', pathMatch: 'full'}
];
#NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(appRoutes),
AgGridModule.withComponents(
[
SquareComponent,
CubeComponent,
])
],
declarations: [
AppComponent,
FromComponentComponent,
SquareComponent,
CubeComponent
],
bootstrap: [AppComponent]
})
export class AppModule {
}
Here the button will allow you to dynamically switch between the two components - this could obviously be done at runtime based on some condition you have.
Note too that it might be simpler for you to have one component, and in the actual output do the conditional logic - for example:
// called on init
agInit(params:any):void {
this.params = params;
if(this.params.isCube) {
// cubed
this.value = this.params.data.value * this.params.data.value * this.params.data.value;
} else {
// square
this.value = this.params.data.value * this.params.data.value;
}
}
You can find more information on how to use Angular 2 with ag-Grid here: https://www.ag-grid.com/best-angular-2-data-grid
With that in place, you can refer to https://github.com/ceolter/ag-grid-ng2-example/blob/master/systemjs_aot/app/clickable.component.ts for an example of using a component that supports click events in the grid. For all the examples take a look at https://github.com/ceolter/ag-grid-ng2-example which provides many examples, together with how to package them up with either systemjs, webpack or angular cli

Making a reusable angular2 component that can be used anywhere on the a site

Use Case: When making asynchronous calls, I want to show some sort of a processing screen so that end users knows something is happening rather than just staring at the screen. Since I have multiple places throughout the site where I want to use this, I figured making it a component at the "global" level is the best approach.
Problem: Being slightly new to angular2, I'm not getting if this is a problem of it being outside the directory in which the main component exists and the OverlayComponent being in another location or if I'm just all together doing it wrong. I can get the component to work fine but I need to be able to call functions to hide/destroy the component and also display the component. I have tried making it a service but that didn't get me any further so I'm back to square one. Essentially my question revolves around building a reusable component that has methods to hide/show itself when invoked from whatever component it's being called from.
Below is my current code:
Assume OverlayComponent.html is at /public/app/templates/mysite.overlay.component.html
Assume OverlayComponent.ts is at /public/app/ts/app.mysite.overlay.component
Assume mysite.tracker.component is at \public\app\ts\pages\Tracker\mysite.tracker.component.ts
OverlayComponent.html
<div class="overlay-component-container">
<div class="overlay-component" (overlay)="onShowOverlay($event)">
<div>{{processingMessage}}</div>
<div>
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
</div>
</div>
OverlayComponent.ts
import { Component } from '#angular/core';
#Component({
selector: 'overlay-component',
templateUrl: '/public/app/templates/mysite.overlay.component.html',
styleUrls: ['public/app/scss/overlay.css']
})
export class OverlayComponent {
onShowOverlay(e) {
$('.overlay-component').fadeIn(1000);
}
hideOverlay(e) {
$('.overlay-component').fadeOut(1000);
}
}
TrackerComponent.ts
import { Component, Output, OnInit, EventEmitter } from '#angular/core';
import { Http } from '#angular/http';
import { TrackerService } from './Tracker.service';
import { MenuCollection } from "./MenuCollection";
import { Menu } from "./Menu";
#Component({
moduleId: module.id,
selector: 'tracker-component',
templateUrl: '/public/app/templates/pages/tracker/mysite.tracker.component.html',
styleUrls: ['../../../scss/pages/racker/tracker.css'],
providers: [TrackerService]
})
export class TrackerComponent implements OnInit{
MenuCollection: MenuCollection;
#Output()
overlay: EventEmitter<any> = new EventEmitter();
constructor(private http: Http, private TrackerService: TrackerService) {
let c = confirm("test");
if (c) {
this.onShowOverlay();
}
}
ngOnInit(): void {
this.MenuCollection = new MenuCollection();
this.MenuCollection.activeMenu = new Menu('Active Menu', []);
this.TrackerService.getTrackerData().then(Tracker => {
this.MenuCollection = Tracker;
this.MenuCollection.activeMenu = this.MenuCollection.liveMenu;
console.log(this.MenuCollection);
},
error => {
alert('error');
})
}
onShowOverlay() { //This doesn't seem to 'emit' and trigger my overlay function
this.overlay.emit('test');
}
}
At a high level, all I'm wanting to do is invoke a components function from another component. Thanks in advance for any helpful input
You can use the #ContentChild annotation to accomplish this:
import { Component, ContentChild } from '#angular/core';
class ChildComponent {
// Implementation
}
// this component's template has an instance of ChildComponent
class ParentComponent {
#ContentChild(ChildComponent) child: ChildComponent;
ngAfterContentInit() {
// Do stuff with this.child
}
}
For more examples, check out the #ContentChildren documentation.

Categories

Resources