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.
This commit is contained in:
Vikram Subramanian
2017-08-08 02:17:40 -07:00
committed by Jason Aden
parent 30d53a8942
commit 2f2d5f35bd
21 changed files with 280 additions and 914 deletions

View File

@ -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,6 +291,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
createHtmlDocument(): HTMLDocument {
return document.implementation.createHTMLDocument('fakeTitle');
}
getDefaultDocument(): Document { return document; }
getBoundingClientRect(el: Element): any {
try {
return el.getBoundingClientRect();
@ -289,10 +299,10 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
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;
}

View File

@ -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[] {