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:

committed by
Jason Aden

parent
30d53a8942
commit
2f2d5f35bd
201
packages/platform-server/src/domino_adapter.ts
Normal file
201
packages/platform-server/src/domino_adapter.ts
Normal file
@ -0,0 +1,201 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
const domino = require('domino');
|
||||
|
||||
import {ɵBrowserDomAdapter as BrowserDomAdapter, ɵsetRootDomAdapter as setRootDomAdapter} from '@angular/platform-browser';
|
||||
|
||||
function _notImplemented(methodName: string) {
|
||||
return new Error('This method is not implemented in DominoAdapter: ' + methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a document string to a Document object.
|
||||
*/
|
||||
export function parseDocument(html: string, url = '/') {
|
||||
let window = domino.createWindow(html, url);
|
||||
let doc = window.document;
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a document to string.
|
||||
*/
|
||||
export function serializeDocument(doc: Document): string {
|
||||
return (doc as any).serialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM Adapter for the server platform based on https://github.com/fgnass/domino.
|
||||
*/
|
||||
export class DominoAdapter extends BrowserDomAdapter {
|
||||
static makeCurrent() { setRootDomAdapter(new DominoAdapter()); }
|
||||
|
||||
private static defaultDoc: Document;
|
||||
|
||||
logError(error: string) { console.error(error); }
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
log(error: string) { console.log(error); }
|
||||
|
||||
logGroup(error: string) { console.error(error); }
|
||||
|
||||
logGroupEnd() {}
|
||||
|
||||
supportsDOMEvents(): boolean { return false; }
|
||||
supportsNativeShadowDOM(): boolean { return false; }
|
||||
|
||||
contains(nodeA: any, nodeB: any): boolean {
|
||||
let inner = nodeB;
|
||||
while (inner) {
|
||||
if (inner === nodeA) return true;
|
||||
inner = inner.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
createHtmlDocument(): HTMLDocument {
|
||||
return parseDocument('<html><head><title>fakeTitle</title></head><body></body></html>');
|
||||
}
|
||||
|
||||
getDefaultDocument(): Document {
|
||||
if (!DominoAdapter.defaultDoc) {
|
||||
DominoAdapter.defaultDoc = domino.createDocument();
|
||||
}
|
||||
return DominoAdapter.defaultDoc;
|
||||
}
|
||||
|
||||
createShadowRoot(el: any, doc: Document = document): DocumentFragment {
|
||||
el.shadowRoot = doc.createDocumentFragment();
|
||||
el.shadowRoot.parent = el;
|
||||
return el.shadowRoot;
|
||||
}
|
||||
getShadowRoot(el: any): DocumentFragment { return el.shadowRoot; }
|
||||
|
||||
isTextNode(node: any): boolean { return node.nodeType === DominoAdapter.defaultDoc.TEXT_NODE; }
|
||||
isCommentNode(node: any): boolean {
|
||||
return node.nodeType === DominoAdapter.defaultDoc.COMMENT_NODE;
|
||||
}
|
||||
isElementNode(node: any): boolean {
|
||||
return node ? node.nodeType === DominoAdapter.defaultDoc.ELEMENT_NODE : false;
|
||||
}
|
||||
hasShadowRoot(node: any): boolean { return node.shadowRoot != null; }
|
||||
isShadowRoot(node: any): boolean { return this.getShadowRoot(node) == node; }
|
||||
|
||||
getProperty(el: Element, name: string): any {
|
||||
// Domino tries tp resolve href-s which we do not want. Just return the
|
||||
// atribute value.
|
||||
if (name === 'href') {
|
||||
return this.getAttribute(el, 'href');
|
||||
}
|
||||
return (<any>el)[name];
|
||||
}
|
||||
|
||||
setProperty(el: Element, name: string, value: any) {
|
||||
// Eventhough the server renderer reflects any properties to attributes
|
||||
// map 'href' to atribute just to handle when setProperty is directly called.
|
||||
if (name === 'href') {
|
||||
this.setAttribute(el, 'href', value);
|
||||
}
|
||||
(<any>el)[name] = value;
|
||||
}
|
||||
|
||||
getGlobalEventTarget(doc: Document, target: string): EventTarget|null {
|
||||
if (target === 'window') {
|
||||
return doc.defaultView;
|
||||
}
|
||||
if (target === 'document') {
|
||||
return doc;
|
||||
}
|
||||
if (target === 'body') {
|
||||
return doc.body;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getBaseHref(doc: Document): string {
|
||||
const base = this.querySelector(doc.documentElement, 'base');
|
||||
let href = '';
|
||||
if (base) {
|
||||
href = this.getHref(base);
|
||||
}
|
||||
// TODO(alxhub): Need relative path logic from BrowserDomAdapter here?
|
||||
return href;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_readStyleAttribute(element: any) {
|
||||
const styleMap = {};
|
||||
const styleAttribute = element.getAttribute('style');
|
||||
if (styleAttribute) {
|
||||
const styleList = styleAttribute.split(/;+/g);
|
||||
for (let i = 0; i < styleList.length; i++) {
|
||||
if (styleList[i].length > 0) {
|
||||
const style = styleList[i] as string;
|
||||
const colon = style.indexOf(':');
|
||||
if (colon === -1) {
|
||||
throw new Error(`Invalid CSS style: ${style}`);
|
||||
}
|
||||
(styleMap as any)[style.substr(0, colon).trim()] = style.substr(colon + 1).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return styleMap;
|
||||
}
|
||||
/** @internal */
|
||||
_writeStyleAttribute(element: any, styleMap: any) {
|
||||
let styleAttrValue = '';
|
||||
for (const key in styleMap) {
|
||||
const newValue = styleMap[key];
|
||||
if (newValue) {
|
||||
styleAttrValue += key + ':' + styleMap[key] + ';';
|
||||
}
|
||||
}
|
||||
element.setAttribute('style', styleAttrValue);
|
||||
}
|
||||
setStyle(element: any, styleName: string, styleValue?: string|null) {
|
||||
const styleMap = this._readStyleAttribute(element);
|
||||
(styleMap as any)[styleName] = styleValue;
|
||||
this._writeStyleAttribute(element, styleMap);
|
||||
}
|
||||
removeStyle(element: any, styleName: string) { this.setStyle(element, styleName, null); }
|
||||
getStyle(element: any, styleName: string): string {
|
||||
const styleMap = this._readStyleAttribute(element);
|
||||
return styleMap.hasOwnProperty(styleName) ? (styleMap as any)[styleName] : '';
|
||||
}
|
||||
hasStyle(element: any, styleName: string, styleValue?: string): boolean {
|
||||
const value = this.getStyle(element, styleName) || '';
|
||||
return styleValue ? value == styleValue : value.length > 0;
|
||||
}
|
||||
|
||||
dispatchEvent(el: Node, evt: any) {
|
||||
el.dispatchEvent(evt);
|
||||
|
||||
// Dispatch the event to the window also.
|
||||
const doc = el.ownerDocument || el;
|
||||
const win = (doc as any).defaultView;
|
||||
if (win) {
|
||||
win.dispatchEvent(evt);
|
||||
}
|
||||
}
|
||||
|
||||
getHistory(): History { throw _notImplemented('getHistory'); }
|
||||
getLocation(): Location { throw _notImplemented('getLocation'); }
|
||||
getUserAgent(): string { return 'Fake user agent'; }
|
||||
|
||||
supportsWebAnimation(): boolean { return false; }
|
||||
performanceNow(): number { return Date.now(); }
|
||||
getAnimationPrefix(): string { return ''; }
|
||||
getTransitionEnd(): string { return 'transitionend'; }
|
||||
supportsAnimation(): boolean { return true; }
|
||||
|
||||
getDistributedNodes(el: any): Node[] { throw _notImplemented('getDistributedNodes'); }
|
||||
|
||||
supportsCookies(): boolean { return false; }
|
||||
getCookie(name: string): string { throw _notImplemented('getCookie'); }
|
||||
setCookie(name: string, value: string) { throw _notImplemented('setCookie'); }
|
||||
}
|
Reference in New Issue
Block a user