angular: NG0912 error after migrating (to 16) multiple components with no selector

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

Yes

Description

Components generated with the CLI (pre 16.x or 16.x) using --skip-selector result in the browser displaying NG0912 error, claiming that multiple of my components have colliding selector ng-component, when using Angular 16. This error is not present in at least 13, 14, and 15.

The components in question all look like this:

import { Component } from '@angular/core';
@Component({
  templateUrl: './foo-with-no-selector.component.html',
  styleUrls: ['./foo-with-no-selector.component.scss'],
})
export class FooWithNoSelector{ ... some stuff }
@Component({
  templateUrl: './bar-with-no-selector.component.html',
  styleUrls: ['./bar-with-no-selector.component.scss'],
})
export class BarWithNoSelector{ ... some stuff }

These components are used only in Routing (or manually created as dialogs using material). Afaik the convention for route only components was to omit selector. Is this change intentional or is it documented somewhere?

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

NG0912: Component ID generation collision detected. Components 'FooWithNoSelector' and 'BarWithNoSelector' with selector 'ng-component' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://angular.io/errors/NG0912

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 16.0.0
Node: 18.16.0
Package Manager: npm 9.6.4
OS: win32 x64

Angular: 16.0.0
... animations, cdk, cli, common, compiler, compiler-cli, core
... forms, material, material-luxon-adapter, platform-browser
... platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1600.0
@angular-devkit/build-angular   16.0.0
@angular-devkit/core            16.0.0
@angular-devkit/schematics      16.0.0
@schematics/angular             16.0.0
rxjs                            7.8.1
typescript                      4.9.5

Anything else?

No response

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 22
  • Comments: 44 (14 by maintainers)

Commits related to this issue

Most upvoted comments

But that’s just a hack, it’s not a solution. I should not have to add anything to a non-routable component to make it work.

This is still an ongoing issue with angular 16.1.1

I really do not want to add selectors/useless properties to my components to get rid of this error, but I want to know if I have 2 instances of the same selector.

The fact that you default to saying componentA and componentB with selector ‘ng-component’ generated the same component ID should lead you to either “hide” the error when the selector is ng-component, or add the whole class metadata

It’s quite annoying to have a bunch of spam in the console when your routing components don’t have selectors.

It would be nice if instead of just assigning “ng-component” to all selector-less components that you’d generate an imaginary selector based on the class name, or just use garbled nonsense. I don’t want to disable ng0912 because I find it quite useful to help debug some issues, but I also don’t want to add selectors to components that don’t need it, or components that shouldn’t be instanciated through selectors.

“”“Fixing”“” this by adding meaningless inputs, attributes or anything else is also a really bad smell and will lead to technical debt. I also don’t want to add //angular big dum dum ignore line do not use do not remove unless warning or something equivalent on top of “unnecessary” fixes

Having the same issue. A solution would be to add an option to angular.json to disable it…

@Sajyd Please tell me how to disable this ?)

Having the same issue. A solution would be to add an option to angular.json to disable it…

This change is pretty frightening. In all projects I was involved I always advocated for removing selectors on components that are included through a route or opened as an overlay. This should avoid that somebody includes a component into the template which is not intended for a certain component. This also reduces the number of components that are suggested by IDE. Now I will have to add something to this components just so they have an id. Would it be possible to disable this behavior for apps that do not use SSR?

But that’s just a hack, it’s not a solution. I should not have to add anything to a non-routable component to make it work.

It’s also a violation of Angular’s own style guide:

Style 06-03

Is there any movement on this? Part of a CI/CD pipeline will break a build if the word “error” appears in a particular log. Since this references this URL: https://angular.io/errors/NG0912 it is considering it an error. If @JeanMeche is correct, that this is NOT an error, then why is it referenced in an error URL? Seems to me, this is a defect. Routed components don’t have selectors and I don’t like the idea of adding arbitrary selectors or host bindings to work around this.

@alan-agius4 Note: ignoring the collisions can result in a broken behaviour during runtime.

The reason why this is a warning and not a hard error is because whilst in most cases this will not cause a runtime issue, there are cases cases when it will.

Could you explain what those cases are, and what the broken behavior would be?

Also, what is the component ID actually used for? I would have thought the component’s type would uniquely identify a component.

This makes no sense, if the selector field is the best way to create a unique ID (using bindings, styles or other fields is a hack, since they can be similar between components as well), the only option from the angular team is to set the selector field to be mandatory and not optional.

It sounds like the mechanism for differentiating between components using a fingerprint of their metadata isn’t as robust and maybe a different method should be considered (file path maybe?).

I doubt making the selector mandatory would be a good change. There are many examples where it’s not a useful thing, such as routing components and dialog components, where you really don’t want someone to just start “manually rendering” those components outside of their intended context. This is without considering all the apps this kind of change would break, we probably have around 100 “selector-less” components in our app.

I do think you hit the nail in the head when you say that the fingerprinting is incomplete.

The reason why this is a warning and not a hard error is because whilst in most cases this will not cause a runtime issue, there are cases cases when it will.

We did discuss the component ID generation at build time, but there are some challenges there based on symbol location due to potential differences in paths on OS (Windows vs Linux), multiple TS roots, etc… we are however, still very much looking into this.

This makes no sense, if the selector field is the best way to create a unique ID (using bindings, styles or other fields is a hack, since they can be similar between components as well), the only option from the angular team is to set the selector field to be mandatory and not optional.

It sounds like the mechanism for differentiating between components using a fingerprint of their metadata isn’t as robust and maybe a different method should be considered (file path maybe?).

Fair enough. My naive suggestion would be an md5 hash computed from the class name as a secondary identifier

ignoring the collisions can result in a broken behaviour during runtime

So why is it just a warning and not a hard error?

Screenshot 2023-09-02 at 19 03 52

The solution for a bunch of components that are used in component outlets and routes… is just sprinkle some random properties on top of them so they differentiate from each other… why isn’t this being caught during compilation and handled automatically?

We getting this error even with selector

I ran into this problem with an angular 16 app that uses material and has not been migrated to mdc components:

core.mjs:1955  NG0912: Component ID generation collision detected. Components 'MatTextColumn' and 'MatLegacyTextColumn' with selector 'mat-text-column' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://angular.io/errors/NG0912

core.mjs:1955  NG0912: Component ID generation collision detected. Components 'MatTab' and 'MatLegacyTab' with selector 'mat-tab' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://angular.io/errors/NG0912

@JoostK, I think we could add exportAs, inputs and outputs.

@JeanMeche, Indeed the name is not consistent between server and client bundles due to minification.

The name would indeed not be stable.

It seems like inputs/outputs are not currently included in the hash function, nor is exportAs. Especially the latter is interesting, as it is invisible to what ends up being rendered.

@alan-agius4 What would you think of adding componentDef.type.name to the hashSelectors to reduce collisions ? Would that still work even after minifiers ?

That would fix @alexaka1’s issue.

@JeanMeche In my opinion, more unnecessary extra things that I have to add. Ie.: adding a host makes no sense because the host would be the router-outlet or the cdk-overlay in my use case. That also suggests behaviour for my component that doesn’t actually exist. Is there a way that it just works, without me going in and adding syntactic sugar to existing code? I’m just curious. If no, then okay, I don’t like this, but I accept the fact that my DX is regressing with this update.

@alan-agius4 @JeanMeche Thanks for clearing it up! I would disagree that the metadata is identical though as the two components have different templateUrl and styleUrls. Also obviously these two components are not in the same file either, I just simplified it for the example.