I'm creating a 3d "card flip" using angular 2. A parent 'card-flip' component contains a nested 'card-flip-front' and 'card-flip-back' component.
<card-flip card-flip-id="demo-1" class="grid_col-6">
<card-flip-front class="card">
<div class="card__inner">
Card Front
</div>
</card-flip-front>
<card-flip-back class="card">
<div class="card__inner">
Card Back
</div>
</card-flip-back>
</card-flip>
I would like to create a "clone" of the card-flip-front component with content projection and data-binding in tact. The "clone" would be used for animating and the "original" would remain in it's original position hidden. That way I have a reference of where the "clone" should animate to when it returns to the original position (even if the user scrolls or resizes the window).
The main challenge I'm facing is that I need the content within the ng-content tag to also be projected in the "clone". The problem being that the first ng-content tag will be used by Angular for content projection and additional, unlabeled ng-content tags will be empty (which I know is the expected behavior).
One might ask, "why not just create a dumb, static copy of the element in the DOM?". I would like to avoid this so that nested components and data bindings that inject data (thereby modifying the dimensions of the element) will continue to work.
Here's my work so far which creates an instance of the CardFlipFront component via ComponentFactory to serve as the "clone" and simply inserts the innerHTML of the "original" CardFlipFront.
import {
Component,
ComponentFactory,
ComponentFactoryResolver,
ComponentRef,
ContentChild,
Inject,
Input,
OnInit,
ViewChild,
ViewContainerRef
} from '#angular/core';
import { CardFlipFrontComponent } from './card-flip-front.component';
import { CardFlipBackComponent } from './card-flip-back.component';
import { CardFlipService } from './card-flip.service';
#Component({
selector: 'card-flip',
templateUrl: './card-flip.component.html',
styleUrls: ['./card-flip.component.css'],
entryComponents: [
CardFlipFrontComponent
]
})
export class CardFlipComponent implements OnInit {
#Input('card-flip-id') public id: string;
#ContentChild(CardFlipFrontComponent) private front: CardFlipFrontComponent;
#ContentChild(CardFlipBackComponent) private back: CardFlipBackComponent;
#ViewChild('frontCloneContainer', { read: ViewContainerRef }) private frontCloneContainer: ViewContainerRef;
private frontComponentRef: ComponentFactory<CardFlipFrontComponent>;
private frontClone: ComponentRef<CardFlipFrontComponent>;
constructor(
#Inject(CardFlipService) private _cardFlipService: CardFlipService,
private _componentFactoryResolver: ComponentFactoryResolver
) {
this.frontComponentRef = this._componentFactoryResolver.resolveComponentFactory(CardFlipFrontComponent);
}
ngOnInit() {
this._cardFlipService.register(this.id);
}
ngAfterViewInit() {
// Create a card-flip-front component instance to serve as a "clone"
this.frontClone = this.frontCloneContainer.createComponent(this.frontComponentRef);
// Copy the innerHTML of the "original" into the "clone"
this.frontClone.instance.el.nativeElement.innerHTML = this.front.el.nativeElement.innerHTML;
}
ngOnDestroy() {
this.frontClone.destroy();
}
}
<ng-content select="card-flip-front"></ng-content>
<ng-container #frontCloneContainer></ng-container>
<ng-content select="card-flip-back"></ng-content>
import {
Component,
ElementRef,
HostBinding,
Input,
OnInit,
Renderer
} from '#angular/core';
#Component({
selector: 'card-flip-front',
templateUrl: './card-flip-front.component.html',
styleUrls: ['./card-flip-front.component.css']
})
export class CardFlipFrontComponent implements OnInit {
constructor(private _el: ElementRef, private _renderer: Renderer) { }
public get el(): ElementRef {
return this._el;
}
public get renderer(): Renderer {
return this._renderer;
}
ngOnInit() { }
}
<ng-content></ng-content>
UPDATE:
Ok, so after reading about some similar challenges and the github issue here, I tried the following.
<ng-template #frontTemplate>
<ng-content select="card-flip-front"></ng-content>
</ng-template>
<ng-container *ngIf="isOpen == true" #front1>
<ng-container *ngTemplateOutlet="frontTemplate"></ng-container>
</ng-container>
<ng-container *ngIf="isOpen == false" #front2>
<ng-container *ngTemplateOutlet="frontTemplate"></ng-container>
</ng-container>
<ng-content select="card-flip-back"></ng-content>
Basically, we can get around the single projection issue with ng-content by placing it within a template and using two ng-container tags with an *ngIf statement that will only show one instance of the template based on a class property isOpen.
This doesn't solve the entire issue though because only one container will be rendered at any given time. So, I can't get the current position of "original" to figure out where to animate the "clone" during the return animation described above.
I think you can have an intermediary component <card-flip-content> inside <card-flip> template which is duplicated and which receives the <ng-content> of the <card-flip>.
Something like:
#Component(
selector = 'card-flip',
template = `
<card-flip-content #theOne>
<ng-content />
</card-flip-content>
<card-flip-content #theClone>
<ng-content />
</card-flip-content>
`)
Then bind data as needed to #theOne and #theClone and animate only #theClone.
This way can have #Input and #Output thus leaving the actions of one component to be interpreted by the parent in order to act on the other component.
Would that work?
Related
I am developing an app and for now, I have a dynamic grid generator which divides the space in the screen to fit several components dynamically. So, the component encharged of this must render the components after angular has rendered the page. In order to achieve that I've followed the angular dynamic component loader guide (https://angular.io/guide/dynamic-component-loader).
So I am in a point where I do have the component where the other components must be rendered, I have my custom directive to render the components.
The directive
#Directive({
selector: '[componentLoader]'
})
export class ComponentLoaderDirective {
constructor (
public ViewContainerRef: ViewContainerRef
) {}
}
Now the component ( grid component )
grid.component.ts
// ... Stuff above
export class GridComponent implements OnInit {
#Input() public items: gridItem[] = [];
#ViewChild(ComponentLoaderDirective) componentLoader: ComponentLoaderDirective | undefined;
constructor(
private sanitizer: DomSanitizer,
private componentFactoryResolver: ComponentFactoryResolver
) {}
ngOnInit(): void { this.processRow(this.items) }
processRow( row: gridItem[] ) {
// Some grid related stuff ...
for ( let item of row ) {
// Stuff performed over every item in each grid row
this.renderComponentIfNeeded(item)
}
}
renderComponentIfNeeded( item: gridItem ):void {
if ( item.components ) {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
let viewContainerRef = this.componentLoader.ViewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent<any>(componentFactory);
componentRef.instance.data = item;
console.log('Directive ', this.componentLoader, 'ComponentRef: ', componentRef);
}
}
And the HTML of the component:
<!-- Dynamic grid generation using ng-template and ng-content. This is generated several times using the *ngFor, for every item in the items array we will have a componentLoader -->
<ng-template componentLoader>
</ng-template>
There is a lot more content in these files but for simplicity I will only post this, If you need more code just tell me.
Okay, so my problem is that when I access to this.contentLoader the returned value is just undefined, so this.componentLoader.viewContainerRef causes an error because componentLoader is undefined.
I've tried adding the exportAs property to the directive's decorator and it is giving exacly the same error.
I've also tried to add the directive in the module declarations without success, and changed the <ng-template componentLoader> to <ng-template #loader=componentLoader> which causes a different error ( No directive has 'componentLoader' exportAs or something like this )
PS: In the ´´´this.componentFacotryResolver.resolveComponentFactory(component)``` I successfully have each component that has been given to the grid.
I prefer you not to solve my issue but to point me in the right direction and help me see what am I doing wrong in order to improve myself.
Any help will be much appreciated :)
I've managed to solve this issue in a very simple way.
I was trying to do too many things inside the grid component so I removed to code related to the component loader and moved it into a single component, called ComponentLoaderComponent.
Inside the component I've setted up all the logic in the same way than I did in the grid component. So now I have a new ts file like this:
import { Component, ComponentFactoryResolver, Input, OnInit, ViewChild } from '#angular/core';
import { ComponentLoaderDirective } from 'src/app/shared/directives/componentLoader.directive';
#Component({
selector: 'component-loader',
templateUrl: './component-loader.component.html',
styleUrls: ['./component-loader.component.css']
})
export class ComponentLoaderComponent implements OnInit {
#Input() public component: any;
#ViewChild(ComponentLoaderDirective, { static: true }) componentLoader!: ComponentLoaderDirective;
constructor(
private componentFactoryResolver: ComponentFactoryResolver
) { }
ngOnInit(): void {
this.loadComponent();
}
loadComponent():void {
if (this.component) {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.component);
let viewContainerRef = this.componentLoader.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent<any>(componentFactory);
}
}
And an HTML like this:
<ng-template componentLoader>
</ng-template>
Now from the grid component I only have to call the ComponentLoader for every component I want to add to the grid, so the grid html will look like this:
<div
*ngIf=" gridItem.components && gridItem.components.length > 0"
class="component-container"
>
<component-loader
*ngFor="let component of gridItem.components"
[component]="component">
</component-loader>
</div >
Now the components are getting loaded correclty, anyways I still don't know what I was missing in before.
THE PROBLEM
So I have two Angular components, a parent and a child. The parent passes a custom template to the child component, which then hydrates the template with its own data using ngTemplateOutlet.
This works well for the most part. Unfortunately, I run into issues when trying to access the DOM elements of this parent template from the child.
If I try to access <div #container></div> from the default child template using #ViewChild('container',{static: false}), it gets the element without issue. When I do the same using the custom template passed in by app.component, I get the error "cannot read property 'nativeElement' of undefined".
What else do I have to do to access the DOM of my template?
Here's a Stackblitz
App.Component (Parent)
import { Component } from "#angular/core";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {}
<child [customTemplate]="parentTemplate"></child>
<ng-template #parentTemplate let-context="context">
<div #container>HELLO FROM CONTAINER</div>
<button (click)="context.toggleShow()">Toggle Display</button>
<div *ngIf="context.canShow">Container contains the text: {{context.getContainerText()}}</div>
</ng-template>
child.component (Child)
import {
Component,
ElementRef,
Input,
TemplateRef,
ViewChild
} from "#angular/core";
#Component({
selector: "child",
templateUrl: "./child.component.html",
styleUrls: ["./child.component.css"]
})
export class ChildComponent {
#Input() public customTemplate!: TemplateRef<HTMLElement>;
#ViewChild("container", { static: false })
public readonly containerRef!: ElementRef;
templateContext = { context: this };
canShow: boolean = false;
toggleShow() {
this.canShow = !this.canShow;
}
getContainerText() {
return this.containerRef.nativeElement.textContent;
}
}
<ng-container *ngTemplateOutlet="customTemplate || defaultTemplate; context: templateContext">
</ng-container>
<ng-template #defaultTemplate>
<div #container>GOODBYE FROM CONTAINER</div>
<button (click)="toggleShow()">Toggle Display</button>
<div *ngIf="canShow">Container contains the text: {{getContainerText()}}</div>
</ng-template>
MY QUESTION
How do I use #ViewChild to access this div from an outside template that updates with any changes in the DOM? (Note: Removing the *ngIf is NOT an option for this project)
What's causing this? Are there any lifecycle methods that I can use to remedy this issue?
MY HUNCH
I'm guessing that ViewChild is being called BEFORE the DOM updates with its new template and I need to setup a listener for DOM changes. I tried this and failed so I'd really appreciate some wisdom on how best to proceed. Thanks in advance :)
EDIT:
This solution needs to properly display <div #container></div> regardless of whether you're passing in a custom template or using the default one.
ViewChild doesn't seem to pick up a rendered template - probably because it's not part of the components template initially. It's not a timing or lifecycle issue, it's just never available as a ViewChild
An approach that does work is to pass in the template as content to the child component, and access it using ContentChildren. You subscribe to the ContentChildren QueryList for changes, which will update when the DOM element becomes rendered
You can then access the nativeElement (the div). If you wanted you could add listeners here to the DOM element, and trigger cd.detectChanges afterwards, but that would be a bit unusual. It would probably be better to handle DOM changes in the parent element, and pass the required values down to the child using regular #Input on the child
#Component({
selector: "my-app",
template: `
<child>
<ng-template #parentTemplate let-context="context">
<div #container>Parent Template</div>
</ng-template>
</child>
`,
styleUrls: ["./app.component.css"]
})
export class AppComponent {}
#Component({
selector: "child",
template: `
<ng-container *ngTemplateOutlet="customTemplate"> </ng-container>
`,
styleUrls: ["./child.component.css"]
})
export class ChildComponent implements AfterContentInit {
#ContentChild("parentTemplate")
customTemplate: TemplateRef<any>;
#ContentChildren("container")
containerList: QueryList<HTMLElement>;
ngAfterContentInit() {
this.containerList.changes.subscribe(list => {
console.log(list.first.nativeElement.innerText);
// prints 'Parent Template'
});
}
}
I want to access the text I have in a text area in my child component to put it on the parent component and keep it updated.
I was told that #input in angular 4 is supposed to perform two-way binding. But I can't do that that way, and I don't understand why.
I found a workaround for this issue. It includes an #Output to send the info to the parent component. But if Input already does that (in some way I don't know), I want to avoid it.
For example, this is my Parent Component
import { Component } from '#angular/core';
#Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
})
export class SettingsComponent {
private studyDesignText = 'Text';
constructor() {
}
public handleStudyDesignUpdated(designText: any) {
this.studyDesignText = designText;
}
}
It's html
<div class="section section-trials-settings-parent light rounded">
<div class="section section-trials-settings-child">
<div class="pure-g">
<div class="pure-u-1-1">
<app-settings-study-design
[studyDesignText]="studyDesignText">
</app-settings-study-design>
</div>
</div>
</div>
</div>
My child component:
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'app-settings-study-design',
templateUrl: './settings-study-design.component.html',
})
export class SettingsStudyDesignComponent implements OnInit {
#Input() studyDesignText: string;
constructor() {
}
ngOnInit() {
super.onInit();
loadControls();
}
loadControls(): void {
this.startAllTextAreas();
}
private startAllTextAreas() {
this.startTextArea('study-design-textarea');
}
private startTextArea(htmlId: string) {
// code to configure my text area; it's right...
}
If I change the value in the text area and send a signal with #Output so my parent component can be notified and console log the value, the printed value is the initial one. My friend did the same thing and it worked.
What am I missing?
#Input() is always one way binding from parent->child. Two way binding happens in this case, only when you have object as an input property. This is because, the reference for objects remain the same. And when one of the object updates, the other will also get updated. This is not true for string or number. It is always one way binding.
I am trying to abstract out a tabular-data display to make it a child component that can be loaded into various parent components. I'm doing this to make the overall app "dryer". Before I was using an observable to subscribe to a service and make API calls and then printing directly to each component view (each of which had the tabular layout). Now I want to make the tabular data area a child component, and just bind the results of the observable for each of the parent components. For whatever reason, this is not working as expected.
Here is what I have in the parent component view:
<div class="page-view">
<div class="page-view-left">
<admin-left-panel></admin-left-panel>
</div>
<div class="page-view-right">
<div class="page-content">
<admin-tabs></admin-tabs>
<table-display [records]="records"></table-display>
</div>
</div>
</div>
And the component file looks like this:
import { API } from './../../../data/api.service';
import { AccountService } from './../../../data/account.service';
import { Component, OnInit, Input } from '#angular/core';
import { Router, ActivatedRoute } from '#angular/router';
import { TableDisplayComponent } from './../table-display/table-display.component';
#Component({
selector: 'account-comp',
templateUrl: 'app/views/account/account.component.html',
styleUrls: ['app/styles/app.styles.css']
})
export class AccountComponent extends TabPage implements OnInit {
private section: string;
records = [];
errorMsg: string;
constructor(private accountService: AccountService,
router: Router,
route: ActivatedRoute) {
}
ngOnInit() {
this.accountService.getAccount()
.subscribe(resRecordsData => this.records = resRecordsData,
responseRecordsError => this.errorMsg = responseRecordsError);
}
}
Then, in the child component (the one that contains the table-display view), I am including an #Input() for "records" - which is what the result of my observable is assigned to in the parent component. So in the child (table-display) component, I have this:
import { AccountService } from './../../../data/account.service';
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'table-display',
templateUrl: './table-display.component.html',
styleUrls: ['./table-display.component.less']
})
export class TableDisplayComponent {
#Input() records;
constructor() {
}
}
Lastly, here's some of the relevant code from my table-display view:
<tr *ngFor="let record of records; let i = index;">
<td>{{record.name.first}} {{record.name.last}}</td>
<td>{{record.startDate | date:"MM/dd/yy"}}</td>
<td><a class="bluelink" [routerLink]="['/client', record._id ]">{{record.name.first}} {{record.name.last}}</a></td>
When I use it with this configuration, I get "undefined" errors for the "records" properties I'm pulling in via the API/database. I wasn't getting these errors when I had both the table display and the service call within the same component. So all I've done here is abstract out the table-display so I can use it nested within several parent components, rather than having that same table-display show up in full in every parent component that needs it.
What am I missing here? What looks wrong in this configuration?
You need to protect against record being null until it comes in to your child component (and therefore it's view).
Use Elvis operators to protect your template:
<tr *ngFor="let record of records; let i = index;">
<td>{{record?.name?.first}} {{record?.name?.last}}</td>
<td>{{record?.startDate | date:"MM/dd/yy"}}</td>
<td><a class="bluelink" [routerLink]="['/client', record?._id ]"> {{record?.name?.first}} {{record?.name?.last}}</a></td>
You can also assign your input to an empty array to help with this issue:
#Input() records = [];
How can I access the "content" of a component from within the component class itself?
I would like to do something like this:
<upper>my text to transform to upper case</upper>
How can I get the content or the upper tag within my component like I would use #Input for attributes?
#Component({
selector: 'upper',
template: `<ng-content></ng-content>`
})
export class UpperComponent {
#Input
content: String;
}
PS: I know I could use pipes for the upper case transformation, this is only an example, I don't want to create an upper component, just know how to access the component's content from with the component class.
If you want to get a reference to a component of the transcluded content, you can use:
#Component({
selector: 'upper',
template: `<ng-content></ng-content>`
})
export class UpperComponent {
#ContentChild(SomeComponent) content: SomeComponent;
}
If you wrap <ng-content> then you can access access to the transcluded content like
#Component({
selector: 'upper',
template: `
<div #contentWrapper>
<ng-content></ng-content>
</div>`
})
export class UpperComponent {
#ViewChild('contentWrapper') content: ElementRef;
ngAfterViewInit() {
console.debug(this.content.nativeElement);
}
}
You need to leverage the #ContentChild decorator for this.
#Component({
selector: 'upper',
template: `<ng-content></ng-content>`
})
export class UpperComponent {
#Input
content: String;
#ContentChild(...)
element: any;
}
Edit
I investigated a bit more your issue and it's not possible to use #ContentChild here since you don't have a root inner DOM element.
You need to leverage the DOM directly. Here is a working solution:
#Component({
selector: 'upper',
template: `<ng-content></ng-content>`
})
export class UpperComponent {
constructor(private elt:ElementRef, private renderer:Renderer) {
}
ngAfterViewInit() {
var textNode = this.elt.nativeElement.childNodes[0];
var textInput = textNode.nodeValue;
this.renderer.setText(textNode, textInput.toUpperCase());
}
}
See this plunkr for more details: https://plnkr.co/edit/KBxWOnyvLovboGWDGfat?p=preview
https://angular.io/api/core/ContentChildren
class SomeDir implements AfterContentInit {
#ContentChildren(ChildDirective) contentChildren : QueryList<ChildDirective>;
ngAfterContentInit() {
// contentChildren is set
}
}
Note that if you do console.log(contentChildren), it will only work on ngAfterContentInit or a later event.
Good morning,
I've been doing a little research on this topic because something similar has happened to me in my project. What I have discovered is that there are two decorators that can help you for this solution: ViewChildren and ContentChildren. The difference mainly according to what I have found in the network is that ViewChildren accesses the interior of the component and ContentChildren accesses the DOM or the component itself.
To access the upper element from the ng-content you must change the upper element leaving it like this:
<upper #upper>my text to transform to upper case</upper>
And then simply to access the interior (In the component that has the ng-content):
#ViewChildren('upper') upper: QueryList<ElementRef>
And to access the component in general (In the component that has the ng-content):
#ContentChildren('upper') upper: QueryList<ElementRef>
Where did you get the information from: https://netbasal.com/understanding-viewchildren-contentchildren-and-querylist-in-angular-896b0c689f6e
You can also use TemplateRef, which was what worked me in the end because I was using a service to initialize my component.
In your-component.component.html
<!-- Modal Component & Content -->
<ng-template #modal>
... modal content here ...
</ng-template>
In your-component.component.ts
#ViewChild('modal') modalContentRef!: TemplateRef<any>;
... pass your modalContentRef into your child ...
... For me here, the service was the middleman - [Not shown] ...
In modal.component.html
<div class="modal-container">
<!-- Content will be rendered here in the ng-container -->
<ng-container *ngTemplateOutlet="modalContentRef">
</ng-container>
</div>
In modal.component.ts
#Input() modalContentRef: TemplateRef<any> | null = null;
This how I ended up solving the problem with a TemplateRef, since ng-content could not be used since a service was creating the modal