X Tutup
The Wayback Machine - https://web.archive.org/web/20220504193006/https://github.com/angular/angular/issues/12530
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ng-content default content #12530

Open
mbeckenbach opened this issue Oct 25, 2016 · 45 comments
Open

ng-content default content #12530

mbeckenbach opened this issue Oct 25, 2016 · 45 comments
Assignees
Milestone

Comments

@mbeckenbach
Copy link

@mbeckenbach mbeckenbach commented Oct 25, 2016

I'm submitting a ... (check one with "x")

[ ] bug report => search github for a similar issue or PR before submitting
[x] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior
When you add something inside a ng-content tag you get an error message: element cannot have content.

Expected behavior
Should render the html inside the ng-content tag as a default if no content was given.

Minimal reproduction of the problem with instructions
Create a component that uses ng-content somewhere. Put some html inside, that should be rendered when nothing else is 'injected' there from outside. Then use the component somewhere else.

What is the motivation / use case for changing the behavior?
In some cases you need a default behavior that can be replaced. In my case its an app header that shows the logo. When you navigate to a sub-page the logo will be replaced by a back button. It feels strange to add something like an input property to hide or show the logo. Some default content for ng content placeholders would feel much more intuitive.

Please tell us about your environment:
Win10, Angular CLI beta 18, VS Code

  • Angular version: 2.1.0
    2.1.0
  • Browser:
    All
  • Language:
    TS
  • Node (for AoT issues): node --version =
@mbeckenbach
Copy link
Author

@mbeckenbach mbeckenbach commented Oct 25, 2016

If i remember correctly thats the way multi slot transclusion works in ng 1

@vicb vicb added feature comp: core freq1: low labels Oct 26, 2016
@webmutation
Copy link

@webmutation webmutation commented Jan 12, 2017

Is there any other way to have a fallback-content for the slot? I found a workaround but its quite ugly

<div #ref><ng-content></ng-content></div> 
           <span *ngIf="ref.childNodes.length == 0">
              Display this if ng-content is empty!
           </span>

@DzmitryShylovich
Copy link

@DzmitryShylovich DzmitryShylovich commented Jan 12, 2017

not yet

@emilio-martinez
Copy link

@emilio-martinez emilio-martinez commented Apr 25, 2017

I don't know if adding something inside ng-content is really the best approach for this, but I agree that a fallback value would be desirable. Perhaps it could interop with ngIf Else Then?

@DrewLandgrave

This comment has been minimized.

@AndrinGautschi
Copy link

@AndrinGautschi AndrinGautschi commented Aug 5, 2017

Anything new on this feature request?

@acaua
Copy link

@acaua acaua commented Sep 15, 2017

@webmutation , theres a slight mistake in your solution: you should test if "ref.children.length == 0", not if "ref.childNodes.length == 0"

It should be like this:

<div #ref><ng-content></ng-content></div> 
<span *ngIf="ref.children.length == 0">
  Display this if ng-content is empty!
</span>

@webmutation
Copy link

@webmutation webmutation commented Sep 15, 2017

@acaua Actually that is not my code, but I agree node.children should be used and not node.childNodes, not that it is a mistake but falsely assumes that child nodes are of type Element. Even though it would be reasonable to assume that whatever is projected in ng-content would be of type Element, it can create issues, therefore the proper API should be used, and since node.children is supported for IE9+ cross browser support also is not an issue.

Node.children is a read-only property that returns a live HTMLCollection of the child elements of Node.

The Node.childNodes read-only property returns a live collection of child nodes of the given element where the first child node is assigned index 0.
To get a collection of only elements, use ParentNode.children instead.

If we look at the polyfill for node.children we see the use of childNodes but it is filtered by node.nodeType === 1 (Where nodeType ===1 represents An Element node such as < p >or < div > ), so node.children should always work even when it needs to be polyfilled.

Object.defineProperty(constructor.prototype, 'children', {
            get: function() {
                var i = 0, node, nodes = this.childNodes, children = [];
                while (node = nodes[i++]) {
                    if (node.nodeType === 1) {
                        children.push(node);
                    }
                }
                return children;
            }
        });

@mbeckenbach You are correct that is the way it works in ng1, therefore it would be nice to keep it consistent, unless there is a good reason to make it different.

Still hopping the ng1 behavior as suggested by OP gets added, since it was straightforward to use.

@didii
Copy link

@didii didii commented Oct 6, 2017

Keep in mind that both children and childNodes will have different behavior depending on how you use them. See the examples below to decide which workaround you want to currently use.


node.children only works when another element is added. Not if only text is present.
Example:

<default-component>Foo</default-component>

where default-component has the template as @acaua mentions:

<div #ref><ng-content></ng-content></div> 
<span *ngIf="ref.children.length == 0">
  Display this if ng-content is empty!
</span>

This will result into

Foo
Display this if ng-content is empty!


This is not the case when you use childNodes. In that case, the opposite happens if the tag has any whitespace or comment.
Example:

<default-component><!--hi--></default-component>

or

<default-component>
</default-component>

or

<default-component> </default-component>

with as template

<div #ref><ng-content></ng-content></div> 
<span *ngIf="ref.childNodes.length == 0">
  Display this if ng-content is empty!
</span>

will all result into nothing that is displayed.


EDIT

Using childNodes we can however check it with the following function:

isEmpty(element: HTMLElement): boolean {
    const nodes = element.childNodes;
    for (let i = 0; i < nodes.length; i++) {
        const node = nodes.item(i);
        if (node.nodeType !== 8 && nodes.item(i).textContent.trim().length !== 0) {
            return false;
        }
    }
    return true;
}

This will ignore nodes that are only whitespace and are comments (nodeType 8).

@jack-bliss
Copy link

@jack-bliss jack-bliss commented Jan 18, 2018

being able to determine wether or not ng-content exists seems very important, and having an option that works with unviersal would be very nice.

@ngbot ngbot bot added this to the Backlog milestone Jan 23, 2018
@benneq
Copy link

@benneq benneq commented Mar 17, 2018

I have a similar requirement:
<h1><ng-content select="[title]"></ng-content></h1>
I want to completely remove the <h1> if there's no content set.

Though I need something like this:
<h1 *ngIf="titleRef"><ng-content select="[title]" #titleRef></ng-content></h1>

@harm-less
Copy link

@harm-less harm-less commented Sep 3, 2018

@benneq I just succeeded with a small hack:

export class ContentComponent implements AfterViewInit {

  @ViewChild("contentAll")
  contentAll: ElementRef;

  constructor(private changeDetector: ChangeDetectorRef) {  }

  ngAfterViewInit(): void {
    console.log(this.contentAll);

    this.changeDetector.detectChanges();
  }
}
<div #contentAll *ngIf="contentAll ? contentAll.nativeElement.children.length : true">
    <ng-content></ng-content>
</div>

contentAll is not available during init which is why the entire element will NOT be rendered at any point afterwards. When you make sure contentAll is true by default after the init phase, you'll be able to use it to check whether or not the ng-content element is empty or not in a later phase (AfterViewInit in this case).
Note: the detectChanges() was necessary as the change wasn't registered in the view after bootstrapping.

I'm not sure if a HTML only fix is possible as I didn't get that to work.

I did manage to clean up the view a bit if you prefer that:

export class ContentComponent implements AfterViewInit {

  @ViewChild("contentAll")
  contentAll: ElementRef;
  private hasContent: boolean = true;

  constructor(private changeDetector: ChangeDetectorRef) {  }

  ngAfterViewInit(): void {
    console.log(this.contentAll);
    this.hasContent = this.contentAll.nativeElement.children.length;
    this.changeDetector.detectChanges();
  }
}
<div #contentAll *ngIf="hasContent">
    <ng-content></ng-content>
</div>

@benneq
Copy link

@benneq benneq commented Sep 3, 2018

Your cleaned up version only works if you only use this functionality once within a component.

My example was a bit stripped down. In reality it's more like this:

<h1><ng-content select="[title]"></ng-content></h1>
<h2><ng-content select="[subtitle]"></ng-content></h2>
<p><ng-content select="[body]"></ng-content></p>

And each of h1, h2, p can be empty and shouldn't be rendered if it has no content.

@WizardPC
Copy link

@WizardPC WizardPC commented Oct 8, 2018

@benneq i have the exact same case with a modal componenet, if footer is empty i don't want to render it.

Here my warkaround in css (.scss here)

html

<div class="modal-footer">
  <ng-content select="[footer]"></ng-content>
</div>

scss

.modal-footer {
  &:empty {
    display: none;
  }
}

@StefanRein
Copy link

@StefanRein StefanRein commented Nov 11, 2018

@WizardPC Thanks!

I wanted to provide a default component if none was set in the ng-content. As they are siblings in my case, one could extend this with your solution.

Which is compatible since IE 9 and partially since IE7/8: https://caniuse.com/#feat=css-sel3

HTML

<div class="my-custom-component-content-wrapper">
    <ng-content select="my-custom-component"></ng-content>
</div>
<my-custom-component>
    This shows something default.
</my-custom-component>

CSS

.my-custom-component-content-wrapper:not(:empty) + my-custom-component {
    display: none;
}

@dawidgarus
Copy link

@dawidgarus dawidgarus commented Dec 6, 2018

You can abstract the logic to show default content to directive:
https://stackblitz.com/edit/angular-ehefzr?file=src%2Fapp%2Fhello.component.ts

@hullis
Copy link

@hullis hullis commented Jan 2, 2019

Does Ivy contribute to this?

@r-hannuschka
Copy link

@r-hannuschka r-hannuschka commented Mar 6, 2019

@dawidgarus greate solution i like it, but i get a problem if i do this.

<li class="list-item" translate="SMC_UI.LIST.HEADER.TOTAL" [translateParams]="{COUNT: total}"></li

It throws an Exception:

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'COUNT: undefined'. Current value: 'COUNT: 0'. It seems like the view has been created after its parent and its children have been dirty checked. Has it been created in a change detection hook ?

What helps me now is to wrap this into a window.setTimeout with a delay from 0, but it looks a bit hackish.

    public ngAfterContentChecked() {

        const childNodes = Array.from(this.node.childNodes);
        const hasContent = childNodes.some((node) => node.nodeType === 1 || node.nodeType === 3);

        if (hasContent !== this.hasContent) {
            this.hasContent = hasContent;
            if (hasContent) {
                this.container.clear();
            } else {
                window.setTimeout(() => {
                    this.container.createEmbeddedView(this.defaultTemplate)
                }, 0);
            }
        }
    }

@AckerApple
Copy link

@AckerApple AckerApple commented Mar 6, 2019

@r-hannuschka

I have stupid set zero timeouts, allllllllllllll over our Angular code just to shut that type of error up.

@didii
Copy link

@didii didii commented Mar 15, 2019

@AckerApple That error is typically due to you as developer not handling change detectin/state changes well. The issue appears when code is executed due to a change detection that changes the state again. This might or might not result in an infinite loop of change detection which is both very bad for performance and means a deeper flaw in how state is handled.
See this SO answer or this blog if you want to know more.

@AckerApple
Copy link

@AckerApple AckerApple commented Mar 15, 2019

@didii, no it’s not as simple as regurgitating some documentation. I know you may think you know the change detection issue and you’ve read that it’s highlighting a deeper issue but you can stop right there.

Often I will bind an ID and a loading counter. When that ID changes, I update the binded loading counter and fetch data. In that scenario, I always setTimeout on my load counter increasing so displayed loading spinners can be displayed without causing the change detection error.

A change can cause changes. Simple as that. I’ve been through all the documentation on this and still find the setTimeout the best and most widely used.

@jcroll
Copy link

@jcroll jcroll commented Mar 15, 2019

Don't use setTimeout() @AckerApple you're just asking for trouble with the Angular component lifecycle

@AckerApple
Copy link

@AckerApple AckerApple commented Mar 15, 2019

@jcroll, both links provided by @didii, have references to recommending use of setTimeout.

I've been up and down this topic too many times and concluded it's just so much less hassle to actually make use of setTimeout. My typical use case is for loading indicators.

I could, do and have, used a shared provider class so nothing has to be binded at all. But I many times found myself not wanting to create a provider, register it, require it, and so on..... You cannot convince me not to use the setTimeout, I'm too confident in my Angular knowledge that I know when I can get away with a two-way bind that has a setTimeout to break the change cycle.

I got it guys. I do. I'm going to continue using setTimeout and recommending it

@AckerApple
Copy link

@AckerApple AckerApple commented Mar 15, 2019

Yo @jcroll , my original comment was intended to give the guy @r-hannuschka a little confidence that the setTimeout is not a sin.

I'll tell you what happens if I remove my setTimeouts.... I get dev errors only.

@AckerApple
Copy link

@AckerApple AckerApple commented Apr 18, 2019

I recently did a lot of homework on this subject thanks to others pushing me to do so.

Slowest

setTimeout(() => {
  this.renderer.addClass(this.element, 'is-empty');
  this.container.createEmbeddedView(this.noContent);
},0);

Faster, depending on what the window is currently working on until next repaint cycle

window.requestAnimationFrame(() => {
  this.renderer.addClass(this.element, 'is-empty');
  this.container.createEmbeddedView(this.noContent);
});

Faster

Promise.resolve().then(()=>{
  this.renderer.addClass(this.element, 'is-empty');
  this.container.createEmbeddedView(this.noContent);
})

Second to Lastly, I theorize a faster way exists by NOT using ngAfterContentChecked at all and that using something like an HTML MutationObserver would be the most performant way to go (if even applicable). I myself would love to see how anyone else could perform the required change without breaking the Angular change detection cycle (thats where I've started dreaming up how to apply an Observerables as its been best way to trigger changes).

Lastly, I got completely around the main issue in this thread by just not even trying to have default ng-content. It's been way long but I totally changed my approach and never needed default ng-content since.

@adover
Copy link

@adover adover commented Apr 25, 2019

This seemed like the cleanest way to me:

@ViewChild('someContentRef') content: ElementRef;

hasMessage = () =>
    this.content.nativeElement.childNodes.length > 0 &&
    Boolean(this.content.nativeElement.childNodes[0].textContent.trim());

I'm making an assumption that the first element will have some form of text, which seems fair for most use cases.

Doesn't give me any errors when using ChangeDetectionStrategy.OnPush

@rsarmiento-pl
Copy link

@rsarmiento-pl rsarmiento-pl commented May 16, 2019

Any update on this feature request?

I am using this workaround for the meantime, its a bit 'hacky':

Class Template:

  <div>
    <div #customContentContainer>
      <ng-content select="[customContent]" ></ng-content>
      <ng-container  *ngIf="customContentContainer?.firstElementChild?.hasAttribute('customContent'); else defaultContent">
      </ng-container>
    </div>
    <ng-template #defaultContent>
        <div>This is the default content</div>
    </ng-template>
  </div>

Usage:

<custom-component>
  <span customContent> My Custom Content Here</span>
</custom-component>

Neater way but without using ng-content itself:

Class:

<ng-container *ngIf="customContentTemplate; else defaultContentTemplate">
   <ng-container  *ngTemplateOutlet="customContentTemplate?.templateRef" > </ng-container>
</ng-container>
<ng-template #defaultContentTemplate>
   <div>This is the default content</div>
</ng-template>

export class CustomComponent {
  @ContentChild(CustomContentDirective)
  public customContentTemplate: CustomContentDirective;
}

Directive:

@Directive({
  selector: '[customContent]'
})

export class CustomContentDirective {
  constructor(public templateRef: TemplateRef<any>) { }
}

Usage:

<custom-component>
  <span *customContent> My Custom Content Here</span>
</custom-component>

@matheo
Copy link

@matheo matheo commented Nov 16, 2020

@mhevery looking at this answer at StackOverflow,
they mention Aurelia's fallback slots, and Vue's default content inside ng-content.

Can we have a clean solution like that please? this is 4 years old,
and is looking like Bitbucket without CODEOWNERS support at this day :( sad stuff

@mhevery
Copy link

@mhevery mhevery commented Nov 16, 2020

It is on our radar, but we have higher priority items in front of it, so we don't have a commit date for it.

@lukenofurther
Copy link

@lukenofurther lukenofurther commented Feb 1, 2021

Vue's slot implementation is truly elegant - default content just goes inside the slot tag, as the OP here suggested. It's incredibly simple and makes it nice and easy to use higher order components. Because it's handled by the core library there's no need to worry about which workaround you use and the compatibility between versions/situations etc.

Angular has some catching up to do here.

@petebacondarwin petebacondarwin added this to Needs Project Proposal in Feature Requests Jul 2, 2021
@petebacondarwin petebacondarwin moved this from Needs Project Proposal to Yes but decide effort in Feature Requests Jul 2, 2021
@alxhub alxhub assigned AndrewKushnir and unassigned mhevery Jul 8, 2021
@alxhub alxhub moved this from Yes but decide effort to Needs Project Proposal in Feature Requests Jul 8, 2021
@AndrewKushnir AndrewKushnir moved this from Needs Project Proposal to Backlog in Feature Requests Jul 9, 2021
@AndrewKushnir AndrewKushnir moved this from Backlog to Proposed Projects in Feature Requests Jul 9, 2021
@woeterman94
Copy link

@woeterman94 woeterman94 commented Aug 31, 2021

Is there any update years later? :)

@adumesny
Copy link

@adumesny adumesny commented Sep 9, 2021

so many votes, such an elegant solution others are doing...and 5 years later. really ?

@matheo
Copy link

@matheo matheo commented Nov 8, 2021

@AndrewKushnir @alxhub this got stuck 4 months ago?
can we expect this feature for Angular v14 please?
it would be awesome just like the features released in v13

@MonsieurMan
Copy link

@MonsieurMan MonsieurMan commented Nov 23, 2021

I managed to workaround using querySelector. It only works if you expect a specific selector from your users though.

@Component({
	selector: "default-ng-content",
	template: `
		<ng-content select="[selector]"></ng-content>
		<ng-template [ngIf]="useDefaultContent">
			Here your default content
		</ng-template>
	`,
})
export class DefaultNgContentComponent implements AfterContentInit {
	useDefaultContent = false;

	constructor(private elRef: ElementRef<HTMLElement>) {}

	ngAfterContentInit() {
		// We check if there is the expected selector deeper in the DOM
		const nav = this.elRef.nativeElement.querySelector("[selector]");
		// If not, we use the default content.
		this.useDefaultContent = nav === null;
	}
}

@michaelurban
Copy link

@michaelurban michaelurban commented Apr 1, 2022

@AndrewKushnir This is something that I bump against time and time again. Feels like it's another basic feature/bug that the community has decided is a priority but Google has declared "NOT A PRIORITY". It has been almost five and a half (5.5) years. Would a community implementation be consider or accepted?

I have about 6 issues like this that I track, basic Angular functionality that is missing/incorrect. I have dozens if not hundreds of instances of workarounds in my codebases. It's getting ridiculous. Removing any one class of workaround is going to be a major feat if/when the fix comes.

Imagine if ngIf didn't exist and instead developers had to put <selector>:empty all over their codebases with notes that like: "Needs to be updated once Angular Bug #XYZLMNOP is addressed." After six years they're going to have a lot of code to update.

That's the position you're putting thousands of developers in by keeping issues like these open for half a decade or more.

It's disrespectful. And, not something that most open source projects/communities would consider acceptable behavior.

@jodinathan
Copy link

@jodinathan jodinathan commented Apr 1, 2022

this exists and work ok in AngularDart.

maybe Google could ask how they did it?

@r-hannuschka
Copy link

@r-hannuschka r-hannuschka commented Apr 2, 2022

@MonsieurMan i think this is a good solution, instead of a specific listener you can use a Directive which has a data selector which can be used as selector.

If you expect it is a static value or it is allways existing or not (static) you can use ContentChild for this and skip afterContentInit or timeouts.

@example

@Directive(
   selector: "[myContentDirective]"
)
class MyContentDirective {}

@Component({
   template: `
    <div ngIf="!defaultContent"> default content </div>
    <ng-content select="[myContentDirective]"></ng-content>
   `
})
class MyComponent {
  // static true so we expect it exists as soon the component is rendered and not hidden by ngIf, nfSwitchCase, async
  @ContentChild(MyContentDirective, { read: MyContentDirective, static: true }) 
  private defaultContent: MyContentDirective;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Feature Requests
Proposed Projects
Development

No branches or pull requests

X Tutup