Skip to content

Commit f7b731c

Browse files
author
Anatoly Ostrovsky
committed
Fix for impure binary expression. Types and test imrovements
1 parent e43a79d commit f7b731c

File tree

19 files changed

+379
-51
lines changed

19 files changed

+379
-51
lines changed

@types/core/scope/interface.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ export interface AsyncQueueTask {
55
fn: (...args: any[]) => any;
66
locals: Record<string, any>;
77
}
8-
export type ListenerFunction = (newValue: any, originalTarget: object) => void;
8+
export type ListenerFn = (newValue?: any, originalTarget?: object) => void;
99
export interface Listener {
1010
originalTarget: object;
11-
listenerFn: ListenerFunction;
11+
listenerFn: ListenerFn;
1212
watchFn: CompiledExpression;
1313
id: number;
1414
scopeId: number;

@types/core/scope/scope.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ export class Scope {
128128
* function is invoked when changes to that property are detected.
129129
*
130130
* @param {string} watchProp - An expression to be watched in the context of this model.
131-
* @param {import('./interface.ts').ListenerFunction} [listenerFn] - A function to execute when changes are detected on watched context.
131+
* @param {ng.ListenerFn} [listenerFn] - A function to execute when changes are detected on watched context.
132132
* @param {boolean} [lazy] - A flag to indicate if the listener should be invoked immediately. Defaults to false.
133133
*/
134134
$watch(
135135
watchProp: string,
136-
listenerFn?: import("./interface.ts").ListenerFunction,
136+
listenerFn?: ng.ListenerFn,
137137
lazy?: boolean,
138138
): () => void;
139139
$new(childInstance: any): any;

@types/namespace.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ export { angular } from "./index.js";
22
import { Angular as TAngular } from "./angular.js";
33
import { Attributes as TAttributes } from "./core/compile/attributes.js";
44
import { Scope as TScope } from "./core/scope/scope.js";
5+
import {
6+
ListenerFn as TListenerFn,
7+
Listener as TListener,
8+
} from "./core/scope/interface.ts";
59
import { NgModule as TNgModule } from "./core/di/ng-module.js";
610
import { InjectorService as TInjectorService } from "./core/di/internal-injector.js";
711
import {
@@ -107,8 +111,10 @@ declare global {
107111
type TemplateCacheService = Map<string, string>;
108112
type TemplateRequestService = TTemplateRequestService;
109113
type ErrorHandlingConfig = TErrorHandlingConfig;
110-
type WindowService = Window;
114+
type ListenerFn = TListenerFn;
115+
type Listener = TListener;
111116
type DocumentService = Document;
117+
type WindowService = Window;
112118
type WorkerConfig = TWorkerConfig;
113119
type WorkerConnection = TWorkerConnection;
114120
}

@types/shared/utils.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,18 @@ export function toDebugString(obj: any): any;
488488
* The resulting string key is in 'type:hashKey' format.
489489
*/
490490
export function hashKey(obj: any): string;
491-
export function mergeClasses(a: any, b: any): any;
491+
/**
492+
* Merges two class name values into a single space-separated string.
493+
* Accepts strings, arrays of strings, or null/undefined values.
494+
*
495+
* @param {string | string[] | null | undefined} a - The first class name(s).
496+
* @param {string | string[] | null | undefined} b - The second class name(s).
497+
* @returns {string} A single string containing all class names separated by spaces.
498+
*/
499+
export function mergeClasses(
500+
a: string | string[] | null | undefined,
501+
b: string | string[] | null | undefined,
502+
): string;
492503
/**
493504
* Converts all accepted directives format into proper directive name.
494505
* @param {string} name Name to normalize

src/animations/animate.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { isFunction, isObject, minErr, extend } from "../shared/utils.js";
1+
import {
2+
isFunction,
3+
isObject,
4+
minErr,
5+
extend,
6+
mergeClasses,
7+
} from "../shared/utils.js";
28
import { removeElement, animatedomInsert } from "../shared/dom.js";
39
import { NG_ANIMATE_CLASSNAME } from "./shared.js";
410

@@ -14,15 +20,6 @@ import { NG_ANIMATE_CLASSNAME } from "./shared.js";
1420

1521
const $animateMinErr = minErr("$animate");
1622

17-
function mergeClasses(a, b) {
18-
if (!a && !b) return "";
19-
if (!a) return b;
20-
if (!b) return a;
21-
if (Array.isArray(a)) a = a.join(" ");
22-
if (Array.isArray(b)) b = b.join(" ");
23-
return `${a} ${b}`;
24-
}
25-
2623
// if any other type of options value besides an Object value is
2724
// passed into the $animate.method() animation then this helper code
2825
// will be run which will ignore it. While this patch is not the

src/animations/animate.spec.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createElementFromHTML, dealoc } from "../shared/dom.js";
22
import { Angular } from "../angular.js";
3-
import { isObject } from "../shared/utils.js";
3+
import { isObject, mergeClasses } from "../shared/utils.js";
44
import { isFunction, wait } from "../shared/utils.js";
55
import { createInjector } from "../core/di/injector.js";
66

@@ -496,4 +496,8 @@ describe("$animate", () => {
496496
});
497497
});
498498
});
499+
500+
describe("mergeClasses", () => {
501+
expect(mergeClasses);
502+
});
499503
});

src/animations/runner/animate-runner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @fileoverview
33
* Frame-synchronized animation runner and scheduler.
44
* Provides async batching of animation callbacks using requestAnimationFrame.
5-
* In AngularJS, this user to be implemented as `$$AnimateRunner`
5+
* In AngularJS, this used to be implemented as `$$AnimateRunner`
66
*/
77

88
/**

src/core/interpolate/interpolate.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ describe("$interpolate", () => {
2929
expect(interp({})).toEqual("why u no }}work{{");
3030
});
3131

32+
it("evaluates binary expressions", function () {
33+
let interp = $interpolate("{{a + b}}");
34+
expect(interp({ a: 11, b: 22 })).toEqual("33");
35+
36+
interp = $interpolate("{{a + b + c}}");
37+
expect(interp({ a: 11, b: 22, c: 33 })).toEqual("66");
38+
});
39+
3240
it("evaluates many expressions", function () {
3341
const interp = $interpolate("First {{anAttr}}, then {{anotherAttr}}!");
3442
expect(interp({ anAttr: "42", anotherAttr: "43" })).toEqual(

src/core/scope/interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ export interface AsyncQueueTask {
77
locals: Record<string, any>;
88
}
99

10-
export type ListenerFunction = (newValue: any, originalTarget: object) => void;
10+
export type ListenerFn = (newValue?: any, originalTarget?: object) => void;
1111

1212
export interface Listener {
1313
originalTarget: object;
14-
listenerFn: ListenerFunction;
14+
listenerFn: ListenerFn;
1515
watchFn: CompiledExpression;
1616
id: number; // Deregistration id
1717
scopeId: number; // The scope id that created the Listener

src/core/scope/scope.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ export class Scope {
616616
* function is invoked when changes to that property are detected.
617617
*
618618
* @param {string} watchProp - An expression to be watched in the context of this model.
619-
* @param {import('./interface.ts').ListenerFunction} [listenerFn] - A function to execute when changes are detected on watched context.
619+
* @param {ng.ListenerFn} [listenerFn] - A function to execute when changes are detected on watched context.
620620
* @param {boolean} [lazy] - A flag to indicate if the listener should be invoked immediately. Defaults to false.
621621
*/
622622
$watch(watchProp, listenerFn, lazy = false) {
@@ -638,7 +638,7 @@ export class Scope {
638638
return () => {};
639639
}
640640

641-
/** @type {import('./interface.ts').Listener} */
641+
/** @type {ng.Listener} */
642642
const listener = {
643643
originalTarget: this.$target,
644644
listenerFn: listenerFn,
@@ -696,13 +696,34 @@ export class Scope {
696696
}
697697
// 6
698698
case ASTType.BinaryExpression: {
699-
let expr = get.decoratedNode.body[0].expression.toWatch[0];
700-
key = expr.property ? expr.property.name : expr.name;
701-
if (!key) {
702-
throw new Error("Unable to determine key");
699+
if (get.decoratedNode.body[0].expression.isPure) {
700+
let expr = get.decoratedNode.body[0].expression.toWatch[0];
701+
key = expr.property ? expr.property.name : expr.name;
702+
if (!key) {
703+
throw new Error("Unable to determine key");
704+
}
705+
listener.property.push(key);
706+
break;
707+
} else {
708+
let keys = [];
709+
get.decoratedNode.body[0].expression.toWatch.forEach((x) => {
710+
const k = x.property ? x.property.name : x.name;
711+
if (!k) {
712+
throw new Error("Unable to determine key");
713+
}
714+
keys.push(k);
715+
});
716+
keys.forEach((key) => {
717+
this.#registerKey(key, listener);
718+
this.#scheduleListener([listener]);
719+
});
720+
721+
return () => {
722+
keys.forEach((key) => {
723+
this.#deregisterKey(key, listener.id);
724+
});
725+
};
703726
}
704-
listener.property.push(key);
705-
break;
706727
}
707728
// 7
708729
case ASTType.UnaryExpression: {

0 commit comments

Comments
 (0)