Skip to content

Defining provider precedence on the same element #68737

@eblocha

Description

@eblocha

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

Currently, providing the same token across multiple directives on the same element does not guarantee which instance will be used.

https://angular.dev/guide/di/defining-dependency-providers#component-or-directive-providers

NOTE: If multiple directives on the same element provide the same token, one will win, but which one is undefined.

I would like to be able to rely on some level of precedence. Specifically, I would like to stabilize the current behavior, where directives applied to a component host in the template will override the providers on that component.

This is extremely useful for component libraries, because it allows us to use providers on a component to define default behavior that can be overridden by directives applied to the same component.

Example: a date input component. Such a component may define a service to determine how a text value is parsed into a date. Some date fields may require a time component, some may only care about the month and year. The base component can define a behavior for the most common case, and provide additional directives to change the date format.

@Component({
  selector: 'app-date-input',
  providers: [
    {
      provide: DATE_FORMAT,
      useClass: DefaultDateFormat,
    },
  ],
})
export class DateInputComponent {
  private readonly format = inject(DATE_FORMAT, { self: true })
}
@Directive({
  selector: '[appDateTime]',
  providers: [
    {
      provide: DATE_FORMAT,
      useClass: DateTimeFormat,
    },
  ],
})
export class DateTimeDirective {}
<!-- this uses DateTimeFormat, guaranteed by Angular -->
<app-date-input appDateTime />

Proposed solution

Define a precedence order for providers on the same element. My proposal:

  1. Directives applied in the template win. If multiple directives applied this way provide the same token, it is undefined which wins as it is today.
<!-- providers from someDirective win here -->
<some-component someDirective />
  1. The component's host directives win next.
  2. Component providers used as the final fallback.

I'm not 100% certain whether 2 or 3 should be swapped (or merged). If component providers win over host directives, it allows the component to override a provider on one of its host directives. However I am not sure if implementing it that way might create performance problems or require significant refactoring. It may make sense to keep that undefined as well.

This would allow a component to define "default" behaviors via a token or service implementation in its providers, or through its host directives. An application using this component can override these behaviors by providing a new implementation via a directive.

Alternatives considered

The main alternative is to not provide the token in the component providers, and instead construct the default implementation if the token is not provided.

// Get the format for this date field.
// Imagine this service is stateful, so it needs to be component-local.
inject(DATE_FORMAT, { self: true, optional: true }) ?? new DefaultDateFormat()

This has a few problems:

  1. Constructing services with new is an anti-pattern in the DI system.
  2. The token is not available to sub-components or directives.
  3. The token is not available to viewChild queries in the parent component.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimegemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions