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

Commit d92257b

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.
1 parent e0775d4 commit d92257b

22 files changed

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

app-shell/src/app/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Type } from '@angular/core';
22
import { ShellRender } from './shell-render.directive';
33
import { ShellNoRender } from './shell-no-render.directive';
44

5+
export * from './app-shell-providers';
6+
export * from './template-visibility-strategy';
57
export * from './is-prerender.service';
68
export * from './shell-no-render.directive';
79
export * from './shell-render.directive';
@@ -11,3 +13,4 @@ export const APP_SHELL_DIRECTIVES: Type[] = [
1113
ShellRender,
1214
ShellNoRender
1315
];
16+

app-shell/src/app/is-prerender.service.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import {
66
inject
77
} from '@angular/core/testing';
88
import {
9-
APP_SHELL_BUILD_PROVIDERS,
10-
APP_SHELL_RUNTIME_PROVIDERS,
119
IS_PRERENDER
1210
} from './is-prerender.service';
1311

12+
import {
13+
APP_SHELL_BUILD_PROVIDERS,
14+
APP_SHELL_RUNTIME_PROVIDERS,
15+
} from './app-shell-providers';
16+
1417
describe('IsPrerender Service', () => {
1518
describe('prerender', () => {
1619
beforeEachProviders(() => [APP_SHELL_BUILD_PROVIDERS]);
@@ -31,6 +34,5 @@ describe('IsPrerender Service', () => {
3134
}));
3235
});
3336

34-
35-
3637
});
38+

app-shell/src/app/is-prerender.service.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,3 @@ import {OpaqueToken, provide, Provider} from '@angular/core';
22

33
export const IS_PRERENDER = new OpaqueToken('IsPrerender');
44

5-
export const APP_SHELL_RUNTIME_PROVIDERS: Provider[] = [
6-
provide(IS_PRERENDER, {
7-
useValue: false
8-
})
9-
];
10-
11-
export const APP_SHELL_BUILD_PROVIDERS: Provider[] = [
12-
provide(IS_PRERENDER, {
13-
useValue: true
14-
})
15-
];

app-shell/src/app/shell-no-render.directive.spec.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ShellNoRender } from './shell-no-render.directive';
1212
import {
1313
APP_SHELL_BUILD_PROVIDERS,
1414
APP_SHELL_RUNTIME_PROVIDERS
15-
} from './is-prerender.service';
15+
} from './app-shell-providers';
1616

1717
@Component({
1818
selector: 'test-component',
@@ -28,8 +28,10 @@ describe('ShellNoRender Directive', () => {
2828
it('should NOT render the element', async(inject([TestComponentBuilder], (tcb:TestComponentBuilder) => {
2929
return tcb.createAsync(TestComponent).then((fixture: ComponentFixture<TestComponent>) => {
3030
fixture.detectChanges();
31-
expect(fixture.debugElement.childNodes.length).toBe(1);
32-
expect(fixture.debugElement.childNodes[0].nativeNode.textContent).toBe('template bindings={}');
31+
const childNodes = fixture.debugElement.nativeElement.childNodes;
32+
expect(childNodes.length).toBe(2);
33+
expect(childNodes[0].textContent).toBe('template bindings={}');
34+
expect(childNodes[1].textContent).toBe('shellNoRender(<div>Rendered</div>)');
3335
});
3436
})));
3537
});
@@ -46,5 +48,15 @@ describe('ShellNoRender Directive', () => {
4648
expect(fixture.debugElement.childNodes[1].nativeNode.textContent).toBe('Rendered');
4749
});
4850
})));
51+
52+
it('should set "shellNoRender" attribute', async(inject([TestComponentBuilder], (tcb:TestComponentBuilder) => {
53+
return tcb.createAsync(TestComponent).then((fixture: ComponentFixture<TestComponent>) => {
54+
fixture.detectChanges();
55+
expect(fixture.debugElement.childNodes.length).toBe(2);
56+
expect(fixture.debugElement.childNodes[1].nativeNode.getAttribute('shellNoRender')).toBe('');
57+
});
58+
})));
59+
4960
});
5061
});
62+
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1-
import { Directive, Inject, OnInit, ViewContainerRef, TemplateRef } from '@angular/core';
1+
import { Directive, Inject, OnInit, ViewContainerRef, TemplateRef, Renderer, AfterViewInit } from '@angular/core';
2+
import { getDOM } from '@angular/platform-browser/src/dom/dom_adapter';
23

3-
import {IS_PRERENDER} from './is-prerender.service';
4+
import { TemplateVisibilityStrategy } from './template-visibility-strategy';
5+
import { IS_PRERENDER } from './is-prerender.service';
46

5-
@Directive({selector: '[shellNoRender]'})
6-
export class ShellNoRender implements OnInit {
7+
const MARKER = 'shellNoRender';
8+
9+
@Directive({selector: `[${MARKER}]`})
10+
export class ShellNoRender implements OnInit, AfterViewInit {
711
constructor(
812
private _viewContainer: ViewContainerRef,
913
private _templateRef: TemplateRef<Object>,
14+
private _templateVisibilityStrategy: TemplateVisibilityStrategy,
1015
@Inject(IS_PRERENDER) private _isPrerender: boolean) {
1116
}
1217

1318
ngOnInit () {
19+
const view = this._viewContainer.createEmbeddedView(this._templateRef);
20+
this._templateVisibilityStrategy.setRootNodes(MARKER, view.rootNodes.pop());
21+
}
22+
23+
ngAfterViewInit() {
1424
if (this._isPrerender) {
25+
this._templateVisibilityStrategy.hide(MARKER);
1526
this._viewContainer.clear();
1627
} else {
17-
this._viewContainer.createEmbeddedView(this._templateRef);
28+
this._templateVisibilityStrategy.show(MARKER);
1829
}
1930
}
2031
}
32+

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export interface ASTNode {
99
parentNode?: ASTNode;
1010
nodeName: string;
1111
value?: string;
12+
data?: string;
1213
}
1314

app-shell/src/app/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
};
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/app/shell-parser/node-visitor/resource-inline/resource-inline-visitor.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ export abstract class ResourceInlineVisitor extends NodeVisitor {
1313
inlineAssets(style: string) {
1414
let urls = this.getImagesUrls(style);
1515
urls = urls.filter((url: string, idx: number) => urls.indexOf(url) === idx);
16-
return this.processInline(urls, style)
17-
.then((content: string) => content);
16+
return this.processInline(urls, style);
1817
}
1918

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

app-shell/src/app/shell-parser/shell-parser-factory.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import {Parse5TemplateParser} from './template-parser';
22
import {ShellParserImpl} from './shell-parser';
33
import {cssNodeMatcherFactory} from './node-matcher';
4-
import {StylesheetResourceInlineVisitor, InlineStyleResourceInlineVisitor, TemplateStripVisitor, NodeVisitor} from './node-visitor';
4+
import {
5+
StylesheetResourceInlineVisitor,
6+
InlineStyleResourceInlineVisitor,
7+
TemplateStripVisitor,
8+
NodeVisitor,
9+
TemplateRecoverVisitor
10+
} from './node-visitor';
511
import {BrowserWorkerScope} from './context';
612
import {ShellParserConfig, SHELL_PARSER_DEFAULT_CONFIG} from './config';
713

@@ -13,8 +19,9 @@ export const shellParserFactory = (config: ShellParserConfig = {}) => {
1319
const parserConfig = normalizeConfig(config);
1420
const scope = new BrowserWorkerScope();
1521
const visitors: NodeVisitor[] = [];
22+
visitors.push(new TemplateRecoverVisitor(parserConfig.RENDER_MARKER, new Parse5TemplateParser()));
23+
visitors.push(new TemplateStripVisitor(cssNodeMatcherFactory(parserConfig.NO_RENDER_CSS_SELECTOR)));
1624
if (config.INLINE_IMAGES) {
17-
visitors.push(new TemplateStripVisitor(cssNodeMatcherFactory(parserConfig.NO_RENDER_CSS_SELECTOR)));
1825
visitors.push(new StylesheetResourceInlineVisitor(scope, config.INLINE_IMAGES));
1926
visitors.push(new InlineStyleResourceInlineVisitor(scope, config.INLINE_IMAGES));
2027
}

0 commit comments

Comments
 (0)