diff --git a/@types/angular.d.ts b/@types/angular.d.ts index ae4b3f376..e98439172 100644 --- a/@types/angular.d.ts +++ b/@types/angular.d.ts @@ -1,4 +1,4 @@ -export class Angular { +export class Angular extends EventTarget { /** @private @type {!Array} */ private _bootsrappedModules; /** @public @type {ng.PubSubService} */ diff --git a/@types/core/parse/ast/ast-node.d.ts b/@types/core/parse/ast/ast-node.d.ts index 22ed9e9ed..69b1b6ee6 100644 --- a/@types/core/parse/ast/ast-node.d.ts +++ b/@types/core/parse/ast/ast-node.d.ts @@ -53,4 +53,6 @@ export type ASTNode = { filter?: boolean; /** Indicates in node depends on non-shallow state of objects */ isPure?: boolean; + /** Indicates the expression is a contant */ + constant?: boolean; }; diff --git a/@types/directive/bind/bind.d.ts b/@types/directive/bind/bind.d.ts index e0dccc63d..c05a078d3 100644 --- a/@types/directive/bind/bind.d.ts +++ b/@types/directive/bind/bind.d.ts @@ -3,16 +3,14 @@ */ export function ngBindDirective(): ng.Directive; /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ -export function ngBindTemplateDirective(): import("../../interface.ts").Directive; +export function ngBindTemplateDirective(): ng.Directive; /** - * @param {import('../../core/parse/interface.ts').ParseService} $parse - * @returns {import('../../interface.ts').Directive} + * @param {ng.ParseService} $parse + * @returns {ng.Directive} */ -export function ngBindHtmlDirective( - $parse: import("../../core/parse/interface.ts").ParseService, -): import("../../interface.ts").Directive; +export function ngBindHtmlDirective($parse: ng.ParseService): ng.Directive; export namespace ngBindHtmlDirective { let $inject: string[]; } diff --git a/@types/directive/form/form.d.ts b/@types/directive/form/form.d.ts index aff21c3c2..e65fcd9df 100644 --- a/@types/directive/form/form.d.ts +++ b/@types/directive/form/form.d.ts @@ -155,7 +155,7 @@ export class FormController { * in the shallow copy. That means you should get a fresh copy from `$getControls()` every time * you need access to the controls. */ - $getControls(): any; + $getControls(): any[]; $$renameControl(control: any, newName: any): void; /** * Deregister a control from the form. diff --git a/@types/directive/init/init.d.ts b/@types/directive/init/init.d.ts index 06a07af88..aa440fb99 100644 --- a/@types/directive/init/init.d.ts +++ b/@types/directive/init/init.d.ts @@ -1,4 +1,4 @@ /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ -export function ngInitDirective(): import("../../interface.ts").Directive; +export function ngInitDirective(): ng.Directive; diff --git a/@types/directive/listener/listener.d.ts b/@types/directive/listener/listener.d.ts new file mode 100644 index 000000000..abeef0087 --- /dev/null +++ b/@types/directive/listener/listener.d.ts @@ -0,0 +1,4 @@ +/** + * @returns {ng.Directive} + */ +export function ngListenerDirective(): ng.Directive; diff --git a/@types/directive/non-bindable/non-bindable.d.ts b/@types/directive/non-bindable/non-bindable.d.ts index 68c40fb3b..41103dc2b 100644 --- a/@types/directive/non-bindable/non-bindable.d.ts +++ b/@types/directive/non-bindable/non-bindable.d.ts @@ -1,4 +1,4 @@ /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ -export function ngNonBindableDirective(): import("../../interface.ts").Directive; +export function ngNonBindableDirective(): ng.Directive; diff --git a/@types/directive/select/select.d.ts b/@types/directive/select/select.d.ts index fe97ed793..411b8420b 100644 --- a/@types/directive/select/select.d.ts +++ b/@types/directive/select/select.d.ts @@ -1,7 +1,7 @@ /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ -export function selectDirective(): import("../../interface.ts").Directive; +export function selectDirective(): ng.Directive; /** * @param {ng.InterpolateService} $interpolate * @returns {ng.Directive} diff --git a/@types/directive/setter/setter.d.ts b/@types/directive/setter/setter.d.ts index eab1a8933..99349f09b 100644 --- a/@types/directive/setter/setter.d.ts +++ b/@types/directive/setter/setter.d.ts @@ -1,12 +1,12 @@ /** * @param {ng.ParseService} $parse * @param {ng.LogService} $log - * @returns {import('interface.ts').Directive} + * @returns {ng.Directive} */ export function ngSetterDirective( $parse: ng.ParseService, $log: ng.LogService, -): any; +): ng.Directive; export namespace ngSetterDirective { let $inject: string[]; } diff --git a/@types/directive/show-hide/show-hide.d.ts b/@types/directive/show-hide/show-hide.d.ts index 6952f709b..3dc874258 100644 --- a/@types/directive/show-hide/show-hide.d.ts +++ b/@types/directive/show-hide/show-hide.d.ts @@ -7,11 +7,9 @@ export namespace ngShowDirective { let $inject: string[]; } /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ -export function ngHideDirective( - $animate: any, -): import("../../interface.ts").Directive; +export function ngHideDirective($animate: any): ng.Directive; export namespace ngHideDirective { let $inject_1: string[]; export { $inject_1 as $inject }; diff --git a/@types/directive/switch/switch.d.ts b/@types/directive/switch/switch.d.ts index 6cf572479..944e16311 100644 --- a/@types/directive/switch/switch.d.ts +++ b/@types/directive/switch/switch.d.ts @@ -7,10 +7,10 @@ export namespace ngSwitchDirective { let $inject: string[]; } /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ -export function ngSwitchWhenDirective(): import("../../interface.ts").Directive; +export function ngSwitchWhenDirective(): ng.Directive; /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ -export function ngSwitchDefaultDirective(): import("../../interface.ts").Directive; +export function ngSwitchDefaultDirective(): ng.Directive; diff --git a/@types/interface.d.ts b/@types/interface.d.ts index d2eb21a8a..3c5da2fde 100644 --- a/@types/interface.d.ts +++ b/@types/interface.d.ts @@ -376,7 +376,7 @@ export interface DirectivePrePost { * A link function executed during directive linking. */ export type DirectiveLinkFn = ( - scope: Scope, + scope: ng.Scope, element: HTMLElement, attrs: ng.Attributes, controller?: TController, diff --git a/@types/namespace.d.ts b/@types/namespace.d.ts index c73db6d19..812a18b46 100644 --- a/@types/namespace.d.ts +++ b/@types/namespace.d.ts @@ -179,7 +179,7 @@ declare global { type Listener = TListener; type DocumentService = Document; type WindowService = Window; - type AngularServie = Angular; + type AngularService = Angular; type WorkerConfig = TWorkerConfig; type WorkerConnection = TWorkerConnection; type Injectable< diff --git a/@types/router/scroll/interface.d.ts b/@types/router/scroll/interface.d.ts index eb23a6cbd..7ce93ce6a 100644 --- a/@types/router/scroll/interface.d.ts +++ b/@types/router/scroll/interface.d.ts @@ -1,3 +1,3 @@ export type ViewScrollService = | ng.AnchorScrollService - | ((el: Element) => void | Promise); + | ((element: Element) => void); diff --git a/@types/services/http/http.d.ts b/@types/services/http/http.d.ts index bd0f719b2..f37ab3d57 100644 --- a/@types/services/http/http.d.ts +++ b/@types/services/http/http.d.ts @@ -30,9 +30,15 @@ export class HttpProvider { common: { Accept: string; }; - post: any; - put: any; - patch: any; + post: { + "Content-Type": string; + }; + put: { + "Content-Type": string; + }; + patch: { + "Content-Type": string; + }; }; xsrfCookieName: string; xsrfHeaderName: string; @@ -125,9 +131,15 @@ export class HttpProvider { common: { Accept: string; }; - post: any; - put: any; - patch: any; + post: { + "Content-Type": string; + }; + put: { + "Content-Type": string; + }; + patch: { + "Content-Type": string; + }; }; xsrfCookieName: string; xsrfHeaderName: string; diff --git a/@types/shared/noderef.d.ts b/@types/shared/noderef.d.ts index af99ed99d..e6c29df25 100644 --- a/@types/shared/noderef.d.ts +++ b/@types/shared/noderef.d.ts @@ -9,11 +9,11 @@ export class NodeRef { * @throws {Error} If the argument is invalid or cannot be wrapped properly. */ constructor(element: Node | Element | string | NodeList | Node[]); - /** @private @type {Node | ChildNode | null} */ + /** @private @type {Node | ChildNode | undefined} */ private _node; /** @type {Element | undefined} */ _element: Element | undefined; - /** @private @type {Array | undefined} a stable list on nodes */ + /** @private @type {Array} a stable list on nodes */ private _nodes; /** @type {boolean} */ _isList: boolean; diff --git a/@types/shared/utils.d.ts b/@types/shared/utils.d.ts index a5c10c083..b05b27051 100644 --- a/@types/shared/utils.d.ts +++ b/@types/shared/utils.d.ts @@ -90,11 +90,10 @@ export function isObject(value: T): value is T & object; export function isBlankObject(value: any): boolean; /** * Determines if a reference is a `string`. - * - * @param value - The value to check. + * @param {unknown} value - The value to check. * @returns {value is string} True if `value` is a string. */ -export function isString(value: any): value is string; +export function isString(value: unknown): value is string; /** * Determines if a reference is a null. * @@ -211,13 +210,23 @@ export function isArrayBuffer(obj: any): boolean; * @returns {string | *} */ export function trim(value: any): string | any; -export function snakeCase(name: any, separator: any): any; +/** + * @param {string} name + * @param {string} separator + */ +export function snakeCase(name: string, separator: string): string; /** * Set or clear the hashkey for an object. - * @param obj object - * @param hashkey the hashkey (!truthy to delete the hashkey) + * @param {{ [x: string]: any; $$hashKey?: any; }} obj object + * @param {any} hashkey the hashkey (!truthy to delete the hashkey) */ -export function setHashKey(obj: any, hashkey: any): void; +export function setHashKey( + obj: { + [x: string]: any; + $$hashKey?: any; + }, + hashkey: any, +): void; /** * Deeply extends a destination object with one or more source objects. * Safely handles Dates, RegExps, DOM nodes, arrays, and nested objects. @@ -262,7 +271,10 @@ export function isNumberNaN(num: any): boolean; * @returns {Object} */ export function inherit(parent: any, extra: any): any; -export function hasCustomToString(obj: any): boolean; +/** + * @param {{ toString: () => string; }} obj + */ +export function hasCustomToString(obj: { toString: () => string }): boolean; /** * Returns a string appropriate for the type of node. * @@ -272,7 +284,11 @@ export function hasCustomToString(obj: any): boolean; * @returns */ export function getNodeName(element: Element): string; -export function includes(array: any, obj: any): boolean; +/** + * @param {any} array + * @param {string} obj + */ +export function includes(array: any, obj: string): boolean; /** * Removes the first occurrence of a specified value from an array. * @@ -282,7 +298,11 @@ export function includes(array: any, obj: any): boolean; * @returns {number} - The index of the removed value, or -1 if the value was not found. */ export function arrayRemove(array: Array, value: T): number; -export function simpleCompare(val1: any, val2: any): boolean; +/** + * @param {unknown} val1 + * @param {unknown} val2 + */ +export function simpleCompare(val1: unknown, val2: unknown): boolean; /** * Determines if two objects or two values are equivalent. Supports value types, regular * expressions, arrays and objects. @@ -347,14 +367,34 @@ export function equals(o1: any, o2: any): boolean; * @param {string} context the context in which the name is used, such as module or directive */ export function assertNotHasOwnProperty(name: string, context: string): void; -export function stringify(value: any): any; +/** + * @param {unknown} value + * @returns {string | unknown} + */ +export function stringify(value: unknown): string | unknown; /** * @param {Number} maxDepth * @return {boolean} */ export function isValidObjectMaxDepth(maxDepth: number): boolean; -export function concat(array1: any, array2: any, index: any): any; -export function sliceArgs(args: any, startIndex: any): any; +/** + * @param {any[]} array1 + * @param {IArguments | any[] | NodeListOf} array2 + * @param {number | undefined} [index] + */ +export function concat( + array1: any[], + array2: IArguments | any[] | NodeListOf, + index?: number | undefined, +): any[]; +/** + * @param {IArguments | [string, ...any[]]} args + * @param {number} startIndex + */ +export function sliceArgs( + args: IArguments | [string, ...any[]], + startIndex: number, +): any; /** * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for * `fn`). You can supply optional `args` that are prebound to the function. This feature is also @@ -407,22 +447,46 @@ export function toJson( * @returns {Object|Array|string|number} Deserialized JSON string. */ export function fromJson(json: string): any | any[] | string | number; -export function timezoneToOffset(timezone: any, fallback: any): any; -export function addDateMinutes(date: any, minutes: any): Date; +/** + * @param {any} timezone + * @param {number} [fallback] + */ +export function timezoneToOffset(timezone: any, fallback?: number): number; +/** + * @param {Date} date + * @param {number} minutes + */ +export function addDateMinutes(date: Date, minutes: number): Date; +/** + * @param {Date} date + * @param {any} timezone + * @param {undefined} [reverse] + */ export function convertTimezoneToLocal( - date: any, + date: Date, timezone: any, - reverse: any, + reverse?: undefined, ): Date; /** * Parses an escaped url query string into key-value pairs. * @param {string} keyValue - * @returns {Object.} + * @returns {Object.>} */ export function parseKeyValue(keyValue: string): { [x: string]: boolean | any[]; }; -export function toKeyValue(obj: any): string; +/** + * @param {string | { [s: string]: any; } | ArrayLike | null} obj + */ +export function toKeyValue( + obj: + | string + | { + [s: string]: any; + } + | ArrayLike + | null, +): string; /** * Tries to decode the URI component without throwing an exception. * @@ -453,14 +517,27 @@ export function encodeUriSegment(val: string): string; * pct-encoded = "%" HEXDIG HEXDIG * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" + * @param {string | number | boolean} val + * @param {boolean | undefined} [pctEncodeSpaces] */ -export function encodeUriQuery(val: any, pctEncodeSpaces: any): string; +export function encodeUriQuery( + val: string | number | boolean, + pctEncodeSpaces?: boolean | undefined, +): string; /** - * Creates a shallow copy of an object, an array or a primitive. + * Creates a shallow copy of an object, an array, or returns primitives as-is. * - * Assumes that there are no proto properties for objects. + * Assumes there are no proto properties. + * + * @template T + * @param {T} src + * @param {T extends any[] ? T : undefined} [dst] + * @returns {T} */ -export function shallowCopy(src: any, dst: any): any; +export function shallowCopy( + src: T, + dst?: T extends any[] ? T : undefined, +): T; /** * Throw error if the argument is false * @param {boolean} argument @@ -469,13 +546,25 @@ export function shallowCopy(src: any, dst: any): any; export function assert(argument: boolean, errorMsg?: string): void; /** * Throw error if the argument is falsy. + * @param {string | boolean | Object} arg + * @param {string} name + * @param {string | undefined} [reason] + */ +export function assertArg( + arg: string | boolean | any, + name: string, + reason?: string | undefined, +): any; +/** + * @param {string | Function | any[]} arg + * @param {string} name + * @param {boolean | undefined} [acceptArrayAnnotation] */ -export function assertArg(arg: any, name: any, reason: any): any; export function assertArgFn( - arg: any, - name: any, - acceptArrayAnnotation: any, -): any; + arg: string | Function | any[], + name: string, + acceptArrayAnnotation?: boolean | undefined, +): string | Function | any[]; /** * Configure several aspects of error handling if used as a setter or return the * current configuration if used as a getter. diff --git a/src/angular.js b/src/angular.js index 52dfb85f8..7bf1a6b69 100644 --- a/src/angular.js +++ b/src/angular.js @@ -31,8 +31,10 @@ const STRICT_DI = "strict-di"; /** @type {ModuleRegistry} */ const moduleRegistry = {}; -export class Angular { +export class Angular extends EventTarget { constructor() { + super(); + /** @private @type {!Array} */ this._bootsrappedModules = []; diff --git a/src/core/compile/compile.js b/src/core/compile/compile.js index b40ade86b..70c5c9af9 100644 --- a/src/core/compile/compile.js +++ b/src/core/compile/compile.js @@ -1036,7 +1036,7 @@ export class CompileProvider { * @param {Attributes|any} attrs The shared attrs object which is used to populate the normalized attributes. * @param {number=} maxPriority Max directive priority. * @param {string} [ignoreDirective] - * @return {import('../../interface.ts').Directive[]} An array to which the directives are added to. This array is sorted before the function returns. + * @return {ng.Directive[]} An array to which the directives are added to. This array is sorted before the function returns. */ function collectDirectives(node, attrs, maxPriority, ignoreDirective) { /** diff --git a/src/core/parse/ast/ast-node.ts b/src/core/parse/ast/ast-node.ts index 8ac58dbcd..c1d3723fa 100644 --- a/src/core/parse/ast/ast-node.ts +++ b/src/core/parse/ast/ast-node.ts @@ -78,4 +78,7 @@ export type ASTNode = { /** Indicates in node depends on non-shallow state of objects */ isPure?: boolean; + + /** Indicates the expression is a contant */ + constant?: boolean; }; diff --git a/src/core/parse/parser/parser.js b/src/core/parse/parser/parser.js index 1652f275d..f7b421a0b 100644 --- a/src/core/parse/parser/parser.js +++ b/src/core/parse/parser/parser.js @@ -34,7 +34,7 @@ export class Parser { const fn = this._astCompiler.compile(ast); fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); + fn.constant = !!ast.constant; return fn; } @@ -52,16 +52,23 @@ export class Parser { } } +/** + * @param {import("../ast/ast-node.d.ts").ASTNode} ast + * @returns {boolean} + */ function isLiteral(ast) { - return ( - ast.body.length === 0 || - (ast.body.length === 1 && - (ast.body[0].expression.type === ASTType._Literal || - ast.body[0].expression.type === ASTType._ArrayExpression || - ast.body[0].expression.type === ASTType._ObjectExpression)) - ); -} + const { body } = ast; -function isConstant(ast) { - return ast.constant; + if (body && body.length === 1) { + switch (body[0].expression?.type) { + case ASTType._Literal: + case ASTType._ArrayExpression: + case ASTType._ObjectExpression: + return true; + default: + return false; + } + } else { + return false; + } } diff --git a/src/directive/bind/bind.js b/src/directive/bind/bind.js index 7ee1d3b25..049535173 100644 --- a/src/directive/bind/bind.js +++ b/src/directive/bind/bind.js @@ -21,7 +21,9 @@ export function ngBindDirective() { scope.$watch( attr.ngBind, (value) => { - element.textContent = stringify(deProxy(value)); + element.textContent = /** @type {string} */ ( + stringify(deProxy(value)) + ); }, isDefined(attr.lazy), ); @@ -30,17 +32,17 @@ export function ngBindDirective() { } /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ export function ngBindTemplateDirective() { return { /** * @param {ng.Scope} _scope * @param {Element} element - * @param {import('../../core/compile/attributes.js').Attributes} attr + * @param {ng.Attributes} attr */ link(_scope, element, attr) { - attr.$observe("ngBindTemplate", (value) => { + attr.$observe("ngBindTemplate", (/** @type {string | null} */ value) => { element.textContent = isUndefined(value) ? "" : value; }); }, @@ -49,8 +51,8 @@ export function ngBindTemplateDirective() { ngBindHtmlDirective.$inject = [$injectTokens._parse]; /** - * @param {import('../../core/parse/interface.ts').ParseService} $parse - * @returns {import('../../interface.ts').Directive} + * @param {ng.ParseService} $parse + * @returns {ng.Directive} */ export function ngBindHtmlDirective($parse) { return { diff --git a/src/directive/channel/channel.js b/src/directive/channel/channel.js index 8a64c8f7e..025f41588 100644 --- a/src/directive/channel/channel.js +++ b/src/directive/channel/channel.js @@ -1,4 +1,4 @@ -import { isObject } from "../../shared/utils.js"; +import { isObject, isString } from "../../shared/utils.js"; import { $injectTokens } from "../../injection-tokens.js"; ngChannelDirective.$inject = [$injectTokens._eventBus]; @@ -13,15 +13,18 @@ export function ngChannelDirective($eventBus) { const hasTemplateContent = element.childNodes.length > 0; - const unsubscribe = $eventBus.subscribe(channel, (value) => { - if (hasTemplateContent) { - if (isObject(value)) { - scope.$merge(value); + const unsubscribe = $eventBus.subscribe( + channel, + (/** @type {string | Object} */ value) => { + if (hasTemplateContent) { + if (isObject(value)) { + scope.$merge(value); + } + } else if (isString(value)) { + element.innerHTML = value; } - } else { - element.innerHTML = value; - } - }); + }, + ); scope.$on("$destroy", () => unsubscribe()); }, diff --git a/src/directive/init/init.js b/src/directive/init/init.js index c90828bfe..409f0027f 100644 --- a/src/directive/init/init.js +++ b/src/directive/init/init.js @@ -1,7 +1,7 @@ import { getController } from "../../shared/dom.js"; /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ export function ngInitDirective() { return { diff --git a/src/directive/listener/listener.html b/src/directive/listener/listener.html new file mode 100644 index 000000000..8de44de64 --- /dev/null +++ b/src/directive/listener/listener.html @@ -0,0 +1,22 @@ + + + + + AngularTS Test Runner + + + + + + + + + + + +
+ + diff --git a/src/directive/listener/listener.js b/src/directive/listener/listener.js new file mode 100644 index 000000000..e16f86946 --- /dev/null +++ b/src/directive/listener/listener.js @@ -0,0 +1,32 @@ +import { isObject, isString } from "../../shared/utils.js"; + +/** + * @returns {ng.Directive} + */ +export function ngListenerDirective() { + return { + scope: false, + link: (scope, element, attrs) => { + const channel = attrs.ngListener; + + const hasTemplateContent = element.childNodes.length > 0; + + /** @type {EventListener} */ + const fn = (event) => { + const value = /** @type {CustomEvent} */ (event).detail; + + if (hasTemplateContent) { + if (isObject(value)) { + scope.$merge(value); + } + } else if (isString(value)) { + element.innerHTML = value; + } + }; + + element.addEventListener(channel, fn); + + scope.$on("$destroy", () => element.removeEventListener(channel, fn)); + }, + }; +} diff --git a/src/directive/listener/listener.spec.js b/src/directive/listener/listener.spec.js new file mode 100644 index 000000000..db27d9bcc --- /dev/null +++ b/src/directive/listener/listener.spec.js @@ -0,0 +1,108 @@ +import { Angular } from "../../angular.js"; +import { dealoc } from "../../shared/dom.js"; +import { wait } from "../../shared/test-utils.js"; + +describe("ngListener", () => { + let $compile, $scope, element, app; + + beforeEach(async () => { + app = document.getElementById("app"); + dealoc(app); + + const angular = new Angular(); + angular.module("myModule", ["ng"]); + + angular.bootstrap(app, ["myModule"]).invoke((_$compile_, _$rootScope_) => { + $compile = _$compile_; + $scope = _$rootScope_; + }); + + await wait(); + }); + + afterEach(() => { + dealoc(app); + }); + + it("handles CustomEvent dispatched on the element", async () => { + element = $compile(`
`)($scope); + await wait(); + + element.dispatchEvent( + new CustomEvent("update", { + detail: "hello", + bubbles: true, + }), + ); + + await wait(); + + expect(element.innerHTML).toBe("hello"); + }); + + it("merges object detail into scope when template content exists", async () => { + $scope.foo = "initial"; + + element = $compile(` +
+ {{ foo }} +
+ `)($scope); + + await wait(); + + element.dispatchEvent( + new CustomEvent("merge", { + detail: { foo: "updated", bar: 42 }, + bubbles: true, + }), + ); + + await wait(); + + expect($scope.foo).toBe("updated"); + expect($scope.bar).toBe(42); + }); + + it("does nothing when detail is not an object and template content exists", async () => { + $scope.foo = "unchanged"; + + element = $compile(` +
+ {{ foo }} +
+ `)($scope); + + await wait(); + + element.dispatchEvent( + new CustomEvent("noop", { + detail: "ignored", + bubbles: true, + }), + ); + + await wait(); + + expect($scope.foo).toBe("unchanged"); + }); + + it("removes the listener on $destroy", async () => { + element = $compile(`
`)($scope); + await wait(); + + $scope.$destroy(); + await wait(); + + element.dispatchEvent( + new CustomEvent("destroy", { + detail: "should not apply", + bubbles: true, + }), + ); + + await wait(); + + expect(element.innerHTML).toBe(""); + }); +}); diff --git a/src/directive/listener/listener.test.js b/src/directive/listener/listener.test.js new file mode 100644 index 000000000..aba05eb92 --- /dev/null +++ b/src/directive/listener/listener.test.js @@ -0,0 +1,12 @@ +import { test, expect } from "@playwright/test"; + +const TEST_URL = "src/directive/listener/listener.html"; + +test("unit tests contain no errors", async ({ page }) => { + await page.goto(TEST_URL); + await page.content(); + await page.waitForTimeout(1000); + await expect(page.locator(".jasmine-overall-result")).toHaveText( + / 0 failures/, + ); +}); diff --git a/src/directive/non-bindable/non-bindable.js b/src/directive/non-bindable/non-bindable.js index 1e98e0343..b0563f19d 100644 --- a/src/directive/non-bindable/non-bindable.js +++ b/src/directive/non-bindable/non-bindable.js @@ -1,5 +1,5 @@ /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ export function ngNonBindableDirective() { return { diff --git a/src/directive/select/select.js b/src/directive/select/select.js index d96244516..486a3ae16 100644 --- a/src/directive/select/select.js +++ b/src/directive/select/select.js @@ -427,7 +427,7 @@ class SelectController { } /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ export function selectDirective() { return { diff --git a/src/directive/setter/setter.js b/src/directive/setter/setter.js index 46f7884ca..e63f9f647 100644 --- a/src/directive/setter/setter.js +++ b/src/directive/setter/setter.js @@ -5,7 +5,7 @@ ngSetterDirective.$inject = [$t._parse, $t._log]; /** * @param {ng.ParseService} $parse * @param {ng.LogService} $log - * @returns {import('interface.ts').Directive} + * @returns {ng.Directive} */ export function ngSetterDirective($parse, $log) { return { @@ -27,7 +27,7 @@ export function ngSetterDirective($parse, $log) { return; } - const updateModel = (value) => { + const updateModel = (/** @type {string} */ value) => { assignModel(scope, value.trim()); }; diff --git a/src/directive/show-hide/show-hide.js b/src/directive/show-hide/show-hide.js index f6d27e5f9..8f98be1b9 100644 --- a/src/directive/show-hide/show-hide.js +++ b/src/directive/show-hide/show-hide.js @@ -42,7 +42,7 @@ export function ngShowDirective($animate) { ngHideDirective.$inject = [$injectTokens._animate]; /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ export function ngHideDirective($animate) { return { diff --git a/src/directive/switch/switch.js b/src/directive/switch/switch.js index 8ddcd5b66..29c8aa9d8 100644 --- a/src/directive/switch/switch.js +++ b/src/directive/switch/switch.js @@ -98,7 +98,7 @@ export function ngSwitchDirective($animate) { } /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ export function ngSwitchWhenDirective() { return { @@ -128,7 +128,7 @@ export function ngSwitchWhenDirective() { } /** - * @returns {import('../../interface.ts').Directive} + * @returns {ng.Directive} */ export function ngSwitchDefaultDirective() { return { diff --git a/src/interface.ts b/src/interface.ts index 96526b116..45b9cb574 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -394,7 +394,7 @@ export interface DirectivePrePost { * A link function executed during directive linking. */ export type DirectiveLinkFn = ( - scope: Scope, + scope: ng.Scope, element: HTMLElement, attrs: ng.Attributes, controller?: TController, diff --git a/src/namespace.ts b/src/namespace.ts index e7211f030..3221f915a 100644 --- a/src/namespace.ts +++ b/src/namespace.ts @@ -195,7 +195,7 @@ declare global { export type Listener = TListener; export type DocumentService = Document; export type WindowService = Window; - export type AngularServie = Angular; + export type AngularService = Angular; export type WorkerConfig = TWorkerConfig; export type WorkerConnection = TWorkerConnection; export type Injectable< diff --git a/src/ng.js b/src/ng.js index d2b841541..6f17f06b7 100644 --- a/src/ng.js +++ b/src/ng.js @@ -138,6 +138,7 @@ import { ngWasmDirective } from "./directive/wasm/wasm.js"; import { ngScopeDirective } from "./directive/scope/scope.js"; import { CookieProvider } from "./services/cookie/cookie.js"; import { RestProvider } from "./services/rest/rest.js"; +import { ngListenerDirective } from "./directive/listener/listener.js"; /** * Initializes core `ng` module. @@ -188,6 +189,7 @@ export function registerNgModule(angular) { ngInclude: ngIncludeDirective, ngInject: ngInjectDirective, ngInit: ngInitDirective, + ngListener: ngListenerDirective, ngMessages: ngMessagesDirective, ngMessage: ngMessageDirective, ngMessageExp: ngMessageExpDirective, diff --git a/src/router/scroll/interface.ts b/src/router/scroll/interface.ts index eb23a6cbd..7ce93ce6a 100644 --- a/src/router/scroll/interface.ts +++ b/src/router/scroll/interface.ts @@ -1,3 +1,3 @@ export type ViewScrollService = | ng.AnchorScrollService - | ((el: Element) => void | Promise); + | ((element: Element) => void); diff --git a/src/router/scroll/view-scroll.js b/src/router/scroll/view-scroll.js index 39309c4b0..dd5f29e71 100644 --- a/src/router/scroll/view-scroll.js +++ b/src/router/scroll/view-scroll.js @@ -21,10 +21,10 @@ export class ViewScrollProvider { return $anchorScroll; } - return async function ($element) { + return (/** @type {Element} */ $element) => { validateInstanceOf($element, Element, "$element"); - return setTimeout(() => { + setTimeout(() => { $element.scrollIntoView(false); }, 0); }; diff --git a/src/services/log/log.js b/src/services/log/log.js index c15090842..379e00398 100644 --- a/src/services/log/log.js +++ b/src/services/log/log.js @@ -43,7 +43,8 @@ export class LogProvider { * @param {string} type */ _consoleLog(type) { - const console = window.console || {}; + const console = + window.console || /** @type {Partial>} */ ({}); const logFn = console[type] || @@ -52,7 +53,7 @@ export class LogProvider { /* empty */ }); - return (...args) => { + return (/** @type {any[]} */ ...args) => { const formattedArgs = args.map((arg) => this._formatError(arg)); return logFn.apply(console, formattedArgs); diff --git a/src/services/pubsub/pubsub.js b/src/services/pubsub/pubsub.js index e3cb80f3c..5dc3f922b 100644 --- a/src/services/pubsub/pubsub.js +++ b/src/services/pubsub/pubsub.js @@ -104,7 +104,7 @@ export class PubSub { let called = false; - const wrapper = (...args) => { + const wrapper = (/** @type {any[]} */ ...args) => { if (called) return; called = true; diff --git a/src/services/stream/stream.js b/src/services/stream/stream.js index 2e81c85ca..587bb0e06 100644 --- a/src/services/stream/stream.js +++ b/src/services/stream/stream.js @@ -14,7 +14,7 @@ export class StreamConnection { retryDelay: 1000, maxRetries: Infinity, heartbeatTimeout: 15000, - transformMessage: (data) => { + transformMessage: (/** @type {string} */ data) => { try { return JSON.parse(data); } catch { @@ -26,7 +26,7 @@ export class StreamConnection { this.$log = log; this.retryCount = 0; this.closed = false; - this.heartbeatTimer = null; + this.heartbeatTimer = undefined; /** @type {EventSource | WebSocket | null} */ this.connection = null; diff --git a/src/shared/noderef.js b/src/shared/noderef.js index 3aa9030de..134a1295b 100644 --- a/src/shared/noderef.js +++ b/src/shared/noderef.js @@ -13,14 +13,14 @@ export class NodeRef { * @throws {Error} If the argument is invalid or cannot be wrapped properly. */ constructor(element) { - /** @private @type {Node | ChildNode | null} */ - this._node = null; + /** @private @type {Node | ChildNode | undefined} */ + this._node = undefined; /** @type {Element | undefined} */ this._element = undefined; - /** @private @type {Array | undefined} a stable list on nodes */ - this._nodes = undefined; + /** @private @type {Array} a stable list on nodes */ + this._nodes = []; /** @type {boolean} */ this._isList = false; @@ -138,7 +138,9 @@ export class NodeRef { if (this._isList) { return this._nodes[0]; } else { - return this._element || this._node; + return /** @type {Element | Node | ChildNode} */ ( + this._element || this._node + ); } } @@ -147,7 +149,9 @@ export class NodeRef { if (this._isList) { return this._nodes; } else { - return this._element || this._node; + return /** @type {Element | Node | ChildNode} */ ( + this._element || this._node + ); } } @@ -156,7 +160,9 @@ export class NodeRef { if (this._isList) { return Array.from(this._nodes); } else { - return [this._element || this._node]; + return [ + /** @type {Element | Node | ChildNode} */ (this._element || this._node), + ]; } } diff --git a/src/shared/utils.js b/src/shared/utils.js index 683a22796..fe15dde92 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -150,8 +150,7 @@ export function isBlankObject(value) { /** * Determines if a reference is a `string`. - * - * @param value - The value to check. + * @param {unknown} value - The value to check. * @returns {value is string} True if `value` is a string. */ export function isString(value) { @@ -345,19 +344,24 @@ export function trim(value) { return isString(value) ? value.trim() : value; } +/** + * @param {string} name + * @param {string} separator + */ export function snakeCase(name, separator) { const modseparator = separator || "_"; return name.replace( /[A-Z]/g, - (letter, pos) => (pos ? modseparator : "") + letter.toLowerCase(), + (/** @type {string} */ letter, /** @type {any} */ pos) => + (pos ? modseparator : "") + letter.toLowerCase(), ); } /** * Set or clear the hashkey for an object. - * @param obj object - * @param hashkey the hashkey (!truthy to delete the hashkey) + * @param {{ [x: string]: any; $$hashKey?: any; }} obj object + * @param {any} hashkey the hashkey (!truthy to delete the hashkey) */ export function setHashKey(obj, hashkey) { if (hashkey) { @@ -445,6 +449,9 @@ export function inherit(parent, extra) { return extend(Object.create(parent), extra); } +/** + * @param {{ toString: () => string; }} obj + */ export function hasCustomToString(obj) { return isFunction(obj.toString) && obj.toString !== toString; } @@ -461,6 +468,10 @@ export function getNodeName(element) { return lowercase(element.nodeName); } +/** + * @param {any} array + * @param {string} obj + */ export function includes(array, obj) { return Array.prototype.indexOf.call(array, obj) !== -1; } @@ -483,6 +494,10 @@ export function arrayRemove(array, value) { return index; } +/** + * @param {unknown} val1 + * @param {unknown} val2 + */ export function simpleCompare(val1, val2) { return val1 === val2 || (Number.isNaN(val1) && Number.isNaN(val2)); } @@ -637,6 +652,10 @@ export function assertNotHasOwnProperty(name, context) { } } +/** + * @param {unknown} value + * @returns {string | unknown} + */ export function stringify(value) { if (isNull(value) || isUndefined(value)) { return ""; @@ -666,10 +685,19 @@ export function isValidObjectMaxDepth(maxDepth) { return isNumber(maxDepth) && maxDepth > 0; } +/** + * @param {any[]} array1 + * @param {IArguments | any[] | NodeListOf} array2 + * @param {number | undefined} [index] + */ export function concat(array1, array2, index) { return array1.concat(Array.prototype.slice.call(array2, index)); } +/** + * @param {IArguments | [string, ...any[]]} args + * @param {number} startIndex + */ export function sliceArgs(args, startIndex) { return Array.prototype.slice.call(args, startIndex || 0); } @@ -705,6 +733,10 @@ export function bind(context, fn) { return fn; } +/** + * @param {string} key + * @param {unknown} value + */ function toJsonReplacer(key, value) { let val = value; @@ -777,6 +809,10 @@ export function fromJson(json) { const MS_PER_MINUTE = 60_000; // 60,000 ms in a minute +/** + * @param {any} timezone + * @param {number} [fallback] + */ export function timezoneToOffset(timezone, fallback) { const requestedTimezoneOffset = Date.parse(`Jan 01, 1970 00:00:00 ${timezone}`) / MS_PER_MINUTE; @@ -786,6 +822,10 @@ export function timezoneToOffset(timezone, fallback) { : requestedTimezoneOffset; } +/** + * @param {Date} date + * @param {number} minutes + */ export function addDateMinutes(date, minutes) { const newDate = new Date(date.getTime()); @@ -794,6 +834,11 @@ export function addDateMinutes(date, minutes) { return newDate; } +/** + * @param {Date} date + * @param {any} timezone + * @param {undefined} [reverse] + */ export function convertTimezoneToLocal(date, timezone, reverse) { const doReverse = reverse ? -1 : 1; @@ -810,7 +855,7 @@ export function convertTimezoneToLocal(date, timezone, reverse) { /** * Parses an escaped url query string into key-value pairs. * @param {string} keyValue - * @returns {Object.} + * @returns {Object.>} */ export function parseKeyValue(keyValue) { const obj = {}; @@ -846,10 +891,16 @@ export function parseKeyValue(keyValue) { } }); - return /** @type {Object.} */ (obj); + return /** @type {Object.>} */ (obj); } +/** + * @param {string | { [s: string]: any; } | ArrayLike | null} obj + */ export function toKeyValue(obj) { + /** + * @type {string[]} + */ const parts = []; obj && @@ -917,6 +968,8 @@ export function encodeUriSegment(val) { * pct-encoded = "%" HEXDIG HEXDIG * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" + * @param {string | number | boolean} val + * @param {boolean | undefined} [pctEncodeSpaces] */ export function encodeUriQuery(val, pctEncodeSpaces) { return encodeURIComponent(val) @@ -931,28 +984,41 @@ export function encodeUriQuery(val, pctEncodeSpaces) { export const ngAttrPrefixes = ["ng-", "data-ng-"]; /** - * Creates a shallow copy of an object, an array or a primitive. + * Creates a shallow copy of an object, an array, or returns primitives as-is. * - * Assumes that there are no proto properties for objects. + * Assumes there are no proto properties. + * + * @template T + * @param {T} src + * @param {T extends any[] ? T : undefined} [dst] + * @returns {T} */ export function shallowCopy(src, dst) { if (isArray(src)) { - dst = dst || []; + /** @type {any[]} */ + const out = dst || []; for (let i = 0, ii = src.length; i < ii; i++) { - dst[i] = src[i]; + out[i] = src[i]; } - } else if (isObject(src)) { - dst = dst || {}; + + return /** @type {T} */ (out); + } + + if (isObject(src)) { + /** @type {Record} */ + const out = {}; for (const key in src) { if (!(key.startsWith("$") && key.charAt(1) === "$")) { - dst[key] = src[key]; + out[key] = src[key]; } } + + return /** @type {T} */ (out); } - return dst || src; + return src; } /** @@ -968,6 +1034,9 @@ export function assert(argument, errorMsg = "Assertion failed") { /** * Throw error if the argument is falsy. + * @param {string | boolean | Object} arg + * @param {string} name + * @param {string | undefined} [reason] */ export function assertArg(arg, name, reason) { if (!arg) { @@ -982,6 +1051,11 @@ export function assertArg(arg, name, reason) { return arg; } +/** + * @param {string | Function | any[]} arg + * @param {string} name + * @param {boolean | undefined} [acceptArrayAnnotation] + */ export function assertArgFn(arg, name, acceptArrayAnnotation) { if (acceptArrayAnnotation && isArray(arg)) { arg = arg[arg.length - 1]; @@ -1069,7 +1143,7 @@ export function minErr(module) { const templateArgs = sliceArgs(args, 2).map((arg) => toDebugString(arg)); - message += template.replace(/\{\d+\}/g, (match) => { + message += template.replace(/\{\d+\}/g, (/** @type {string} */ match) => { const index = +match.slice(1, -1); if (index < templateArgs.length) {