import { IExpressionParser } from '@aurelia/expression-parser'; import { Protocol, camelCase, emptyArray, firstDefined, getResourceKeyFor, mergeArrays, resolve, resource, resourceBaseName, isString } from '@aurelia/kernel'; import { defineMetadata, getMetadata } from './utilities-metadata'; import { IAttrMapper } from './attribute-mapper'; import { itAttributeBinding, itIteratorBinding, itListenerBinding, itMultiAttr, itPropertyBinding, itRefBinding, itSpreadValueBinding, type MultiAttrInstruction, type PropertyBindingInstruction, type IInstruction, IteratorBindingInstruction, ListenerBindingInstruction, AttributeBindingInstruction, RefBindingInstruction, SpreadValueBindingInstruction, } from './instructions'; import { aliasRegistration, etIsFunction, etIsProperty, tcObjectFreeze, singletonRegistration } from './utilities'; import type { Constructable, IContainer, IServiceLocator, PartialResourceDefinition, ResourceDefinition, ResourceType, StaticResourceType, } from '@aurelia/kernel'; import { AttrSyntax, IAttributeParser } from './attribute-pattern'; import { ErrorNames, createMappedError } from './errors'; import { IAttributeComponentDefinition, IElementComponentDefinition, IComponentBindablePropDefinition } from './interfaces-template-compiler'; import { BindingMode, InternalBindingMode } from './binding-mode'; export type PartialBindingCommandDefinition = PartialResourceDefinition; export type BindingCommandStaticAuDefinition = PartialBindingCommandDefinition & { type: 'binding-command'; }; export interface IPlainAttrCommandInfo { readonly node: Element; readonly attr: AttrSyntax; readonly bindable: null; readonly def: null; } export interface IBindableCommandInfo { readonly node: Element; readonly attr: AttrSyntax; readonly bindable: IComponentBindablePropDefinition; readonly def: IAttributeComponentDefinition | IElementComponentDefinition; } export type ICommandBuildInfo = IPlainAttrCommandInfo | IBindableCommandInfo; export type BindingCommandInstance = { /** * Characteristics of a binding command. * - `false`: The normal process (check custom attribute -> check bindable -> command.build()) should take place. * - `true`: The binding command wants to take over the processing of an attribute. The template compiler keeps the attribute as is in compilation, instead of executing the normal process. */ ignoreAttr: boolean; build(info: ICommandBuildInfo, parser: IExpressionParser, mapper: IAttrMapper): IInstruction; } & T; export type BindingCommandType = ResourceType; export type BindingCommandKind = { readonly name: string; keyFrom(name: string): string; // isType(value: T): value is (T extends Constructable ? BindingCommandType : never); define(name: string, Type: T): BindingCommandType; define(def: PartialBindingCommandDefinition, Type: T): BindingCommandType; define(nameOrDef: string | PartialBindingCommandDefinition, Type: T): BindingCommandType; getAnnotation(Type: Constructable, prop: K): PartialBindingCommandDefinition[K] | undefined; find(container: IContainer, name: string): BindingCommandDefinition | null; get(container: IServiceLocator, name: string): BindingCommandInstance; }; export type BindingCommandDecorator = (Type: T, context: ClassDecoratorContext) => BindingCommandType; /** * Decorator to describe a class as a binding command resource */ export function bindingCommand(name: string): BindingCommandDecorator; export function bindingCommand(definition: PartialBindingCommandDefinition): BindingCommandDecorator; export function bindingCommand(nameOrDefinition: string | PartialBindingCommandDefinition): BindingCommandDecorator { return function (target: T, context: ClassDecoratorContext): BindingCommandType { context.addInitializer(function (this) { BindingCommand.define(nameOrDefinition, target); }); return target as BindingCommandType; }; } export class BindingCommandDefinition implements ResourceDefinition { private constructor( public readonly Type: BindingCommandType, public readonly name: string, public readonly aliases: readonly string[], public readonly key: string, ) {} public static create( nameOrDef: string | PartialBindingCommandDefinition, Type: BindingCommandType, ): BindingCommandDefinition { let name: string; let def: PartialBindingCommandDefinition; if (isString(nameOrDef)) { name = nameOrDef; def = { name }; } else { name = nameOrDef.name; def = nameOrDef; } return new BindingCommandDefinition( Type, firstDefined(getCommandAnnotation(Type, 'name'), name), mergeArrays(getCommandAnnotation(Type, 'aliases'), def.aliases, Type.aliases), getCommandKeyFrom(name), ); } public register(container: IContainer, aliasName?: string | undefined): void { const $Type = this.Type; const key = typeof aliasName === 'string' ? getCommandKeyFrom(aliasName) : this.key; const aliases = this.aliases; if (!container.has(key, false)) { container.register( container.has($Type, false) ? null : singletonRegistration($Type, $Type), aliasRegistration($Type, key), ...aliases.map(alias => aliasRegistration($Type, getCommandKeyFrom(alias))), ); } /* istanbul ignore next */ else if (__DEV__) { // eslint-disable-next-line no-console console.warn(`[DEV:aurelia] ${createMappedError(ErrorNames.binding_command_existed, this.name)}`); } } } const bindingCommandTypeName = 'binding-command'; const cmdBaseName = /*@__PURE__*/getResourceKeyFor(bindingCommandTypeName); const getCommandKeyFrom = (name: string): string => `${cmdBaseName}:${name}`; const getCommandAnnotation = ( Type: Constructable, prop: K, ): PartialBindingCommandDefinition[K] | undefined => getMetadata(Protocol.annotation.keyFor(prop), Type); export const BindingCommand = /*@__PURE__*/ (() => { const staticResourceDefinitionMetadataKey = '__au_static_resource__'; const getDefinitionFromStaticAu = ( // eslint-disable-next-line @typescript-eslint/ban-types Type: C | Function, typeName: string, createDef: (au: PartialResourceDefinition, Type: C) => Def, ): Def => { let def = getMetadata(staticResourceDefinitionMetadataKey, Type) as Def; if (def == null) { if ((Type as StaticResourceType).$au?.type === typeName) { def = createDef((Type as StaticResourceType).$au!, Type as C); defineMetadata(def, Type, staticResourceDefinitionMetadataKey); } } return def; }; return tcObjectFreeze({ name: cmdBaseName, keyFrom: getCommandKeyFrom, // isType(value: T): value is (T extends Constructable ? BindingCommandType : never) { // return isFunction(value) && hasOwnMetadata(cmdBaseName, value); // }, define>(nameOrDef: string | PartialBindingCommandDefinition, Type: T): T & BindingCommandType { const definition = BindingCommandDefinition.create(nameOrDef, Type as Constructable); const $Type = definition.Type as BindingCommandType; // registration of resource name is a requirement for the resource system in kernel (module-loader) defineMetadata(definition, $Type, cmdBaseName, resourceBaseName); return $Type; }, getAnnotation: getCommandAnnotation, find(container, name) { const Type = container.find(bindingCommandTypeName, name); return Type == null ? null : getMetadata(cmdBaseName, Type) ?? getDefinitionFromStaticAu(Type, bindingCommandTypeName, BindingCommandDefinition.create) ?? null; }, get(container, name) { if (__DEV__) { try { return container.get(resource(getCommandKeyFrom(name))); } catch (ex) { // eslint-disable-next-line no-console console.log(`\n\n\n[DEV:aurelia] Cannot retrieve binding command with name\n\n\n\n\n`, name); throw ex; } } return container.get(resource(getCommandKeyFrom(name))); }, }); })(); export class OneTimeBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'one-time', }; public get ignoreAttr() { return false; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction { const attr = info.attr; let target = attr.target; let value = info.attr.rawValue; value = value === '' ? camelCase(target) : value; if (info.bindable == null) { target = attrMapper.map(info.node, target) // if the mapper doesn't know how to map it // use the default behavior, which is camel-casing ?? camelCase(target); } else { target = info.bindable.name; } return { type: itPropertyBinding, from: exprParser.parse(value, etIsProperty), to: target, mode: InternalBindingMode.oneTime }; } } export class ToViewBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'to-view', }; public get ignoreAttr() { return false; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction { const attr = info.attr; let target = attr.target; let value = info.attr.rawValue; value = value === '' ? camelCase(target) : value; if (info.bindable == null) { target = attrMapper.map(info.node, target) // if the mapper doesn't know how to map it // use the default behavior, which is camel-casing ?? camelCase(target); } else { target = info.bindable.name; } return { type: itPropertyBinding, from: exprParser.parse(value, etIsProperty), to: target, mode: InternalBindingMode.toView }; } } export class FromViewBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'from-view', }; public get ignoreAttr() { return false; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction { const attr = info.attr; let target = attr.target; let value = attr.rawValue; value = value === '' ? camelCase(target) : value; if (info.bindable == null) { target = attrMapper.map(info.node, target) // if the mapper doesn't know how to map it // use the default behavior, which is camel-casing ?? camelCase(target); } else { target = info.bindable.name; } return { type: itPropertyBinding, from: exprParser.parse(value, etIsProperty), to: target, mode: InternalBindingMode.fromView }; } } export class TwoWayBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'two-way', }; public get ignoreAttr() { return false; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction { const attr = info.attr; let target = attr.target; let value = attr.rawValue; value = value === '' ? camelCase(target) : value; if (info.bindable == null) { target = attrMapper.map(info.node, target) // if the mapper doesn't know how to map it // use the default behavior, which is camel-casing ?? camelCase(target); } else { target = info.bindable.name; } return { type: itPropertyBinding, from: exprParser.parse(value, etIsProperty), to: target, mode: InternalBindingMode.twoWay }; } } export class DefaultBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'bind', }; public get ignoreAttr() { return false; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction { const attr = info.attr; const bindable = info.bindable; let value = attr.rawValue; let target = attr.target; let mode: string | number; value = value === '' ? camelCase(target) : value; if (bindable == null) { mode = attrMapper.isTwoWay(info.node, target) ? InternalBindingMode.twoWay : InternalBindingMode.toView; target = attrMapper.map(info.node, target) // if the mapper doesn't know how to map it // use the default behavior, which is camel-casing ?? camelCase(target); } else { mode = bindable.mode === 0 || bindable.mode == null ? InternalBindingMode.toView : bindable.mode; target = bindable.name; } return { type: itPropertyBinding, from: exprParser.parse(value, etIsProperty), to: target, mode: isString(mode) ? BindingMode[mode as keyof typeof BindingMode] ?? InternalBindingMode.default : mode as BindingMode }; } } export class ForBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'for', }; public get ignoreAttr() { return false; } /** @internal */ private readonly _attrParser = resolve(IAttributeParser); public build(info: ICommandBuildInfo, exprParser: IExpressionParser): IteratorBindingInstruction { const target = info.bindable === null ? camelCase(info.attr.target) : info.bindable.name; const forOf = exprParser.parse(info.attr.rawValue, 'IsIterator'); let props: MultiAttrInstruction[] = emptyArray; // Parse additional iterator properties after the for-of expression. // This supports multiple semicolon-separated properties like: // repeat.for="item of items; key: id; previous.bind: true" // Previously, only a single property was supported after the semicolon. // This enhancement allows combining multiple iterator options (key, previous, etc.) if (forOf.semiIdx > -1) { const attrsString = info.attr.rawValue.slice(forOf.semiIdx + 1); const attrParts = attrsString.split(';'); const parsedProps: MultiAttrInstruction[] = []; for (let j = 0, jj = attrParts.length; j < jj; j++) { const attrPart = attrParts[j]; const colonIdx = attrPart.indexOf(':'); if (colonIdx > -1) { const attrName = attrPart.slice(0, colonIdx).trim(); const attrValue = attrPart.slice(colonIdx + 1).trim(); const attrSyntax = this._attrParser.parse(attrName, attrValue); parsedProps.push({ type: itMultiAttr, value: attrValue, to: attrSyntax.target, command: attrSyntax.command }); } } props = parsedProps; } return { type: itIteratorBinding, forOf, to: target, props }; } } export class TriggerBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'trigger', }; public get ignoreAttr() { return true; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser): ListenerBindingInstruction { return { type: itListenerBinding, from: exprParser.parse(info.attr.rawValue, etIsFunction), to: info.attr.target, capture: false, modifier: info.attr.parts?.[2] ?? null }; } } export class CaptureBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'capture', }; public get ignoreAttr() { return true; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser): ListenerBindingInstruction { return { type: itListenerBinding, from: exprParser.parse(info.attr.rawValue, etIsFunction), to: info.attr.target, capture: true, modifier: info.attr.parts?.[2] ?? null }; } } /** * Attr binding command. Compile attr with binding symbol with command `attr` to `AttributeBindingInstruction` */ export class AttrBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'attr', }; public get ignoreAttr() { return true; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser): AttributeBindingInstruction { const attr = info.attr; const target = attr.target; let value = attr.rawValue; value = value === '' ? camelCase(target) : value; return { type: itAttributeBinding, attr: target, from: exprParser.parse(value, etIsProperty), to: target }; } } /** * Style binding command. Compile attr with binding symbol with command `style` to `AttributeBindingInstruction` */ export class StyleBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'style', }; public get ignoreAttr() { return true; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser): AttributeBindingInstruction { return { type: itAttributeBinding, attr: 'style', from: exprParser.parse(info.attr.rawValue, etIsProperty), to: info.attr.target }; } } /** * Class binding command. Compile attr with binding symbol with command `class` to `AttributeBindingInstruction` */ export class ClassBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'class', }; public get ignoreAttr() { return true; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser): AttributeBindingInstruction { let target = info.attr.target; if (target.includes(",")) { const classes = target .split(",") .filter(c => c.length > 0); if (classes.length === 0) { throw createMappedError(ErrorNames.compiler_invalid_class_binding_syntax); } target = classes.join(' '); } return { type: itAttributeBinding, attr: 'class', from: exprParser.parse(info.attr.rawValue, etIsProperty), to: target }; } } /** * Binding command to refer different targets (element, custom element/attribute view models, controller) attached to an element */ export class RefBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'ref', }; public get ignoreAttr() { return true; } public build(info: ICommandBuildInfo, exprParser: IExpressionParser): RefBindingInstruction { return { type: itRefBinding, from: exprParser.parse(info.attr.rawValue, etIsProperty), to: info.attr.target }; } } export class SpreadValueBindingCommand implements BindingCommandInstance { public static readonly $au: BindingCommandStaticAuDefinition = { type: bindingCommandTypeName, name: 'spread', }; public get ignoreAttr() { return false; } public build(info: ICommandBuildInfo): SpreadValueBindingInstruction { return { type: itSpreadValueBinding, target: info.attr.target as '$bindables' | '$element', from: info.attr.rawValue }; } }