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

feat(AppShell): shellRender and shellNoRender attributes and allow different element stripping strategies #81

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app-shell/src/app/app-shell-providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Provider} from '@angular/core';

import { IS_PRERENDER } from './is-prerender.service';
import { TemplateVisibilityStrategy } from './template-visibility-strategy';
import { TemplateCommentStrategy } from './template-comment-strategy';

export const APP_SHELL_RUNTIME_PROVIDERS: Provider[] = [
{
provide: IS_PRERENDER,
useValue: false
},
{
provide: TemplateVisibilityStrategy,
useClass: TemplateCommentStrategy
}
];

export const APP_SHELL_BUILD_PROVIDERS: Provider[] = [
{
provide: IS_PRERENDER,
useValue: true
},
{
provide: TemplateVisibilityStrategy,
useClass: TemplateCommentStrategy
}
];
1 change: 1 addition & 0 deletions app-shell/src/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './module';
export * from './prerender';
export * from './shell';

2 changes: 2 additions & 0 deletions app-shell/src/app/shell.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function () {
expect(fixture.debugElement.childNodes.length).toBe(1);
expect(fixture.debugElement.childNodes[0].nativeNode.data).toBe('template bindings={}');
});

it('should render the element at runtime', () => {
const fixture = TestBed
.configureTestingModule({
Expand Down Expand Up @@ -55,6 +56,7 @@ export default function () {
expect(fixture.debugElement.childNodes[0].nativeNode.data).toBe('template bindings={}');
expect(fixture.debugElement.childNodes[1].nativeNode.name).toBe('div');
});

it('should NOT render the element at runtime', () => {
const fixture = TestBed
.configureTestingModule({
Expand Down
31 changes: 31 additions & 0 deletions app-shell/src/app/template-comment-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { __platform_browser_private__ as _ } from '@angular/platform-browser';

import { TemplateVisibilityStrategy } from './template-visibility-strategy';

@Injectable()
export class TemplateCommentStrategy extends TemplateVisibilityStrategy {
private _hidden: any[] = [];
private _shown: any[] = [];

show(marker: string) {
(this._rootNodes[marker] || [])
.filter((node: any) => this._shown.indexOf(node) < 0)
.forEach((node: any) => {
_.getDOM().setAttribute(node, marker, '');
this._shown.push(node);
});
}

hide(marker: string) {
const DOM = _.getDOM();
(this._rootNodes[marker] || [])
.filter((node: any) => this._hidden.indexOf(node) < 0)
.forEach((node: any) => {
const comment = DOM.createComment(`${marker}(${DOM.getOuterHTML(node)})`);
const parentNode = DOM.parentElement(node);
DOM.replaceChild(parentNode, comment, node);
this._hidden.push(node);
});
}
}
15 changes: 15 additions & 0 deletions app-shell/src/app/template-visibility-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface DirectiveNodes {
[marker: string]: any[];
};

export abstract class TemplateVisibilityStrategy {
protected _rootNodes: DirectiveNodes = {};

abstract show(marker: string): void;
abstract hide(marker: string): void;

setRootNodes(marker: string, ...rootNodes: any[]) {
const nodes = this._rootNodes[marker] || [];
this._rootNodes[marker] = nodes.concat(rootNodes);
}
}
2 changes: 1 addition & 1 deletion app-shell/src/experimental/shell-parser/ast/ast-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export interface ASTNode {
parentNode?: ASTNode;
nodeName: string;
value?: string;
data?: string;
}

1 change: 0 additions & 1 deletion app-shell/src/experimental/shell-parser/ast/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './ast-node';

3 changes: 3 additions & 0 deletions app-shell/src/experimental/shell-parser/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type RouteDefinition = string;
const SHELL_PARSER_CACHE_NAME = 'mobile-toolkit:app-shell';
const APP_SHELL_URL = './app_shell.html';
const NO_RENDER_CSS_SELECTOR = '[shellNoRender]';
const RENDER_MARKER = 'shellRender';
const ROUTE_DEFINITIONS: RouteDefinition[] = [];
const INLINE_IMAGES: string[] = ['png', 'svg', 'jpg'];

Expand All @@ -13,6 +14,7 @@ export interface ShellParserConfig {
APP_SHELL_URL?: string;
SHELL_PARSER_CACHE_NAME?: string;
NO_RENDER_CSS_SELECTOR?: string;
RENDER_MARKER?: string;
ROUTE_DEFINITIONS?: RouteDefinition[];
INLINE_IMAGES?: string[];
}
Expand All @@ -21,6 +23,7 @@ export const SHELL_PARSER_DEFAULT_CONFIG: ShellParserConfig = {
SHELL_PARSER_CACHE_NAME,
APP_SHELL_URL,
NO_RENDER_CSS_SELECTOR,
RENDER_MARKER,
ROUTE_DEFINITIONS,
INLINE_IMAGES
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import {
inject
} from '@angular/core/testing';
import { ASTNode } from '../ast';
import { CssSelector } from './css-selector';
import { CssNodeMatcher } from './css-node-matcher';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import {
inject
} from '@angular/core/testing';
import { CssSelector } from './css-selector';

describe('CssSelector', () => {
Expand Down Expand Up @@ -52,7 +49,5 @@ describe('CssSelector', () => {
expect(result.elementId).toBe('baz');
expect(result.classNames).toEqual(['foo', 'qux']);
});

});
});

Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './css-selector';

Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ import {ASTNode} from '../ast';
export abstract class NodeMatcher {
abstract match(node: ASTNode): boolean;
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './node-visitor';
export * from './resource-inline';
export * from './template-strip-visitor';
export * from './template-recover-visitor';

Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export abstract class NodeVisitor {
} else {
return null;
}
})
});
}

}

Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import {
inject
} from '@angular/core/testing';

import {ASTNode} from '../../ast';
import {MockWorkerScope, MockResponse} from '../../testing';
import {InlineStyleResourceInlineVisitor} from './';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import {ASTNode, ASTAttribute} from '../../ast';
import {ResourceInlineVisitor} from './resource-inline-visitor';
import {WorkerScope} from '../../context';

const URL_REGEXP = /:\s+url\(['"]?(.*?)['"]?\)/gmi;

export class InlineStyleResourceInlineVisitor extends ResourceInlineVisitor {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {ASTNode} from '../../ast';
import {NodeVisitor} from '../node-visitor';
import {WorkerScope} from '../../context';

Expand All @@ -13,8 +12,7 @@ export abstract class ResourceInlineVisitor extends NodeVisitor {
inlineAssets(style: string) {
let urls = this.getImagesUrls(style);
urls = urls.filter((url: string, idx: number) => urls.indexOf(url) === idx);
return this.processInline(urls, style)
.then((content: string) => content);
return this.processInline(urls, style);
}

protected getImagesUrls(styles: string): string[] {
Expand Down Expand Up @@ -55,6 +53,4 @@ export abstract class ResourceInlineVisitor extends NodeVisitor {
img ? content.replace(new RegExp(urls[idx], 'g'), img) : content, styles);
});
}

}

Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import {
inject
} from '@angular/core/testing';

import {ASTNode} from '../../ast';
import {MockWorkerScope, MockResponse} from '../../testing';
import {StylesheetResourceInlineVisitor} from './';
Expand Down Expand Up @@ -193,4 +189,3 @@ describe('ResourceInlineVisitor', () => {
});

});

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ export class StylesheetResourceInlineVisitor extends ResourceInlineVisitor {
}
return Promise.resolve(node);
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {ASTNode} from '../ast';
import {TemplateRecoverVisitor} from './';
import {Parse5TemplateParser} from '../template-parser';

describe('TemplateRecoverVisitor', () => {

let astRoot: ASTNode;
let nestedNode: ASTNode;
let differentComments: ASTNode;

beforeEach(() => {
astRoot = {
nodeName: '#comment',
attrs: null,
data: 'shellRender(<div>Hello world!</div>)'
};
const comment2: ASTNode = {
nodeName: '#comment',
attrs: null,
data: 'shellRender(<div>)<span>Test</span></div>)'
};
const span: ASTNode = {
nodeName: 'span',
attrs: null,
childNodes: [comment2]
};
comment2.parentNode = span;
const section: ASTNode = {
nodeName: 'section',
attrs: null
};
const comment1: ASTNode = {
nodeName: '#comment',
attrs: null,
data: 'shellRender(bar)'
};
nestedNode = {
childNodes: [span, section, comment1],
attrs: null,
nodeName: 'div'
};
span.parentNode = nestedNode;
comment1.parentNode = nestedNode;
section.parentNode = nestedNode;

const nonMatchingComment1: ASTNode = {
nodeName: '#comment',
attrs: null,
data: 'bindings {}'
};
const nonMatchingComment2: ASTNode = {
nodeName: '#comment',
attrs: null,
data: 'shellRender(test)'
};
const matchingComment: ASTNode = {
nodeName: '#comment',
attrs: null,
data: 'shellNoRender(<div></div>)'
};
differentComments = {
nodeName: 'div',
attrs: null,
childNodes: [nonMatchingComment1, matchingComment, nonMatchingComment2]
};
nonMatchingComment1.parentNode = differentComments;
nonMatchingComment2.parentNode = differentComments;
matchingComment.parentNode = differentComments;
});

it('should process top-level comments', (done: any) => {
const visitor = new TemplateRecoverVisitor('shellRender', new Parse5TemplateParser());
visitor.visit(astRoot)
.then((node: ASTNode) => {
expect(node.nodeName).toBe('div');
expect(node.childNodes.length).toBe(1);
expect(node.childNodes[0].value).toBe('Hello world!');
done();
});
});

it('should process nested nodes', (done: any) => {
const visitor = new TemplateRecoverVisitor('shellRender', new Parse5TemplateParser());
visitor.visit(nestedNode)
.then((node: ASTNode) => {
const span = nestedNode.childNodes[0];
expect(span.childNodes.length).toBe(1);
expect(span.childNodes[0].nodeName).toBe('div');
expect(span.childNodes[0].childNodes.length).toBe(2);
expect(span.childNodes[0].childNodes[0].value).toBe(')');
expect(span.childNodes[0].childNodes[1].nodeName).toBe('span');
expect(node.childNodes[2].nodeName).toBe('#text');
expect(node.childNodes[2].value).toBe('bar');
done();
});
});

it('should process only nodes with appropriate marker', (done: any) => {
const visitor = new TemplateRecoverVisitor('shellNoRender', new Parse5TemplateParser());
visitor.visit(differentComments)
.then((node: ASTNode) => {
expect(node.childNodes[0].nodeName).toBe('#comment');
expect(node.childNodes[0].data).toBe('bindings {}');
expect(node.childNodes[1].nodeName).toBe('div');
expect(node.childNodes[2].nodeName).toBe('#comment');
expect(node.childNodes[2].data).toBe('shellRender(test)');
done();
});
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {ASTNode} from '../ast';
import {TemplateParser} from '../template-parser';
import {NodeVisitor} from './node-visitor';

export class TemplateRecoverVisitor extends NodeVisitor {

constructor(private marker: string, private parser: TemplateParser) {
super();
}

process(node: ASTNode) {
const regexp = new RegExp(`^${this.marker}\\(\\s*([\\s\\S]*)\\)$`);
if (node.nodeName === '#comment' && regexp.test(node.data)) {
const template = node.data.match(regexp)[1];
// Returns a #document-fragment node with multiple childs.
// The regular expression above should strip all the whitespace before
// the HTML fragment so once the parser parses the HTML fragment,
// the first should should be our target node.
const replacement = this.parser.parseFragment(template).childNodes.shift();
if (node.parentNode) {
const commentIdx = node.parentNode.childNodes.indexOf(node);
node.parentNode.childNodes[commentIdx] = replacement;
}
return Promise.resolve(replacement);
}
return Promise.resolve(node);
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {
inject
} from '@angular/core/testing';

import {ASTNode} from '../ast';
import {cssNodeMatcherFactory} from '../node-matcher';
import {MockWorkerScope, MockResponse} from '../testing';
import {TemplateStripVisitor} from './';

describe('TemplateStripVisitor', () => {
Expand Down Expand Up @@ -69,6 +64,5 @@ describe('TemplateStripVisitor', () => {
done();
});
});

});

Loading