Skip to content
This repository was archived by the owner on Oct 12, 2021. It is now read-only.

Commit ec90574

Browse files
committed
feat(AppShell): shellRender and shellNoRender attributes and implement different stripping strategies
- [x] Make the shell-related directives add `shellRender` and `shellNoRender` attributes. - [x] Encapsulate the "hiding", "showing" element logic into a service which is injected with DI and can its provider can be overridden. - [x] Add visitor which finds App Shell related comments in templates and processes them. - [x] Remove useless imports.
1 parent 42a61bf commit ec90574

29 files changed

+307
-56
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Provider} from '@angular/core';
2+
3+
import { IS_PRERENDER } from './is-prerender.service';
4+
import { TemplateVisibilityStrategy } from './template-visibility-strategy';
5+
import { TemplateCommentStrategy } from './template-comment-strategy';
6+
7+
export const APP_SHELL_RUNTIME_PROVIDERS: Provider[] = [
8+
{
9+
provide: IS_PRERENDER,
10+
useValue: false
11+
},
12+
{
13+
provide: TemplateVisibilityStrategy,
14+
useClass: TemplateCommentStrategy
15+
}
16+
];
17+
18+
export const APP_SHELL_BUILD_PROVIDERS: Provider[] = [
19+
{
20+
provide: IS_PRERENDER,
21+
useValue: true
22+
},
23+
{
24+
provide: TemplateVisibilityStrategy,
25+
useClass: TemplateCommentStrategy
26+
}
27+
];

app-shell/src/app/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './module';
22
export * from './prerender';
33
export * from './shell';
4+

app-shell/src/app/shell.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default function () {
2222
expect(fixture.debugElement.childNodes.length).toBe(1);
2323
expect(fixture.debugElement.childNodes[0].nativeNode.data).toBe('template bindings={}');
2424
});
25+
2526
it('should render the element at runtime', () => {
2627
const fixture = TestBed
2728
.configureTestingModule({
@@ -55,6 +56,7 @@ export default function () {
5556
expect(fixture.debugElement.childNodes[0].nativeNode.data).toBe('template bindings={}');
5657
expect(fixture.debugElement.childNodes[1].nativeNode.name).toBe('div');
5758
});
59+
5860
it('should NOT render the element at runtime', () => {
5961
const fixture = TestBed
6062
.configureTestingModule({
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Injectable } from '@angular/core';
2+
import { __platform_browser_private__ as _ } from '@angular/platform-browser';
3+
4+
import { TemplateVisibilityStrategy } from './template-visibility-strategy';
5+
6+
@Injectable()
7+
export class TemplateCommentStrategy extends TemplateVisibilityStrategy {
8+
private _hidden: any[] = [];
9+
private _shown: any[] = [];
10+
11+
show(marker: string) {
12+
(this._rootNodes[marker] || [])
13+
.filter((node: any) => this._shown.indexOf(node) < 0)
14+
.forEach((node: any) => {
15+
_.getDOM().setAttribute(node, marker, '');
16+
this._shown.push(node);
17+
});
18+
}
19+
20+
hide(marker: string) {
21+
const DOM = _.getDOM();
22+
(this._rootNodes[marker] || [])
23+
.filter((node: any) => this._hidden.indexOf(node) < 0)
24+
.forEach((node: any) => {
25+
const comment = DOM.createComment(`${marker}(${DOM.getOuterHTML(node)})`);
26+
const parentNode = DOM.parentElement(node);
27+
DOM.replaceChild(parentNode, comment, node);
28+
this._hidden.push(node);
29+
});
30+
}
31+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export interface DirectiveNodes {
2+
[marker: string]: any[];
3+
};
4+
5+
export abstract class TemplateVisibilityStrategy {
6+
protected _rootNodes: DirectiveNodes = {};
7+
8+
abstract show(marker: string): void;
9+
abstract hide(marker: string): void;
10+
11+
setRootNodes(marker: string, ...rootNodes: any[]) {
12+
const nodes = this._rootNodes[marker] || [];
13+
this._rootNodes[marker] = nodes.concat(rootNodes);
14+
}
15+
}

app-shell/src/experimental/shell-parser/ast/ast-node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export interface ASTNode {
99
parentNode?: ASTNode;
1010
nodeName: string;
1111
value?: string;
12+
data?: string;
1213
}
13-
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export * from './ast-node';
2-

app-shell/src/experimental/shell-parser/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export type RouteDefinition = string;
33
const SHELL_PARSER_CACHE_NAME = 'mobile-toolkit:app-shell';
44
const APP_SHELL_URL = './app_shell.html';
55
const NO_RENDER_CSS_SELECTOR = '[shellNoRender]';
6+
const RENDER_MARKER = 'shellRender';
67
const ROUTE_DEFINITIONS: RouteDefinition[] = [];
78
const INLINE_IMAGES: string[] = ['png', 'svg', 'jpg'];
89

@@ -13,6 +14,7 @@ export interface ShellParserConfig {
1314
APP_SHELL_URL?: string;
1415
SHELL_PARSER_CACHE_NAME?: string;
1516
NO_RENDER_CSS_SELECTOR?: string;
17+
RENDER_MARKER?: string;
1618
ROUTE_DEFINITIONS?: RouteDefinition[];
1719
INLINE_IMAGES?: string[];
1820
}
@@ -21,6 +23,7 @@ export const SHELL_PARSER_DEFAULT_CONFIG: ShellParserConfig = {
2123
SHELL_PARSER_CACHE_NAME,
2224
APP_SHELL_URL,
2325
NO_RENDER_CSS_SELECTOR,
26+
RENDER_MARKER,
2427
ROUTE_DEFINITIONS,
2528
INLINE_IMAGES
2629
};

app-shell/src/experimental/shell-parser/node-matcher/css-node-matcher.spec.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import {
2-
inject
3-
} from '@angular/core/testing';
41
import { ASTNode } from '../ast';
52
import { CssSelector } from './css-selector';
63
import { CssNodeMatcher } from './css-node-matcher';

app-shell/src/experimental/shell-parser/node-matcher/css-selector/css-selector.spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import {
2-
inject
3-
} from '@angular/core/testing';
41
import { CssSelector } from './css-selector';
52

63
describe('CssSelector', () => {
@@ -52,7 +49,5 @@ describe('CssSelector', () => {
5249
expect(result.elementId).toBe('baz');
5350
expect(result.classNames).toEqual(['foo', 'qux']);
5451
});
55-
5652
});
5753
});
58-
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export * from './css-selector';
2-

app-shell/src/experimental/shell-parser/node-matcher/node-matcher.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,3 @@ import {ASTNode} from '../ast';
33
export abstract class NodeMatcher {
44
abstract match(node: ASTNode): boolean;
55
}
6-
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './node-visitor';
22
export * from './resource-inline';
33
export * from './template-strip-visitor';
4+
export * from './template-recover-visitor';
45

app-shell/src/experimental/shell-parser/node-visitor/node-visitor.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ export abstract class NodeVisitor {
1616
} else {
1717
return null;
1818
}
19-
})
19+
});
2020
}
21-
2221
}
23-

app-shell/src/experimental/shell-parser/node-visitor/resource-inline/inline-style-resource-inline-visitor.spec.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import {
2-
inject
3-
} from '@angular/core/testing';
4-
51
import {ASTNode} from '../../ast';
62
import {MockWorkerScope, MockResponse} from '../../testing';
73
import {InlineStyleResourceInlineVisitor} from './';

app-shell/src/experimental/shell-parser/node-visitor/resource-inline/inline-style-resource-inline-visitor.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import {ASTNode, ASTAttribute} from '../../ast';
22
import {ResourceInlineVisitor} from './resource-inline-visitor';
3-
import {WorkerScope} from '../../context';
4-
5-
const URL_REGEXP = /:\s+url\(['"]?(.*?)['"]?\)/gmi;
63

74
export class InlineStyleResourceInlineVisitor extends ResourceInlineVisitor {
85

app-shell/src/experimental/shell-parser/node-visitor/resource-inline/resource-inline-visitor.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {ASTNode} from '../../ast';
21
import {NodeVisitor} from '../node-visitor';
32
import {WorkerScope} from '../../context';
43

@@ -13,8 +12,7 @@ export abstract class ResourceInlineVisitor extends NodeVisitor {
1312
inlineAssets(style: string) {
1413
let urls = this.getImagesUrls(style);
1514
urls = urls.filter((url: string, idx: number) => urls.indexOf(url) === idx);
16-
return this.processInline(urls, style)
17-
.then((content: string) => content);
15+
return this.processInline(urls, style);
1816
}
1917

2018
protected getImagesUrls(styles: string): string[] {
@@ -55,6 +53,4 @@ export abstract class ResourceInlineVisitor extends NodeVisitor {
5553
img ? content.replace(new RegExp(urls[idx], 'g'), img) : content, styles);
5654
});
5755
}
58-
5956
}
60-

app-shell/src/experimental/shell-parser/node-visitor/resource-inline/stylesheet-resource-inline-visitor.spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import {
2-
inject
3-
} from '@angular/core/testing';
4-
51
import {ASTNode} from '../../ast';
62
import {MockWorkerScope, MockResponse} from '../../testing';
73
import {StylesheetResourceInlineVisitor} from './';
@@ -193,4 +189,3 @@ describe('ResourceInlineVisitor', () => {
193189
});
194190

195191
});
196-

app-shell/src/experimental/shell-parser/node-visitor/resource-inline/stylesheet-resource-inline-visitor.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,4 @@ export class StylesheetResourceInlineVisitor extends ResourceInlineVisitor {
1717
}
1818
return Promise.resolve(node);
1919
}
20-
2120
}
22-
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {ASTNode} from '../ast';
2+
import {TemplateRecoverVisitor} from './';
3+
import {Parse5TemplateParser} from '../template-parser';
4+
5+
describe('TemplateRecoverVisitor', () => {
6+
7+
let astRoot: ASTNode;
8+
let nestedNode: ASTNode;
9+
let differentComments: ASTNode;
10+
11+
beforeEach(() => {
12+
astRoot = {
13+
nodeName: '#comment',
14+
attrs: null,
15+
data: 'shellRender(<div>Hello world!</div>)'
16+
};
17+
const comment2: ASTNode = {
18+
nodeName: '#comment',
19+
attrs: null,
20+
data: 'shellRender(<div>)<span>Test</span></div>)'
21+
};
22+
const span: ASTNode = {
23+
nodeName: 'span',
24+
attrs: null,
25+
childNodes: [comment2]
26+
};
27+
comment2.parentNode = span;
28+
const section: ASTNode = {
29+
nodeName: 'section',
30+
attrs: null
31+
};
32+
const comment1: ASTNode = {
33+
nodeName: '#comment',
34+
attrs: null,
35+
data: 'shellRender(bar)'
36+
};
37+
nestedNode = {
38+
childNodes: [span, section, comment1],
39+
attrs: null,
40+
nodeName: 'div'
41+
};
42+
span.parentNode = nestedNode;
43+
comment1.parentNode = nestedNode;
44+
section.parentNode = nestedNode;
45+
46+
const nonMatchingComment1: ASTNode = {
47+
nodeName: '#comment',
48+
attrs: null,
49+
data: 'bindings {}'
50+
};
51+
const nonMatchingComment2: ASTNode = {
52+
nodeName: '#comment',
53+
attrs: null,
54+
data: 'shellRender(test)'
55+
};
56+
const matchingComment: ASTNode = {
57+
nodeName: '#comment',
58+
attrs: null,
59+
data: 'shellNoRender(<div></div>)'
60+
};
61+
differentComments = {
62+
nodeName: 'div',
63+
attrs: null,
64+
childNodes: [nonMatchingComment1, matchingComment, nonMatchingComment2]
65+
};
66+
nonMatchingComment1.parentNode = differentComments;
67+
nonMatchingComment2.parentNode = differentComments;
68+
matchingComment.parentNode = differentComments;
69+
});
70+
71+
it('should process top-level comments', (done: any) => {
72+
const visitor = new TemplateRecoverVisitor('shellRender', new Parse5TemplateParser());
73+
visitor.visit(astRoot)
74+
.then((node: ASTNode) => {
75+
expect(node.nodeName).toBe('div');
76+
expect(node.childNodes.length).toBe(1);
77+
expect(node.childNodes[0].value).toBe('Hello world!');
78+
done();
79+
});
80+
});
81+
82+
it('should process nested nodes', (done: any) => {
83+
const visitor = new TemplateRecoverVisitor('shellRender', new Parse5TemplateParser());
84+
visitor.visit(nestedNode)
85+
.then((node: ASTNode) => {
86+
const span = nestedNode.childNodes[0];
87+
expect(span.childNodes.length).toBe(1);
88+
expect(span.childNodes[0].nodeName).toBe('div');
89+
expect(span.childNodes[0].childNodes.length).toBe(2);
90+
expect(span.childNodes[0].childNodes[0].value).toBe(')');
91+
expect(span.childNodes[0].childNodes[1].nodeName).toBe('span');
92+
expect(node.childNodes[2].nodeName).toBe('#text');
93+
expect(node.childNodes[2].value).toBe('bar');
94+
done();
95+
});
96+
});
97+
98+
it('should process only nodes with appropriate marker', (done: any) => {
99+
const visitor = new TemplateRecoverVisitor('shellNoRender', new Parse5TemplateParser());
100+
visitor.visit(differentComments)
101+
.then((node: ASTNode) => {
102+
expect(node.childNodes[0].nodeName).toBe('#comment');
103+
expect(node.childNodes[0].data).toBe('bindings {}');
104+
expect(node.childNodes[1].nodeName).toBe('div');
105+
expect(node.childNodes[2].nodeName).toBe('#comment');
106+
expect(node.childNodes[2].data).toBe('shellRender(test)');
107+
done();
108+
});
109+
});
110+
111+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {ASTNode} from '../ast';
2+
import {TemplateParser} from '../template-parser';
3+
import {NodeVisitor} from './node-visitor';
4+
5+
export class TemplateRecoverVisitor extends NodeVisitor {
6+
7+
constructor(private marker: string, private parser: TemplateParser) {
8+
super();
9+
}
10+
11+
process(node: ASTNode) {
12+
const regexp = new RegExp(`^${this.marker}\\(\\s*([\\s\\S]*)\\)$`);
13+
if (node.nodeName === '#comment' && regexp.test(node.data)) {
14+
const template = node.data.match(regexp)[1];
15+
// Returns a #document-fragment node with multiple childs.
16+
// The regular expression above should strip all the whitespace before
17+
// the HTML fragment so once the parser parses the HTML fragment,
18+
// the first should should be our target node.
19+
const replacement = this.parser.parseFragment(template).childNodes.shift();
20+
if (node.parentNode) {
21+
const commentIdx = node.parentNode.childNodes.indexOf(node);
22+
node.parentNode.childNodes[commentIdx] = replacement;
23+
}
24+
return Promise.resolve(replacement);
25+
}
26+
return Promise.resolve(node);
27+
}
28+
29+
}

app-shell/src/experimental/shell-parser/node-visitor/template-strip-visitor.spec.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import {
2-
inject
3-
} from '@angular/core/testing';
4-
51
import {ASTNode} from '../ast';
62
import {cssNodeMatcherFactory} from '../node-matcher';
7-
import {MockWorkerScope, MockResponse} from '../testing';
83
import {TemplateStripVisitor} from './';
94

105
describe('TemplateStripVisitor', () => {
@@ -69,6 +64,5 @@ describe('TemplateStripVisitor', () => {
6964
done();
7065
});
7166
});
72-
7367
});
7468

0 commit comments

Comments
 (0)