Skip to content

Commit 51c3673

Browse files
author
Anatoly Ostrovsky
committed
ngEl directive
1 parent e8278eb commit 51c3673

File tree

10 files changed

+185
-24
lines changed

10 files changed

+185
-24
lines changed

@types/directive/el/el.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* @returns {ng.Directive}
3+
*/
4+
export function ngElDirective(): ng.Directive;
Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
/**
22
*
3-
* @param {import("../../core/parse/interface.ts").ParseService} $parse
4-
* @param {import('../../services/exception/exception-handler.js').ErrorHandler} $exceptionHandler
3+
* @param {ng.ParseService} $parse
4+
* @param {ng.ExceptionHandlerService} $exceptionHandler
55
* @param {string} directiveName
66
* @param {string} eventName
7-
* @returns {import("../../interface.ts").Directive}
7+
* @returns {ng.Directive}
88
*/
99
export function createEventDirective(
10-
$parse: import("../../core/parse/interface.ts").ParseService,
11-
$exceptionHandler: import("../../services/exception/exception-handler.js").ErrorHandler,
10+
$parse: ng.ParseService,
11+
$exceptionHandler: ng.ExceptionHandlerService,
1212
directiveName: string,
1313
eventName: string,
14-
): import("../../interface.ts").Directive;
14+
): ng.Directive;
1515
/**
16-
* @type {Record<string, import("../../interface.js").DirectiveFactory>}
16+
* @type {Record<string, ng.DirectiveFactory>}
1717
*/
18-
export const ngEventDirectives: Record<
19-
string,
20-
import("../../interface.js").DirectiveFactory
21-
>;
18+
export const ngEventDirectives: Record<string, ng.DirectiveFactory>;

@types/directive/validators/validators.d.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ export const patternDirective: (
104104
*/
105105
export const maxlengthDirective: (
106106
| string
107-
| ((
108-
$parse: import("../../core/parse/interface.ts").ParseService,
109-
) => import("../../interface.ts").Directive)
107+
| (($parse: ng.ParseService) => ng.Directive)
110108
)[];
111109
/**
112110
*

src/directive/el/el.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 type="module" src="/src/directive/el/el.spec.js"></script>
15+
</head>
16+
<body>
17+
<div id="app"></div>
18+
</body>
19+
</html>

src/directive/el/el.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @returns {ng.Directive}
3+
*/
4+
export function ngElDirective() {
5+
return {
6+
restrict: "A",
7+
link(scope, element, attrs) {
8+
const expr = attrs["ngEl"];
9+
const key = !expr ? element.id : expr;
10+
11+
scope.$target[key] = element;
12+
const parent = element.parentNode;
13+
if (!parent) return;
14+
15+
const observer = new MutationObserver((mutations) => {
16+
for (const mutation of mutations) {
17+
Array.from(mutation.removedNodes).forEach((removedNode) => {
18+
if (removedNode === element) {
19+
//
20+
delete scope.$target[key];
21+
observer.disconnect();
22+
}
23+
});
24+
}
25+
});
26+
27+
observer.observe(parent, { childList: true });
28+
},
29+
};
30+
}

src/directive/el/el.spec.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { Angular } from "../../angular.js";
2+
import { dealoc } from "../../shared/dom.js";
3+
import { wait } from "../../shared/test-utils.js";
4+
import { ngElDirective } from "./el.js";
5+
6+
describe("ngEl", () => {
7+
let $compile, $rootScope, el, $log;
8+
9+
beforeEach(() => {
10+
el = document.getElementById("app");
11+
dealoc(el);
12+
el.innerHTML = "";
13+
14+
const angular = new Angular();
15+
angular.module("default", []);
16+
17+
angular
18+
.bootstrap(el, ["default"])
19+
.invoke((_$compile_, _$rootScope_, _$log_) => {
20+
$compile = _$compile_;
21+
$rootScope = _$rootScope_;
22+
$log = _$log_;
23+
});
24+
});
25+
26+
it("should attach element to scope.$target by id when no expression is provided", async () => {
27+
el.innerHTML = `<div id="foo" ng-el></div>`;
28+
$compile(el)($rootScope);
29+
await wait();
30+
31+
expect($rootScope.$target.foo).toBeDefined();
32+
expect($rootScope.$target.foo instanceof HTMLElement).toBe(true);
33+
expect($rootScope.$target.foo.id).toBe("foo");
34+
});
35+
36+
it("should attach element to scope.$target using ng-el value as key", async () => {
37+
el.innerHTML = `<div id="bar" ng-el="myEl"></div>`;
38+
$compile(el)($rootScope);
39+
await wait();
40+
41+
expect($rootScope.$target.myEl).toBeDefined();
42+
expect($rootScope.$target.myEl.id).toBe("bar");
43+
});
44+
45+
it("should support multiple ng-el elements", async () => {
46+
el.innerHTML = `
47+
<div id="a" ng-el="first"></div>
48+
<div id="b" ng-el="second"></div>
49+
<div id="c" ng-el></div>
50+
`;
51+
52+
$compile(el)($rootScope);
53+
await wait();
54+
55+
expect(Object.keys($rootScope.$target)).toContain("first");
56+
expect(Object.keys($rootScope.$target)).toContain("second");
57+
expect(Object.keys($rootScope.$target)).toContain("c");
58+
expect($rootScope.$target.first.id).toBe("a");
59+
expect($rootScope.$target.second.id).toBe("b");
60+
expect($rootScope.$target.c.id).toBe("c");
61+
});
62+
63+
it("should not throw if $target is not defined on scope", async () => {
64+
el.innerHTML = `<div id="noTarget" ng-el="missing"></div>`;
65+
66+
// no $target defined on scope
67+
expect(() => {
68+
$compile(el)($rootScope);
69+
}).not.toThrow();
70+
});
71+
72+
it("should override previous entries with the same key", async () => {
73+
el.innerHTML = `
74+
<div id="x1" ng-el="dup"></div>
75+
<div id="x2" ng-el="dup"></div>
76+
`;
77+
$rootScope.$target = {};
78+
79+
$compile(el)($rootScope);
80+
await wait();
81+
82+
expect($rootScope.$target.dup.id).toBe("x2");
83+
});
84+
85+
it("should remove reference from scope.$target when element is removed", async () => {
86+
el.innerHTML = `<div id="temp" ng-el="tempEl"></div>`;
87+
$compile(el)($rootScope);
88+
await wait();
89+
90+
expect($rootScope.$target.tempEl).toBeDefined();
91+
const elem = $rootScope.$target.tempEl;
92+
93+
// simulate element removal and scope destruction
94+
elem.remove();
95+
await wait();
96+
97+
expect($rootScope.$target.tempEl).toBeUndefined();
98+
});
99+
});

src/directive/el/el.test.js

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/el/el.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/directive/events/events.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { directiveNormalize } from "../../shared/utils.js";
66
*/
77

88
/**
9-
* @type {Record<string, import("../../interface.js").DirectiveFactory>}
9+
* @type {Record<string, ng.DirectiveFactory>}
1010
*/
1111
export const ngEventDirectives = {};
1212

@@ -19,7 +19,7 @@ export const ngEventDirectives = {};
1919
"$exceptionHandler",
2020
/**
2121
* @param {import("../../core/parse/interface.ts").ParseService} $parse
22-
* @param {import('../../services/exception/exception-handler.js').ErrorHandler} $exceptionHandler
22+
* @param {ng.ExceptionHandlerService} $exceptionHandler
2323
* @returns
2424
*/
2525
($parse, $exceptionHandler) => {
@@ -35,11 +35,11 @@ export const ngEventDirectives = {};
3535

3636
/**
3737
*
38-
* @param {import("../../core/parse/interface.ts").ParseService} $parse
39-
* @param {import('../../services/exception/exception-handler.js').ErrorHandler} $exceptionHandler
38+
* @param {ng.ParseService} $parse
39+
* @param {ng.ExceptionHandlerService} $exceptionHandler
4040
* @param {string} directiveName
4141
* @param {string} eventName
42-
* @returns {import("../../interface.ts").Directive}
42+
* @returns {ng.Directive}
4343
*/
4444
export function createEventDirective(
4545
$parse,

src/directive/validators/validators.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,17 +217,17 @@ export const patternDirective = [
217217
export const maxlengthDirective = [
218218
$t.$parse,
219219
/**
220-
* @param {import("../../core/parse/interface.ts").ParseService} $parse
221-
* @returns {import("../../interface.ts").Directive}
220+
* @param {ng.ParseService} $parse
221+
* @returns {ng.Directive}
222222
*/
223223
($parse) => ({
224224
restrict: "A",
225225
require: "?ngModel",
226226
link:
227227
/**
228-
* @param {import("../../core/scope/scope.js").Scope} scope
229-
* @param {*} _elm
230-
* @param {import("../../core/compile/attributes.js").Attributes} attr
228+
* @param {ng.Scope} scope
229+
* @param {Element} _elm
230+
* @param {ng.Attributes} attr
231231
* @param {import("../../interface.ts").NgModelController} ctrl
232232
* @returns
233233
*/

src/ng.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ import {
133133
} from "./directive/http/http.js";
134134
import { $injectTokens as $t } from "./injection-tokens.js";
135135
import { ngInjectDirective } from "./directive/inject/inject.js";
136+
import { ngElDirective } from "./directive/el/el.js";
136137

137138
/**
138139
* Initializes core `ng` module.
@@ -173,6 +174,7 @@ export function registerNgModule(angular) {
173174
ngController: ngControllerDirective,
174175
ngDelete: ngDeleteDirective,
175176
ngDisabled: ngDisabledAriaDirective,
177+
ngEl: ngElDirective,
176178
ngForm: ngFormDirective,
177179
ngGet: ngGetDirective,
178180
ngHide: ngHideDirective,

0 commit comments

Comments
 (0)