I have the following code and i wish to update the parent class when click on the image. The image will call "SelectVariation" method when clicked. Is there any way to do this?
component.html :
<div class="clr-row">
<ng-container *ngFor="let variOption of product.variOptions">
<div class="card clickable clr-col-2 variationCard"
*ngFor="let variOptionTwo of variOption.variOptionTwos"> //Update this class
<div class="card-img" (click)="selectVariation(variOptionTwo.id, $event)">
<clr-tooltip>
<img src="{{variOptionTwo.url}}" class="variationImgScale" clrTooltipTrigger>
<clr-tooltip-content clrPosition="top-right" clrSize="m" *clrIfOpen>
<span>{{variOption.optName}} - {{variOptionTwo.optName}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
</div>
component.ts :
selectVariation(id: number, event: any) {
//Update parent class
}
In the child component use
#Output() variableHere = new EventEmitter<>();
this.variableHere.emit(this.variableToSend);
Then in the parent associate the variable to a method in html child template definition:
<app-child (variableHere)="manageVariable($event)"></app-achild>
In the parent component define the method and do a variable equels the result of the method for example:
manageVariable(event) {
this.variableToUpdate = event;
}
If you have to check if the variable has changed his state call what you need to check in an ngDoCheck().
Take the advantage of the EventEmitter in angular with output
parent.component.html
<my-child-comp (onSelectVariation)="myVariation($event)" ></my-child-comp>
parent.component.ts
myVariation(myVars) {
console.log(myVars)
}
child.component.html
<button (click)="onSelectVariation.emit(myData)">trigger variation</button>
child.component.ts
#Output() onSelectVariation = new EventEmitter();
Name which you have defined in the output should be used as a event in it host element in parent
Related
I have a div with the id of 1. I'm trying to set the display to none dynamically. Is there an Angular way of doing this. Currently, I'm using vanilla javascript. I was asking about doing this dynamically because there will be over 60 divs that will be created from an array.
In my html
<div *ngFor="let item of items; i = index;">
<div id={{i}} (click)=hideDiv()></div>
</div>
In my method
hideDiv() {
return document.getElementById('1').style.display = "none";
}
That works but I'm looking for the Angular way of doing the above.
It was suggested that I use #ViewChild. Here's what I've changed. I can't use a Template Reference Variable as the html divs are created dynamically. Unless someone can let me know how to create the template variables dynamically. Although I don't think it's possible to create template variables with a loop.
#ViewChild('imgId', { static: true }) elementRef: ElementRef<HTMLDivElement>;
imgId: string;
Then in the method I have:
this.imgId = event.path[0].attributes[1].value;
this.elementRef.nativeElement.style.display = "none";
The event.path[0].attributes[1].value gets me the id of the image. The imgId shows when I console log it. It's still not changing the display on the div to none. Also I'm getting the error:
Cannot read properties of undefined (reading 'nativeElement')
Yes, you can use the ViewChild query in Angular to do this. In your component, define a query like this:
#ViewChild('#1') elementRef: ElementRef<HTMLDivElement>;
Implement the AfterViewInit interface in your component, and inside it, use this:
this.elementRef.nativeElement.style.display = "none";
You can simply use ngIf for this
Component
shouldDisplay: boolean = true;
hide(): void {
this.shouldDisplay = false;
}
show(): void {
this.shouldDisplay = true;
}
Html
<button (click)="hide()">Hide</button>
<button (click)="show()">Show</button>
<div *ngIf="shouldDisplay">this is the content</div>
Here is the working example
This is the Angular way:
template
<div *ngIf="showMe"></div>
or
<div [hidden]="!showMe"></div>
TypeScript:
showMe: boolean;
hideDiv() {
this.showMe = false;
}
For dynamic items where your don't know how many you will get the best approach would be to add a directive that would store and adjust that for you:
#Directive({ selector: '[hide-me]' })
export class HideDirective {
#Input() id!: string;
#HostBinding('style.display')
shouldShow: string = '';
}
then in your component just address them by ID:
#Component({
selector: 'my-app',
styleUrls: ['./app.component.css'],
template: `
<div *ngFor="let item of items; let index = index;">
<div hide-me id="{{index}}" (click)="hideDiv(index)">Some value</div>
</div>
`,
})
export class AppComponent {
#ViewChildren(HideDirective) hideDirectives!: QueryList<HideDirective>;
items = [null, null, null];
hideDiv(id: number) {
this.hideDirectives.find((p) => p.id === id.toString()).shouldShow = 'none';
}
}
Stackblitz: https://stackblitz.com/edit/angular-ivy-pnrdhv?file=src/app/app.component.ts
An angular official example: https://stackblitz.com/edit/angular-ivy-pnrdhv?file=src/app/app.component.ts
How about passing the div reference to the hideDiv method directly in the Dom using a template variable like this.
<div *ngFor="let item of items; i = index;">
<div #divElement (click)=hideDiv(divElement)></div>
And in your hide div method you will have access to the element directly
hideDiv(div) { div.style.display = "none";}
Here is a Stackblitz example
https://stackblitz.com/edit/angular-ivy-w1s3jl
There are many ways to do this, but in my opinion this is a simple solution the achieves your goal with less code.
PS:
It is always recommended to use the angular Renderer2 to manipulate Dom elements. This service has the method setStyle which you can use for your code.
I have an element where it has a class creates dynamically. I want to add a new class to it's parent element if the child element has a specific class.
<a [ngclass]="addClassHere"> //need to add class here if child has a specific class
<div [ngclass]="getScheduleDateColour(date.day)">{{date.day}}</div> //child class
</a>
the a element is created by the package and cannot access directly.
Assuming your getScheduleDateColour() returns different names of classes, say 'childClass1','childClass2' and so on, and the required is 'childClass1'.
Now you can set a new class to your parent according to your child's class by :
<a [ngclass]="childDiv.className == 'childClass1'? 'ReqParentClass' : '' ">
<div #childDiv [ngclass]="getScheduleDateColour(date.day)">{{date.day}}</div>
</a>
PS: If you want to add different parentClasses if child is not the required one, you can add it also in the ternary operator, like: 'childClass1'? 'ReqParentClass' : 'ElseThisWillBeTheParentClass'
One of the options you have is to use Template reference variables
for both your parent and child elements and change them pragmatically from your TS file
like so in html
<div #parentElem class="aquaDay">
<div #childElem class="aqua">day</div>
</div>
and use #ViewChild to bind them and change them as you want
#ViewChild("parentElem") parent: ElementRef;
#ViewChild("childElem") child: ElementRef;
changeColour() {
const childClass = this.child.nativeElement.className;
this.parent.nativeElement.className =
childClass === "aqua" ? "greenDay" : "aquaDay";
}
I created a simple demo here: https://stackblitz.com/edit/angular-ivy-422p1t?file=src/app/app.component.ts
I have a working code which injects any component via a service to the HTML:
ModalWindow.ts:
#Component({
selector: 'modal-window'
template: `
<div class="modal-dialog" role="document">
<div class="modal-content"><ng-content></ng-content></div>
</div>
`
})
export class ModalWindow {
}
Modalcontent.ts :
#Component({
selector: 'modal-content'
template: `
I'm beeing opened as modal!
`
})
export class ModalContent {
}
ModalService.ts :
/*1*/ #Injectable()
/*2*/ export class ModalService {
/*3*/
/*4*/ constructor(private _appRef: ApplicationRef, private _cfr: ComponentFactoryResolver, private _injector: Injector) {
/*5*/ }
/*6*/
/*7*/ open(content: any) {
/*8*/ const contentCmpFactory = this._cfr.resolveComponentFactory(content);
/*9*/ const windowCmpFactory = this._cfr.resolveComponentFactory(ModalWindow);
/*10*/
/*11*/ const contentCmpt = contentCmpFactory.create(this._injector);
/*12*/ const windowCmpt = windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);
/*13*/
/*14*/ document.querySelector('body').appendChild(windowCmpt.location.nativeElement);
/*15*/
/*16*/ this._appRef.attachView(contentCmpt.hostView);
/*17*/ this._appRef.attachView(windowCmpt.hostView);
/*18*/ }
/*19*/ }
App.ts:
#Component({
selector: 'my-app',
template: `
<button (click)="open()">Open modal window</button>
`,
})
Result (when click a button which calls this service method ) :
I already know what contentCmpFactory and windowCmpFactory are (lines #8,9)
But I don't udnerstnad what's going on later. Regarding lines #11,#12 - the docs says "creates a new component".
Questions :
1 - line #12 : What does [[contentCmpt.location.nativeElement]] do ? (the docs says its type is projectableNodes?: any[][] - What do they mean ??)
2 - line #14 : What does [[windowCmpt.location.nativeElement]] do ?
3 - line #16,#17 : what and why do I need them if I already did appendChild ? (docs says : Attaches a view so that it will be dirty checked. - so ?).
PLUNKER
Answers:
1) Angular takes ComponentFactory and create component instance with given element injector and with array of projectable nodes
windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);
1.1 Element Injector will be used when angular will resolve dependency
const value = startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
Here is also simple illustration of dependency resolution algorithm for app without lazy loading. With lazy loading it will look a litte more complicated.
For more details see design doc element injector vs module injector
1.2 Projectable nodes are the node elements, which are "projected"(transcluded) in the ng-content that we have in the template of our component.
In order to project something our component template has to contain ng-content node.
#Component({
selector: 'modal-window',
template: `
<div class="modal-dialog">
<div class="modal-content">
<ng-content></ng-content> // <== place for projection
</div>
</div>
`
})
export class ModalWindow {
We can use component above in parent component template as follows:
<modal-window>
<modal-content></modal-content>
<div>Some other content</div>
</modal-window>
So our final result will look like:
<modal-window>
<div class="modal-dialog">
<div class="modal-content">
<modal-content></modal-content> // our projectable nodes
<div>Some other content</div> // replaced ng-content
</div>
</div>
</modal-window>
So when we're passing projectable nodes to create method
windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);
we do the same things as described above.
We'are getting reference (contentCmpt.location) to the host element of created early contentCmpt component. This is modal-content element. And then angular will do all magic to project it in ng-content place.
In example above i added one div
<modal-window>
<modal-content></modal-content>
<div>Some other content</div> <== here
</modal-window>
So the real code should looks like:
let div = document.createElement('div');
div.textContent = 'Some other content';
windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement, div]]);
In conclusion Why is projectableNodes an any[][]?
2) During the next line
document.querySelector('body').appendChild(windowCmpt.location.nativeElement);
we're getting reference to created in memory modal-window element. ComponentRef allows us to do this because it stores reference to the host element in location getter
export abstract class ComponentRef<C> {
/**
* Location of the Host Element of this Component Instance.
*/
abstract get location(): ElementRef;
and then inseting it in document.body tag as last child. So we see it on the page.
3) Let's say our ModalContent has not just static content but will perform some operations for interaction.
#Component({
selector: 'modal-content',
template: `
I'm beeing opened as modal! {{ counter }}
<button (click)="counter = counter + 1">Increment</button>
`
})
export class ModalContent {
counter = 1;
}
If we remove
this._appRef.attachView(contentCmpt.hostView);
then our view will not being updated during change detection cycle because we created view via ComponentFactory.create and our view is not part of any item in change detection tree (unlike creation via ViewContainerRef.createComponent). Angular opened API for such purposes and we can easily add view to root views https://github.com/angular/angular/blob/master/packages/core/src/application_ref.ts#L428 and after that our component will be updated during Application.tick https://github.com/angular/angular/blob/master/packages/core/src/application_ref.ts#L558
I have several components that use basically the same table so I'm in the process of abstracting out that table. I have solved most of my dynamic table population needs but have yet to find a solution to the following.
In one of my table instances the rows need to be clickable. In the original table I simply added a click event in the row and had it call a function in my typescript file.
Now that the table is a child of any consuming component I am not sure how to dynamically add this click event. Here is an essentially what I am trying to achieve:
HTML:
<tr class="someClass" <!-- want click event here -->>
<td *ngFor="let column of row;"><div [innerHtml]="column"></div></td>
</tr>
This is the tables typescript file, where all the data is coming in on the visibleData object:
export class GenericTableComponent implements OnInit {
#Input() visibleData;
constructor() { }
ngOnInit() {
}
}
I implement the generic table in my parent HTML here
Parent HTML:
<oma-generic-table [visibleData]="visibleData"></oma-generic-table>
And here is a function in the parent which prepares the data. I have attempted to store the click event in a string and pass it but everything I've tried so far has failed (data binding with {{}}, square brackets, etc..).
transformData(visibleData) {
const ret: any = {};
ret.headings = visibleData.headings;
ret.action = '(click)="rowClicked([row.id])"';
ret.checkbox = this.checkBox; //add if the table needs checkboxes
ret.content = [];
for (let i = 0; i < visibleData.content.length; i++) {
ret.content.push(_.values(_.omit(visibleData.content[i], 'id')));
}
return ret;
}
However, even when hard coded into the child, the click event doesn't recognize the function in the parent and I get the following error:
EXCEPTION: Error in ./GenericTableComponent class GenericTableComponent - inline template:35:4 caused by: self.parentView.context.rowClicked is not a function
ORIGINAL EXCEPTION: self.parentView.context.rowClicked is not a function
I'm not sure if this is something simple or not. I'm new to Angular 2 so I apologize if this question is simplistic. Thanks in advance for any help.
Your generic table can emit custom events to which parent component can subscribe:
#Component({
selector: 'oma-generic-table',
template: `
<table>
<tr *ngFor="let row of visibleData" class="someClass" (click)="selectRow(row)">
<td *ngFor="let column of row;"><div [innerHtml]="column"></div></td>
</tr>
</table>
`
})
export class OmaGenericTable {
#Input() visibleData: VisibleDataRow[];
#Output() select = new EventEmitter<VisibleDataRow>();
selectRow(row: VisibleDataRow) {
this.select.emit(row);
}
}
Then in your parent component:
// in template
<oma-generic-table
[visibleData]="visibleData"
(select)="tableRowSelected($event)"
></oma-generic-table>
// in component
tableRowSelected(r: VisibleDataRow) {
console.log(`Selected row ${r}`);
}
Angular2 as far as I know will not bind to a "(click)" event that way. It may look ugly, but I would add the NgIf directive to the table on the row that the click event should act upon, and inversely apply the logic to another row that should not be affected by a click event. Ex:
<tr *ngIf="!isClickable" class="someClass">
<tr *ngIf="isClickable" class="someClass" (click)="rowClicked()>
<td *ngFor="let column of row;"><div [innerHtml]="column"></div></td>
</tr>
Then you can pass the isClickable variable as an input into your table from anywhere that you instantiate the table component, and act upon it.
Using this in HTML
<tr *ngIf="!isClickable" class="someClass">
<tr *ngIf="isClickable" class="someClass" #click>
<td *ngFor="let column of row;"><div [innerHtml]="column"></div></td>
</tr>
this in typescript
export class GenericTableComponent implements OnInit {
#Input() visibleData;
#ViewChild('click') protected _click:ElementRef;
constructor(enderer: Renderer) {
renderer.listen(this._click.nativeElement, 'click', (event) => {
// Do something with 'event'
})
}
}
Hope that help you
I try to make my own component that needs to have reference to a child element define by user.
In my html template i have this :
<fn-photo-editor fxLayout="row" fxLayoutGap="20px" class="coloredContainerX box">
<div fxFlex.gt-sm="80" fxFlex="33" fxLayout="row" fxLayoutAlign="center center">
<canvas fnPhotoEditorCanvas height="1080px" width="1920px" fxFlex dropzone="copy"></canvas>
</div>
</fn-photo-editor>
In my Component.ts I have this :
#ContentChild(FnPhotoEditorCanvasDirective) canvas:HTMLCanvasElement;
and in my component template, just :
<ng-content></ng-content>
I use this directive on the canvas to get it's reference, since just using #ContentChild('canvas') seems to not work.
#Directive({
selector: '[fnPhotoEditorCanvas]',
})
export class FnPhotoEditorCanvasDirective { }
So its this code, i get a reference to a FnPhotoEditorCanvasDirective object but it does not contains or represent a canvas object.
What i'm missing?
#ContentChild('canvas') works only if the element has a template variable #canvas
<canvas #canvas fnPhotoEditorCanvas height="1080px" width="1920px" fxFlex dropzone="copy"></canvas>
This query
#ContentChild(FnPhotoEditorCanvasDirective) canvas:HTMLCanvasElement;
will return the instance of the FnPhotoEditorCanvasDirective, not a HTMLCanvasElement.
Perhaps you want
#ContentChild(FnPhotoEditorCanvasDirective, {read: ElementRef}) canvas:HTMLCanvasElement;
then you can access the HTMLCanvasElement like
ngAfterContentInit() {
this.canvas.nativeElement...
}
See also angular 2 / typescript : get hold of an element in the template