X Tutup
Skip to content

[Feature Request] Provide ability to specify class resolution logic at component's host when supplied by multiple sources. #67519

@YanGeruch

Description

@YanGeruch

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

compiler, core

Description

There is currently no way to customize how conflicting host attribute values from multiple sources (component, directives, host binding) are resolved. Classes are concatenated; singular attributes are last-write-wins. Developers cannot intercept this.

Tailwind CSS, Angular's styling option since v21, makes this a concrete problem. Concatenating conflicting utility classes like bg-blue-500 bg-red-500 produces unpredictable results governed by stylesheet generation order. The ecosystem-standard solution is tailwind-merge (twMerge), already ubiquitous in React, Svelte, and SolidJS component libraries. Angular has no declarative way to use it.

Proposed solution

Add option to configure using twMegre instead of String.join for component's class resolution logic when classes are provided by multiple sources.

Angular should consider supporting twMerge as a first-class feature of the framework and enable it through angular.json configuration framework-wide. When creating new project it will be offered as an option after selecting tailwind as styles. The resolution order will be supported via angular.json configuration. This feature will facilitate improved DX of using component's host element for styling thus reducing nesting and allowing tailwind based library components to expose styles (like background color) on the host for easy override by the consumer without using !important. This feature also partially addresses problems #19119 and #18877 are trying to solve (but specific for tailwind).

Alternatively

An optional hostAttributeResolvers map on @Component. Each resolver receives contributed values in component, directives, host and returns the final value.

type AttributeResolver = (sources: {
  component: string[];
  directives: string[];
  host: string[];
}) => string;

since its possible to define same attribute multiple times we are supplying them as array. For directives the nested arrays should be flattened.

@Component({
  selector: 'app-button',
  hostDirectives: [SubmitButtonDirective], 
  hostAttributeResolvers: {
    class: ({ component, directives, host }) => twMerge(component, directives, host),
    id: ({ component, directives, host }) => host[0] || directives[0] || component[0],
  },
  host: {
    class: 'px-4 py-2 bg-blue-500 text-white rounded',
    id: 'default-btn',
  },
  template: `<ng-content />`,
})
export class ButtonComponent {}
  <app-button id=customId coolBtnDirective class="bg-red-500"/>

This feature while giving developers more control allows for some footguns (like suppressing consumer's ability to assign attributes at all). It should be considered

Alternatives considered

Tuple instead of object

type AttributeResolver = ([string[], string[], string[]]) => string;

@Component({
  hostAttributeResolvers: {
    class: twMerge;
  },
})

Functionally equivalent but positional meaning is invisible at the call site. More concise by matching twMerge signature if most developers going to use same resolution order. However the named object is self-documenting.

Extending the existing host property

host: {
  class: {
    value: 'px-4 py-2 bg-blue-500',
    resolve: (sources) => twMerge(sources.component, sources.directives, sources.host),
  },
}

Changes the shape of Record<string, string> to Record<string, string | { value: string, resolve: AttributeResolver }> — requires language service support for property binding inside value. This approach has benefit of colocation. A hostAttributeResolvers purely additive and fully backward compatible. Updating host also poses question of updating @HostBinding, could be skipped since it's deprecated.

Event resolvers (hostEventResolvers)

A parallel API controlling invocation order when the same event is bound by multiple sources. Could be added for feature parity, but unlike attribute conflicts there is no established real-world pattern demanding custom event resolution at the decorator level.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      X Tutup