import { mergeArrays, firstDefined, Key, resourceBaseName, getResourceKeyFor, isFunction, isString } from '@aurelia/kernel'; import { Bindable } from '../bindable'; import { Watch } from '../watch'; import { getEffectiveParentNode } from '../dom'; import { INode, refs } from '../dom.node'; import { defineMetadata, getAnnotationKeyFor, getMetadata, hasMetadata } from '../utilities-metadata'; import { objectFreeze } from '../utilities'; import { aliasRegistration, singletonRegistration } from '../utilities-di'; import type { Constructable, IContainer, ResourceDefinition, PartialResourceDefinition, ResourceType, StaticResourceType, } from '@aurelia/kernel'; import type { BindableDefinition, PartialBindableDefinition } from '../bindable'; import type { ICustomAttributeViewModel, ICustomAttributeController, Controller } from '../templating/controller'; import type { IWatchDefinition } from '../watch'; import { ErrorNames, createMappedError } from '../errors'; import { dtAttribute, getDefinitionFromStaticAu, type IResourceKind } from './resources-shared'; import { IAttributeComponentDefinition } from '@aurelia/template-compiler'; export type PartialCustomAttributeDefinition = PartialResourceDefinition & { readonly isTemplateController?: boolean; readonly bindables?: (Record>) | (TBindables | PartialBindableDefinition & { name: TBindables })[]; /** * A config that can be used by template compliler to change attr value parsing mode * `true` to always parse as a single value, mostly will be string in URL scenario * Example: * ```html *
* ``` * With `noMultiBinding: true`, user does not need to escape the `:` with `\` * or use binding command to escape it. * * With `noMultiBinding: false (default)`, the above will be parsed as it's binding * to a property name `http`, with value equal to literal string `//bla.bla.com` */ readonly noMultiBindings?: boolean; readonly watches?: IWatchDefinition[]; readonly dependencies?: readonly Key[]; /** * **Only used by template controller custom attributes.** * * Container strategy for the view factory of this template controller. * * By default, the view factory will be reusing the container of the parent view (controller), * as this container has information about the resources registered. * * Specify `'new'` to create a new container for the view factory. */ readonly containerStrategy?: 'reuse' | 'new'; }>; export type CustomAttributeStaticAuDefinition = PartialCustomAttributeDefinition & { type: 'custom-attribute'; }; export type CustomAttributeType = ResourceType; export type CustomAttributeKind = IResourceKind & { for(node: Node, name: string): ICustomAttributeController | undefined; closest ? Constructable : Constructable = A extends Constructable ? Constructable : Constructable>(node: Node, Type: CustomAttributeType): ICustomAttributeController> | null; closest ? Constructable : Constructable = A extends Constructable ? Constructable : Constructable>(node: Node, name: string): ICustomAttributeController> | null; isType(value: T): value is (T extends Constructable ? CustomAttributeType : never); define(name: string, Type: T): CustomAttributeType; define(def: PartialCustomAttributeDefinition, Type: T): CustomAttributeType; define(nameOrDef: string | PartialCustomAttributeDefinition, Type: T): CustomAttributeType; getDefinition(Type: T, context?: DecoratorContext | null): CustomAttributeDefinition; // eslint-disable-next-line getDefinition(Type: Function, context?: DecoratorContext | null): CustomAttributeDefinition; annotate(Type: Constructable, prop: K, value: PartialCustomAttributeDefinition[K]): void; getAnnotation(Type: Constructable, prop: K, context: DecoratorContext | undefined | null): PartialCustomAttributeDefinition[K] | undefined; find(c: IContainer, name: string): CustomAttributeDefinition | null; }; export type CustomAttributeDecorator = (Type: T, context: ClassDecoratorContext) => CustomAttributeType; /** * Decorator: Indicates that the decorated class is a custom attribute. */ export function customAttribute(definition: PartialCustomAttributeDefinition): CustomAttributeDecorator; export function customAttribute(name: string): CustomAttributeDecorator; export function customAttribute(nameOrDef: string | PartialCustomAttributeDefinition): CustomAttributeDecorator; export function customAttribute(nameOrDef: string | PartialCustomAttributeDefinition): CustomAttributeDecorator { return function (target: T, context: ClassDecoratorContext): CustomAttributeType { context.addInitializer(function (this) { defineAttribute(nameOrDef, this as Constructable); }); return target as CustomAttributeType; }; } /** * Decorator: Applied to custom attributes. Indicates that whatever element the * attribute is placed on should be converted into a template and that this * attribute controls the instantiation of the template. */ export function templateController(definition: Omit): CustomAttributeDecorator; export function templateController(name: string): CustomAttributeDecorator; export function templateController(nameOrDef: string | Omit): CustomAttributeDecorator; export function templateController(nameOrDef: string | Omit): CustomAttributeDecorator { return function (target, context) { context.addInitializer(function (this) { defineAttribute( isString(nameOrDef) ? { isTemplateController: true, name: nameOrDef } : { isTemplateController: true, ...nameOrDef }, this as Constructable, ); }); return target; } as CustomAttributeDecorator; } export class CustomAttributeDefinition implements ResourceDefinition { // a simple marker to distinguish between Custom Element definition & Custom attribute definition public get type(): 'custom-attribute' { return dtAttribute; } private constructor( public readonly Type: CustomAttributeType, public readonly name: string, public readonly aliases: readonly string[], public readonly key: string, public readonly isTemplateController: boolean, public readonly bindables: Record, public readonly noMultiBindings: boolean, public readonly watches: IWatchDefinition[], public readonly dependencies: Key[], public readonly containerStrategy: 'reuse' | 'new', public readonly defaultProperty: string, ) {} public static create( nameOrDef: string | PartialCustomAttributeDefinition, Type: CustomAttributeType, ): CustomAttributeDefinition { let name: string; let def: PartialCustomAttributeDefinition; if (isString(nameOrDef)) { name = nameOrDef; def = { name }; } else { name = nameOrDef.name; def = nameOrDef; } for(const bindable of Object.values(Bindable.from(def.bindables))) { Bindable._add(bindable, Type); } return new CustomAttributeDefinition( Type, firstDefined(getAttributeAnnotation(Type, 'name'), name), mergeArrays(getAttributeAnnotation(Type, 'aliases'), def.aliases, Type.aliases), getAttributeKeyFrom(name), firstDefined(getAttributeAnnotation(Type, 'isTemplateController'), def.isTemplateController, Type.isTemplateController, false), Bindable.from(...Bindable.getAll(Type), getAttributeAnnotation(Type, 'bindables'), Type.bindables, def.bindables), firstDefined(getAttributeAnnotation(Type, 'noMultiBindings'), def.noMultiBindings, Type.noMultiBindings, false), mergeArrays(Watch.getDefinitions(Type), Type.watches), mergeArrays(getAttributeAnnotation(Type, 'dependencies'), def.dependencies, Type.dependencies), firstDefined(getAttributeAnnotation(Type, 'containerStrategy'), def.containerStrategy, Type.containerStrategy, 'reuse'), firstDefined(getAttributeAnnotation(Type, 'defaultProperty'), def.defaultProperty, Type.defaultProperty, 'value'), ); } public register(container: IContainer, aliasName?: string | undefined): void { const $Type = this.Type; const key = typeof aliasName === 'string' ? getAttributeKeyFrom(aliasName) : this.key; const aliases = this.aliases; if (container.has(key, false)) { // eslint-disable-next-line no-console console.warn(createMappedError(ErrorNames.attribute_existed, this.name)); } container.register( singletonRegistration(key, $Type), ...aliases.map(alias => aliasRegistration(key, getAttributeKeyFrom(alias))) ); } public toString() { return `au:ca:${this.name}`; } } /** @internal */ export const attrTypeName = 'custom-attribute'; const attributeBaseName = /*@__PURE__*/getResourceKeyFor(attrTypeName); const getAttributeKeyFrom = (name: string): string => `${attributeBaseName}:${name}`; const getAttributeAnnotation = ( Type: Constructable, prop: K, ): PartialCustomAttributeDefinition[K] | undefined => getMetadata(getAnnotationKeyFor(prop), Type); /** @internal */ export const isAttributeType = (value: T): value is (T extends Constructable ? CustomAttributeType : never) => { return isFunction(value) && ( hasMetadata(attributeBaseName, value) || (value as StaticResourceType).$au?.type === attrTypeName ); }; /** @internal */ export const findAttributeControllerFor = (node: Node, name: string): ICustomAttributeController | undefined => { return (refs.get(node, getAttributeKeyFrom(name)) ?? void 0) as ICustomAttributeController | undefined; }; /** @internal */ export const defineAttribute = (nameOrDef: string | PartialCustomAttributeDefinition, Type: T): CustomAttributeType => { const definition = CustomAttributeDefinition.create(nameOrDef, Type as Constructable); const $Type = definition.Type as CustomAttributeType; defineMetadata(definition, $Type, attributeBaseName, resourceBaseName); return $Type; }; /** @internal */ // eslint-disable-next-line @typescript-eslint/ban-types export const getAttributeDefinition = (Type: T | Function): CustomAttributeDefinition => { const def: CustomAttributeDefinition = getMetadata>(attributeBaseName, Type) ?? getDefinitionFromStaticAu(Type as CustomAttributeType, attrTypeName, CustomAttributeDefinition.create); if (def === void 0) { throw createMappedError(ErrorNames.attribute_def_not_found, Type); } return def; }; const findClosestControllerByName = (node: Node, attrNameOrType: string | CustomAttributeType): ICustomAttributeController | null => { let key = ''; let attrName = ''; if (isString(attrNameOrType)) { key = getAttributeKeyFrom(attrNameOrType); attrName = attrNameOrType; } else { const definition = getAttributeDefinition(attrNameOrType); key = definition.key; attrName = definition.name; } let cur = node as INode | null; while (cur !== null) { const controller = refs.get(cur, key) as Controller | null; if (controller?.is(attrName)) { return controller as ICustomAttributeController; } cur = getEffectiveParentNode(cur); } return null; }; export const CustomAttribute = /*@__PURE__*/ objectFreeze({ name: attributeBaseName, keyFrom: getAttributeKeyFrom, isType: isAttributeType, for: findAttributeControllerFor, closest: findClosestControllerByName, define: defineAttribute, getDefinition: getAttributeDefinition, annotate(Type: Constructable, prop: K, value: PartialCustomAttributeDefinition[K]): void { defineMetadata(value, Type, getAnnotationKeyFor(prop)); }, getAnnotation: getAttributeAnnotation, find(c, name) { const Type = c.find(attrTypeName, name); return Type === null ? null : getMetadata(attributeBaseName, Type) ?? getDefinitionFromStaticAu(Type, attrTypeName, CustomAttributeDefinition.create) ?? null; }, });