Skip to content

Commit

Permalink
feat(platform-server): provide a DOM implementation on the server
Browse files Browse the repository at this point in the history
Fixes angular#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 31, 2017
1 parent 30d53a8 commit 2f2d5f3
Show file tree
Hide file tree
Showing 21 changed files with 280 additions and 914 deletions.
11 changes: 3 additions & 8 deletions npm-shrinkwrap.clean.json
Original file line number Diff line number Diff line change
Expand Up @@ -2352,6 +2352,9 @@
"domhandler": {
"version": "2.3.0"
},
"domino": {
"version": "1.0.29"
},
"domutils": {
"version": "1.5.1"
},
Expand Down Expand Up @@ -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"
},
Expand Down
17 changes: 5 additions & 12 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-cli/integrationtest/test/basic_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down
6 changes: 3 additions & 3 deletions packages/compiler-cli/integrationtest/test/ng_module_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand All @@ -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`);
});

Expand Down
48 changes: 29 additions & 19 deletions packages/platform-browser/src/browser/browser_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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; }
Expand Down Expand Up @@ -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(); }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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; }
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-browser/src/browser/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down
1 change: 1 addition & 0 deletions packages/platform-browser/src/dom/dom_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions packages/platform-browser/test/browser/meta_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
12 changes: 9 additions & 3 deletions packages/platform-browser/test/browser/title_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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); });

Expand Down
2 changes: 1 addition & 1 deletion packages/platform-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"dependencies": {
"tslib": "^1.7.1",
"parse5": "^3.0.1",
"domino": "^1.0.29",
"xhr2": "^0.1.4"
},
"repository": {
Expand Down
Loading

0 comments on commit 2f2d5f3

Please sign in to comment.