Skip to content

Commit

Permalink
feat(core): add API to inject attributes on the host node (#54604)
Browse files Browse the repository at this point in the history
Angular has the `@Attribute` decorator that allows for attributes to be injected from the host node, but we don't have an equivalent for the `inject` function. These changes introduce the new `HostAttributeToken` class that can be used to inject attributes similarly to `@Attribute`. It can be used as follows:

```typescript
import {HostAttributeToken, inject} from '@angular/core';

class MyDir {
  someAttr = inject(new HostAttributeToken('some-attr'));
}
```

The new API works similarly to `@Attribute` with one key exception: it will throw a DI error when the attribute doesn't exist, instead of returning `null` like `@Attribute`. We made this change to align its behavior closer to other injection tokens.

PR Close #54604
  • Loading branch information
crisbeto authored and dylhunn committed Feb 27, 2024
1 parent 4066e62 commit 331b16e
Show file tree
Hide file tree
Showing 6 changed files with 396 additions and 5 deletions.
20 changes: 20 additions & 0 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,13 @@ export interface Host {
// @public
export const Host: HostDecorator;

// @public
export class HostAttributeToken {
constructor(attributeName: string);
// (undocumented)
toString(): string;
}

// @public
export interface HostBinding {
hostPropertyName?: string;
Expand Down Expand Up @@ -789,6 +796,19 @@ export function inject<T>(token: ProviderToken<T>, options: InjectOptions & {
// @public (undocumented)
export function inject<T>(token: ProviderToken<T>, options: InjectOptions): T | null;

// @public (undocumented)
export function inject(token: HostAttributeToken): string;

// @public (undocumented)
export function inject(token: HostAttributeToken, options: {
optional: true;
}): string | null;

// @public (undocumented)
export function inject(token: HostAttributeToken, options: {
optional: false;
}): string;

// @public
export interface Injectable {
providedIn?: Type<any> | 'root' | 'platform' | 'any' | null;
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/di/host_attribute_token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {ɵɵinjectAttribute} from '../render3/instructions/di_attr';

/**
* Creates a token that can be used to inject static attributes of the host node.
*
* @usageNotes
* ### Injecting an attribute that is known to exist
* ```typescript
* @Directive()
* class MyDir {
* attr: string = inject(new HostAttributeToken('some-attr'));
* }
* ```
*
* ### Optionally injecting an attribute
* ```typescript
* @Directive()
* class MyDir {
* attr: string | null = inject(new HostAttributeToken('some-attr'), {optional: true});
* }
* ```
* @publicApi
*/
export class HostAttributeToken {
constructor(private attributeName: string) {}

/** @internal */
__NG_ELEMENT_ID__ = () => ɵɵinjectAttribute(this.attributeName);

toString(): string {
return `HostAttributeToken ${this.attributeName}`;
}
}
1 change: 1 addition & 0 deletions packages/core/src/di/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export {InjectOptions} from './interface/injector';
export {INJECTOR} from './injector_token';
export {ClassProvider, ModuleWithProviders, ClassSansProvider, ImportedNgModuleProviders, ConstructorProvider, EnvironmentProviders, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, Provider, StaticClassProvider, StaticClassSansProvider, StaticProvider, TypeProvider, ValueProvider, ValueSansProvider} from './interface/provider';
export {InjectionToken} from './injection_token';
export {HostAttributeToken} from './host_attribute_token';
42 changes: 38 additions & 4 deletions packages/core/src/di/injector_compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {getInjectImplementation, injectRootLimpMode} from './inject_switch';
import type {Injector} from './injector';
import {DecoratorFlags, InjectFlags, InjectOptions, InternalInjectFlags} from './interface/injector';
import {ProviderToken} from './provider_token';
import type {HostAttributeToken} from './host_attribute_token';


const _THROW_IF_NOT_FOUND = {};
Expand Down Expand Up @@ -85,8 +86,14 @@ export function injectInjectorOnly<T>(token: ProviderToken<T>, flags = InjectFla
*/
export function ɵɵinject<T>(token: ProviderToken<T>): T;
export function ɵɵinject<T>(token: ProviderToken<T>, flags?: InjectFlags): T|null;
export function ɵɵinject<T>(token: ProviderToken<T>, flags = InjectFlags.Default): T|null {
return (getInjectImplementation() || injectInjectorOnly)(resolveForwardRef(token), flags);
export function ɵɵinject(token: HostAttributeToken): string;
export function ɵɵinject(token: HostAttributeToken, flags?: InjectFlags): string|null;
export function ɵɵinject<T>(
token: ProviderToken<T>|HostAttributeToken, flags?: InjectFlags): string|null;
export function ɵɵinject<T>(
token: ProviderToken<T>|HostAttributeToken, flags = InjectFlags.Default): T|null {
return (getInjectImplementation() || injectInjectorOnly)(
resolveForwardRef(token as Type<T>), flags);
}

/**
Expand Down Expand Up @@ -153,6 +160,30 @@ export function inject<T>(token: ProviderToken<T>, options: InjectOptions&{optio
* @publicApi
*/
export function inject<T>(token: ProviderToken<T>, options: InjectOptions): T|null;
/**
* @param token A token that represents a static attribute on the host node that should be injected.
* @returns Value of the attribute if it exists.
* @throws If called outside of a supported context or the attribute does not exist.
*
* @publicApi
*/
export function inject(token: HostAttributeToken): string;
/**
* @param token A token that represents a static attribute on the host node that should be injected.
* @returns Value of the attribute if it exists, otherwise `null`.
* @throws If called outside of a supported context.
*
* @publicApi
*/
export function inject(token: HostAttributeToken, options: {optional: true}): string|null;
/**
* @param token A token that represents a static attribute on the host node that should be injected.
* @returns Value of the attribute if it exists.
* @throws If called outside of a supported context or the attribute does not exist.
*
* @publicApi
*/
export function inject(token: HostAttributeToken, options: {optional: false}): string;
/**
* Injects a token from the currently active injector.
* `inject` is only supported in an [injection context](/guide/dependency-injection-context). It can
Expand Down Expand Up @@ -219,8 +250,11 @@ export function inject<T>(token: ProviderToken<T>, options: InjectOptions): T|nu
* @publicApi
*/
export function inject<T>(
token: ProviderToken<T>, flags: InjectFlags|InjectOptions = InjectFlags.Default): T|null {
return ɵɵinject(token, convertToBitFlags(flags));
token: ProviderToken<T>|HostAttributeToken,
flags: InjectFlags|InjectOptions = InjectFlags.Default) {
// The `as any` here _shouldn't_ be necessary, but without it JSCompiler
// throws a disambiguation error due to the multiple signatures.
return ɵɵinject(token as any, convertToBitFlags(flags));
}

// Converts object-based DI flags (`InjectOptions`) to bit flags (`InjectFlags`).
Expand Down

0 comments on commit 331b16e

Please sign in to comment.