Permalink
Browse files

feat(platform-server): provide a DOM implementation on the server

Fixes #14638

Uses Domino - https://github.com/fgnass/domino and removes dependency on
Parse5.

The DOCUMENT and nativeElement were never typed earlier and were
different on the browser(DOM nodes) and the server(Parse5 nodes). With
this change, platform-server also exposes a DOCUMENT and nativeElement
that is closer to the client. If you were relying on nativeElement on
the server, you would have to change your code to use the DOM API now
instead of Parse5 AST API.

Removes the need to add services for each and every Document
manipulation like Title/Meta etc.

This does *not* provide a global variable 'document' or 'window' on the
server. You still have to inject DOCUMENT to get the document backing
the current platform server instance.
  • Loading branch information...
vikerman authored and jasonaden committed Aug 8, 2017
1 parent 30d53a8 commit 2f2d5f35bd086abeafceabdd5ff9430221b1917f
View
@@ -2352,6 +2352,9 @@
"domhandler": {
"version": "2.3.0"
},
"domino": {
"version": "1.0.29"
},
"domutils": {
"version": "1.5.1"
},
@@ -6162,14 +6165,6 @@
"parse-json": {
"version": "2.2.0"
},
"parse5": {
"version": "3.0.1",
"dependencies": {
"@types/node": {
"version": "6.0.63"
}
}
},
"parsejson": {
"version": "0.0.1"
},
View

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
View
@@ -56,6 +56,7 @@
"cors": "^2.7.1",
"dgeni": "^0.4.2",
"dgeni-packages": "^0.16.5",
"domino": "^1.0.29",
"entities": "^1.1.1",
"firebase-tools": "^3.9.2",
"firefox-profile": "^0.3.4",
@@ -81,7 +82,6 @@
"minimist": "^1.2.0",
"nan": "^2.4.0",
"node-uuid": "1.4.x",
"parse5": "^3.0.1",
"protractor": "^4.0.14",
"react": "^0.14.0",
"rewire": "^2.3.3",
@@ -84,8 +84,8 @@ describe('template codegen output', () => {
it('should support i18n for content tags', () => {
const containerElement = createComponent(BasicComp).nativeElement;
const pElement = containerElement.children.find((c: any) => c.name == 'p');
const pText = pElement.children.map((c: any) => c.data).join('').trim();
const pElement = containerElement.querySelector('p');
const pText = pElement.textContent;
expect(pText).toBe('tervetuloa');
});
@@ -49,7 +49,7 @@ describe('NgModule', () => {
// https://github.com/angular/angular/issues/15221
const fixture = createComponent(ComponentUsingFlatModule);
const bundleComp = fixture.nativeElement.children;
expect(bundleComp[0].children[0].children[0].data).toEqual('flat module component');
expect(bundleComp[0].children[0].textContent).toEqual('flat module component');
});
});
@@ -58,8 +58,8 @@ describe('NgModule', () => {
it('should support third party entryComponents components', () => {
const fixture = createComponent(ComponentUsingThirdParty);
const thirdPComps = fixture.nativeElement.children;
expect(thirdPComps[0].children[0].children[0].data).toEqual('3rdP-component');
expect(thirdPComps[1].children[0].children[0].data).toEqual(`other-3rdP-component
expect(thirdPComps[0].children[0].textContent).toEqual('3rdP-component');
expect(thirdPComps[1].children[0].textContent).toEqual(`other-3rdP-component
multi-lines`);
});
@@ -129,12 +129,12 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
}
dispatchEvent(el: Node, evt: any) { el.dispatchEvent(evt); }
createMouseEvent(eventType: string): MouseEvent {
const evt: MouseEvent = document.createEvent('MouseEvent');
const evt: MouseEvent = this.getDefaultDocument().createEvent('MouseEvent');
evt.initEvent(eventType, true, true);
return evt;
}
createEvent(eventType: any): Event {
const evt: Event = document.createEvent('Event');
const evt: Event = this.getDefaultDocument().createEvent('Event');
evt.initEvent(eventType, true, true);
return evt;
}
@@ -147,7 +147,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
}
getInnerHTML(el: HTMLElement): string { return el.innerHTML; }
getTemplateContent(el: Node): Node|null {
return 'content' in el && el instanceof HTMLTemplateElement ? el.content : null;
return 'content' in el && this.isTemplateElement(el) ? (<any>el).content : null;
}
getOuterHTML(el: HTMLElement): string { return el.outerHTML; }
nodeName(node: Node): string { return node.nodeName; }
@@ -198,25 +198,34 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
setValue(el: any, value: string) { el.value = value; }
getChecked(el: any): boolean { return el.checked; }
setChecked(el: any, value: boolean) { el.checked = value; }
createComment(text: string): Comment { return document.createComment(text); }
createComment(text: string): Comment { return this.getDefaultDocument().createComment(text); }
createTemplate(html: any): HTMLElement {
const t = document.createElement('template');
const t = this.getDefaultDocument().createElement('template');
t.innerHTML = html;
return t;
}
createElement(tagName: string, doc = document): HTMLElement { return doc.createElement(tagName); }
createElementNS(ns: string, tagName: string, doc = document): Element {
createElement(tagName: string, doc?: Document): HTMLElement {
doc = doc || this.getDefaultDocument();
return doc.createElement(tagName);
}
createElementNS(ns: string, tagName: string, doc?: Document): Element {
doc = doc || this.getDefaultDocument();
return doc.createElementNS(ns, tagName);
}
createTextNode(text: string, doc = document): Text { return doc.createTextNode(text); }
createScriptTag(attrName: string, attrValue: string, doc = document): HTMLScriptElement {
createTextNode(text: string, doc?: Document): Text {
doc = doc || this.getDefaultDocument();
return doc.createTextNode(text);
}
createScriptTag(attrName: string, attrValue: string, doc?: Document): HTMLScriptElement {
doc = doc || this.getDefaultDocument();
const el = <HTMLScriptElement>doc.createElement('SCRIPT');
el.setAttribute(attrName, attrValue);
return el;
}
createStyleElement(css: string, doc = document): HTMLStyleElement {
createStyleElement(css: string, doc?: Document): HTMLStyleElement {
doc = doc || this.getDefaultDocument();
const style = <HTMLStyleElement>doc.createElement('style');
this.appendChild(style, this.createTextNode(css));
this.appendChild(style, this.createTextNode(css, doc));
return style;
}
createShadowRoot(el: HTMLElement): DocumentFragment { return (<any>el).createShadowRoot(); }
@@ -253,7 +262,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
const res = new Map<string, string>();
const elAttrs = element.attributes;
for (let i = 0; i < elAttrs.length; i++) {
const attrib = elAttrs[i];
const attrib = elAttrs.item(i);
res.set(attrib.name, attrib.value);
}
return res;
@@ -282,17 +291,18 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
createHtmlDocument(): HTMLDocument {
return document.implementation.createHTMLDocument('fakeTitle');
}
getDefaultDocument(): Document { return document; }
getBoundingClientRect(el: Element): any {
try {
return el.getBoundingClientRect();
} catch (e) {
return {top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0};
}
}
getTitle(doc: Document): string { return document.title; }
setTitle(doc: Document, newTitle: string) { document.title = newTitle || ''; }
getTitle(doc: Document): string { return doc.title; }
setTitle(doc: Document, newTitle: string) { doc.title = newTitle || ''; }
elementMatches(n: any, selector: string): boolean {
if (n instanceof HTMLElement) {
if (this.isElementNode(n)) {
return n.matches && n.matches(selector) ||
n.msMatchesSelector && n.msMatchesSelector(selector) ||
n.webkitMatchesSelector && n.webkitMatchesSelector(selector);
@@ -301,7 +311,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
return false;
}
isTemplateElement(el: Node): boolean {
return el instanceof HTMLElement && el.nodeName == 'TEMPLATE';
return this.isElementNode(el) && el.nodeName === 'TEMPLATE';
}
isTextNode(node: Node): boolean { return node.nodeType === Node.TEXT_NODE; }
isCommentNode(node: Node): boolean { return node.nodeType === Node.COMMENT_NODE; }
@@ -312,7 +322,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
isShadowRoot(node: any): boolean { return node instanceof DocumentFragment; }
importIntoDoc(node: Node): any { return document.importNode(this.templateAwareRoot(node), true); }
adoptNode(node: Node): any { return document.adoptNode(node); }
getHref(el: Element): string { return (<any>el).href; }
getHref(el: Element): string { return el.getAttribute('href') !; }
getEventKey(event: any): string {
let key = event.key;
@@ -342,10 +352,10 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
return window;
}
if (target === 'document') {
return document;
return doc;
}
if (target === 'body') {
return document.body;
return doc.body;
}
return null;
}
@@ -56,7 +56,7 @@ export class Meta {
getTag(attrSelector: string): HTMLMetaElement|null {
if (!attrSelector) return null;
return this._dom.querySelector(this._doc, `meta[${attrSelector}]`);
return this._dom.querySelector(this._doc, `meta[${attrSelector}]`) || null;
}
getTags(attrSelector: string): HTMLMetaElement[] {
@@ -125,6 +125,7 @@ export abstract class DomAdapter {
abstract removeAttributeNS(element: any, ns: string, attribute: string): any;
abstract templateAwareRoot(el: any): any;
abstract createHtmlDocument(): HTMLDocument;
abstract getDefaultDocument(): Document;
abstract getBoundingClientRect(el: any): any;
abstract getTitle(doc: Document): string;
abstract setTitle(doc: Document, newTitle: string): any;
@@ -14,11 +14,13 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
export function main() {
describe('Meta service', () => {
const doc = getDOM().createHtmlDocument();
const metaService = new Meta(doc);
let doc: Document;
let metaService: Meta;
let defaultMeta: HTMLMetaElement;
beforeEach(() => {
doc = getDOM().createHtmlDocument();
metaService = new Meta(doc);
defaultMeta = getDOM().createElement('meta', doc) as HTMLMetaElement;
getDOM().setAttribute(defaultMeta, 'property', 'fb:app_id');
getDOM().setAttribute(defaultMeta, 'content', '123456789');
@@ -14,9 +14,15 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
export function main() {
describe('title service', () => {
const doc = getDOM().createHtmlDocument();
const initialTitle = getDOM().getTitle(doc);
const titleService = new Title(doc);
let doc: Document;
let initialTitle: string;
let titleService: Title;
beforeEach(() => {
doc = getDOM().createHtmlDocument();
initialTitle = getDOM().getTitle(doc);
titleService = new Title(doc);
});
afterEach(() => { getDOM().setTitle(doc, initialTitle); });
@@ -18,7 +18,7 @@
},
"dependencies": {
"tslib": "^1.7.1",
"parse5": "^3.0.1",
"domino": "^1.0.29",
"xhr2": "^0.1.4"
},
"repository": {
Oops, something went wrong.

3 comments on commit 2f2d5f3

@Gorniv

This comment has been minimized.

Show comment
Hide comment
@Gorniv

Gorniv Nov 1, 2017

How can I use this for global variable 'document' or 'window'? Or another demo of using this?

Gorniv replied Nov 1, 2017

How can I use this for global variable 'document' or 'window'? Or another demo of using this?

@Toxicable

This comment has been minimized.

Show comment
Hide comment
@Toxicable

Toxicable Nov 1, 2017

Contributor

window dosen't exist on the server
you can however inject DOCUMENT (from common) and you can use that as you'd use document

Contributor

Toxicable replied Nov 1, 2017

window dosen't exist on the server
you can however inject DOCUMENT (from common) and you can use that as you'd use document

@Gorniv

This comment has been minimized.

Show comment
Hide comment
Please sign in to comment.