Skip to content

Commit 4792b13

Browse files
committed
Listener directive
1 parent fafbd53 commit 4792b13

File tree

11 files changed

+204
-13
lines changed

11 files changed

+204
-13
lines changed

@types/angular.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export class Angular {
1+
export class Angular extends EventTarget {
22
/** @private @type {!Array<string|any>} */
33
private _bootsrappedModules;
44
/** @public @type {ng.PubSubService} */
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* @param {ng.AngularService} $angular
3+
* @returns {ng.Directive}
4+
*/
5+
export function ngListenerDirective($angular: ng.AngularService): ng.Directive;
6+
export namespace ngListenerDirective {
7+
let $inject: string[];
8+
}

@types/namespace.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ declare global {
179179
type Listener = TListener;
180180
type DocumentService = Document;
181181
type WindowService = Window;
182-
type AngularServie = Angular;
182+
type AngularService = Angular;
183183
type WorkerConfig = TWorkerConfig;
184184
type WorkerConnection = TWorkerConnection;
185185
type Injectable<

src/angular.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ const STRICT_DI = "strict-di";
3131
/** @type {ModuleRegistry} */
3232
const moduleRegistry = {};
3333

34-
export class Angular {
34+
export class Angular extends EventTarget {
3535
constructor() {
36+
super();
37+
3638
/** @private @type {!Array<string|any>} */
3739
this._bootsrappedModules = [];
3840

src/directive/channel/channel.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isObject } from "../../shared/utils.js";
1+
import { isObject, isString } from "../../shared/utils.js";
22
import { $injectTokens } from "../../injection-tokens.js";
33

44
ngChannelDirective.$inject = [$injectTokens._eventBus];
@@ -13,15 +13,18 @@ export function ngChannelDirective($eventBus) {
1313

1414
const hasTemplateContent = element.childNodes.length > 0;
1515

16-
const unsubscribe = $eventBus.subscribe(channel, (value) => {
17-
if (hasTemplateContent) {
18-
if (isObject(value)) {
19-
scope.$merge(value);
16+
const unsubscribe = $eventBus.subscribe(
17+
channel,
18+
(/** @type {string | Object} */ value) => {
19+
if (hasTemplateContent) {
20+
if (isObject(value)) {
21+
scope.$merge(value);
22+
}
23+
} else if (isString(value)) {
24+
element.innerHTML = value;
2025
}
21-
} else {
22-
element.innerHTML = value;
23-
}
24-
});
26+
},
27+
);
2528

2629
scope.$on("$destroy", () => unsubscribe());
2730
},
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>AngularTS Test Runner</title>
6+
7+
<link rel="shortcut icon" type="image/png" href="/images/favicon.ico" />
8+
<link rel="stylesheet" href="/jasmine/jasmine.css" />
9+
<link rel="stylesheet" href="/public/jasmine-helper.css" />
10+
<script src="/jasmine/jasmine.js"></script>
11+
<script src="/jasmine/jasmine-html.js"></script>
12+
<script src="/jasmine/boot0.js"></script>
13+
<script src="/jasmine/boot1.js"></script>
14+
<script
15+
type="module"
16+
src="/src/directive/listener/listener.spec.js"
17+
></script>
18+
</head>
19+
<body>
20+
<div id="app"></div>
21+
</body>
22+
</html>

src/directive/listener/listener.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { isObject, isString } from "../../shared/utils.js";
2+
import { $injectTokens } from "../../injection-tokens.js";
3+
4+
ngListenerDirective.$inject = [$injectTokens._angular];
5+
/**
6+
* @param {ng.AngularService} $angular
7+
* @returns {ng.Directive}
8+
*/
9+
export function ngListenerDirective($angular) {
10+
return {
11+
scope: false,
12+
link: (scope, element, attrs) => {
13+
const channel = attrs.ngListener;
14+
15+
const hasTemplateContent = element.childNodes.length > 0;
16+
17+
/** @type {EventListener} */
18+
const fn = (event) => {
19+
const value = /** @type {CustomEvent} */ (event).detail;
20+
21+
if (hasTemplateContent) {
22+
if (isObject(value)) {
23+
scope.$merge(value);
24+
}
25+
} else if (isString(value)) {
26+
element.innerHTML = value;
27+
}
28+
};
29+
30+
$angular.addEventListener(channel, fn);
31+
32+
scope.$on("$destroy", () => $angular.removeEventListener(channel, fn));
33+
},
34+
};
35+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Angular } from "../../angular.js";
2+
import { dealoc } from "../../shared/dom.js";
3+
import { wait } from "../../shared/test-utils.js";
4+
5+
describe("ngListenerDirective", () => {
6+
let $compile, $scope, element, angular, app;
7+
8+
beforeEach(async () => {
9+
app = document.getElementById("app");
10+
dealoc(app);
11+
12+
angular = new Angular();
13+
angular.module("myModule", ["ng"]);
14+
15+
angular.bootstrap(app, ["myModule"]).invoke((_$compile_, _$rootScope_) => {
16+
$compile = _$compile_;
17+
$scope = _$rootScope_;
18+
});
19+
20+
await wait();
21+
});
22+
23+
afterEach(() => {
24+
dealoc(app);
25+
});
26+
27+
it("registers and unregisters an event listener", async () => {
28+
spyOn(angular, "addEventListener").and.callThrough();
29+
spyOn(angular, "removeEventListener").and.callThrough();
30+
31+
element = $compile(`<div ng-listener="test:event"></div>`)($scope);
32+
await wait();
33+
34+
expect(angular.addEventListener).toHaveBeenCalledWith(
35+
"test:event",
36+
jasmine.any(Function),
37+
);
38+
39+
$scope.$destroy();
40+
await wait();
41+
42+
expect(angular.removeEventListener).toHaveBeenCalledWith(
43+
"test:event",
44+
jasmine.any(Function),
45+
);
46+
});
47+
48+
it("merges object detail into scope when template content exists", async () => {
49+
$scope.foo = "initial";
50+
51+
element = $compile(`
52+
<div ng-listener="update">
53+
<span>{{ foo }}</span>
54+
</div>
55+
`)($scope);
56+
57+
await wait();
58+
59+
angular.dispatchEvent(
60+
new CustomEvent("update", {
61+
detail: { foo: "updated", bar: 123 },
62+
}),
63+
);
64+
65+
await wait();
66+
67+
expect($scope.foo).toBe("updated");
68+
expect($scope.bar).toBe(123);
69+
});
70+
71+
it("updates innerHTML when detail is a string and no template content exists", async () => {
72+
element = $compile(`<div ng-listener="html"></div>`)($scope);
73+
await wait();
74+
75+
angular.dispatchEvent(
76+
new CustomEvent("html", {
77+
detail: "<span>Injected</span>",
78+
}),
79+
);
80+
81+
await wait();
82+
83+
expect(element.innerHTML).toBe("<span>Injected</span>");
84+
});
85+
86+
it("ignores non-object detail when template content exists", async () => {
87+
$scope.foo = "unchanged";
88+
89+
element = $compile(`
90+
<div ng-listener="noop">
91+
<span>{{ foo }}</span>
92+
</div>
93+
`)($scope);
94+
95+
await wait();
96+
97+
angular.dispatchEvent(
98+
new CustomEvent("noop", {
99+
detail: "should not apply",
100+
}),
101+
);
102+
103+
await wait();
104+
105+
expect($scope.foo).toBe("unchanged");
106+
});
107+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
const TEST_URL = "src/directive/listener/listener.html";
4+
5+
test("unit tests contain no errors", async ({ page }) => {
6+
await page.goto(TEST_URL);
7+
await page.content();
8+
await page.waitForTimeout(1000);
9+
await expect(page.locator(".jasmine-overall-result")).toHaveText(
10+
/ 0 failures/,
11+
);
12+
});

src/namespace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ declare global {
195195
export type Listener = TListener;
196196
export type DocumentService = Document;
197197
export type WindowService = Window;
198-
export type AngularServie = Angular;
198+
export type AngularService = Angular;
199199
export type WorkerConfig = TWorkerConfig;
200200
export type WorkerConnection = TWorkerConnection;
201201
export type Injectable<

0 commit comments

Comments
 (0)