Angular. "*ngIf; else" alternative with custom component - javascript

I want simplify the built in directive ngIf; else with a custom component. The idea is eg.:
#Component({
selector: 'app-main',
template: `
<ng-condition>
<ng-if *ngCondition="number <= 5">
Number {{ number }} is minor or equal 5
</ngif>
<ng-else-if *ngCondition="number <= 10">
Number {{ number }} is minor or equal 10
</ng-else-if>
<ng-else>
Number {{ number }} is major of 10
</ng-else>
</ng-condition>`
})
export class AppComponent {
number = 6;
}
output
Number 6 is minor or equal 10
The idea is:
*ngCondition directive is a custom directive like ngIf
ng-condition component must have ng-if and ng-else-if, ng-else is optional.
Components ng-if, ng-else-if and ng-else are just:
#Component({
selector: 'ng-if',
template: '<ng-content></ng-content>'
})
export class NgIf {}
#Component({
selector: 'ng-else-if',
template: '<ng-content></ng-content>'
})
export class NgElseIf {}
#Component({
selector: 'ng-else',
template: '<ng-content></ng-content>'
})
export class NgElse {}
*ngCondition directive is:
#Directive({
selector: '[ngCondition]'
})
export class NgConditionDirective {
constructor(
private element: ElementRef,
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
#Input()
set ngCondition(val: boolean) {
if(val) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
The ng-condition is:
#Component({
selector: 'ng-condition',
template: '<ng-content></ng-content>'
})
export class NgCondition {
// ... HERE THE MAGIC CODE TO WRITE
}
The idea is, ng-condition component retrive all child component (ng-if, ng-else-if and ng-else) and:
check if a ng-if is present (and only one is available) otherwise raise an exception;
if a ng-if is visible (ng-if *ngCondition is true) all ng-else-if and ng-else are destroyed;
if a ng-else-if is visible, the ng-else is destroyed;
Only one ng-else is avalable, otherwaise an exception is raised;
ng-if, ng-else-if and ng-else only can be placed into ng-condition;
ng-if, ng-else-if must have a *ngCondition directive otherwaise an exception is raised;
ng-else can't have a *ngCondition directive otherwaise an exception is raised;
The main problem is detect change of <ng-content></ng-content> into ng-condition componet and retrive all child component for write all the logic.
Final questions:
How detect change of <ng-content></ng-content> and retrive all the child components?
How check if a child component have a specific directive?
I'm also searching some inspiration, tips and trick for start to develop my idea.
Thanks!
UPDATE
I have developed my idea: https://www.npmjs.com/package/ng-condition

Related

custom if directive does not show expected results

in this tutorial
https://www.sitepoint.com/practical-guide-angular-directives/
i am learning how to create a customised directive. i followed the steps as shown in the code posted below, but despite added the exact code as explained in the aforemenrtioned website, when i run the command
ng serve --open
i get something as shown in the image posted below.
please let me know why myCustomIf is not working. i say that myCustomIf is not working because what i got on the localhost:4200 is something as shown in the image posted
please let me know how to make the myCustomIf works as explained in the tutorial in the above posted link
app.component.ts:
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'ngDirective1';
name = 'Angular';
condition = false;
}
app.myCustomeIfDirective.ts:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
constructor(private templateRef: TemplateRef<any>,private viewContainer: ViewContainerRef){ }
#Input()
setMyCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
app.module:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
constructor(private templateRef: TemplateRef<any>,private viewContainer: ViewContainerRef){ }
#Input()
setMyCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
app.component.html:
<h1 my-error>Hello {{name}}</h1>
<h2 *myCustomIf="condition">Hello {{name}}</h2>
<button (click)="condition = !condition">Click</button>
image:
If you open console it should show smth like:
NG0303: Can't bind to 'myCustomIf' since it isn't a known property of
'h2'
An Angular structural directive, that is written in a short syntax(with *) and that takes one input or more inputs, must have an #Input with the same name as directive's attribute selector(other inputs follow another rule described here What is the exact grammar for Angulars structural directives), e.g.:
#Directive({
selector: '[anyAttr]'
})
export class MyCustomeIfDirective{
#Input()
anyAttr: any;
or
#Directive({
selector: '[anotherAttr]'
})
export class MyCustomeIfDirective{
#Input()
set anotherAttr(val: any) {}
Why is it so?
That's because *ngIf is just a shortcut for expanded version:
<ng-template [ngIf]="...">...
or
*anyAttr => <ng-template [anyAttr]="...">...
Now, let's look at your code:
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
#Input()
setMyCustomIf(condition : boolean) {
Several things to notice:
setMyCustomIf is just a method in your case
if you convert it to a setter set MyCustomIf then MyCustomIf doesnt match myCustomIf because js is case-sensitive.
The solution is:
#Input()
set myCustomIf(condition : boolean) {
Ng-run Example
in your directive (app.myCustomeIfDirective.ts), you need to match the name of your input to the name of the directive (because the condition is passed with that attribute):
#Input("myCustomIf")
set myCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
(note you can also change the name of the function to match the directive name)
stackblitz demo

How do I use #ViewChild with an external ng-template (Angular 11)

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

Access text (not instance of another component) with ContentChild

How can I access a string of text given within the tags of a component
<my-custom-component>THIS TEXT</my-custom-component>
Within a template, I can use ng-content, or if it is an instance of some other class I can access it within the component definition like demonstrated in these examples. However I am interested in detecting if there is a string of text there or not, which I believe would make providedText undefined. However, I am always getting undefined.
#ContentChild(Element, { static: true }) providedText: Text | undefined;
I have tried Text as the first element passed to #ContentChild. Passing any will not work (I don't know why).
StackBlitz
I am interested mostly in finding if there is a string or undefined, but am also curious why ContentChild(Text... isn't working.
Edit:
I have added a potential solution, but it seems pretty imperfect, so I hope something better comes along.
Edit 2:
I now understand that #ContentChild is not a mechanism for selecting whatever native HTML I want without wiring it up to Angular’s dependency graph with a ref, directive, etc.
I am still curious if my proposed solution below is a bad idea for any reason.
My solution for now (since I wish to capture all transcluded content) is to wrap ng-content in a containing element, then get its innerText.
#Component({
selector: "app-parent",
template: `
<span #transcludedContainerRef>
<ng-content></ng-content>
</span>
`
})
export class ParentComponent implements AfterViewInit {
#ViewChild("transcludedContainerRef", { static: false })
transcludedContainerRef: ElementRef | undefined;
buttonText: string;
ngAfterViewInit() {
const isButtonTextPresent = this.transcludedContainerRef.nativeElement
.innerText;
if (isButtonTextPresent) {
console.log(isButtonTextPresent); // successfully logs content
}else {
console.log('No text set');
}
}
}
It does feel hacky, but it works. I am holding out for something better.
it's difficult if I don't know about your <my-custom-component>
In general if your custom component it's only
<ng-content></ng-content>
You can inject in constructor the elementRef
constructor(public el:ElementRef){}
From a parent
<hello >
Start editing to see some magic happen :)
</hello>
You can use
#ViewChild(HelloComponent,{static:false}) helloComponent:HelloComponent
click()
{
console.log(this.helloComponent.el.nativeElement.innerHTML)
}
If your component has any variable -or ViewContent-, you can access this variables in a similar way
So the other way to read the inner text from the component is that child component emit the value whatever it get's as input from other component. See below:
hello.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '#angular/core';
#Component({
selector: 'hello',
template: `<h1>Hello {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent implements OnInit {
#Input() name: string;
#Output() innerText: EventEmitter<string> = new EventEmitter();
ngOnInit() {
this.innerText.emit(this.name);
}
}
app.component.ts
import { Component, ContentChild, AfterContentInit, OnInit } from "#angular/core";
#Component({
selector: "app-parent",
template: "content from <code>app-parent</code>"
})
export class ParentComponent implements AfterContentInit {
#ContentChild(Element, { static: true }) providedText: Text | undefined;
ngAfterContentInit() {
console.log("ngAfterContentInit Content text: ", this.providedText);
}
}
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
name = "Angular";
_innerText: string;
ngAfterContentInit() {}
get childContent(): string {
return this._innerText;
}
set childContent(text) {
this._innerText = text;
}
innerTextFn(innertext: string) {
this.childContent = innertext;
console.log('Event: ', innertext);
}
}
app.component.html
<hello name="{{ name }}" (innerText)="innerTextFn($event)"></hello>
<app-parent>This is the content text</app-parent>
Here is stackblitz url to check: https://stackblitz.com/edit/angular-bacizp
I hope this may helpful for you and if yes then accept this as correct answer.

Create a "clone" of an Angular2 component with ng-content

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?

angular 2 access ng-content within component

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

Categories