Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

Commit

Permalink
customElements -> components
Browse files Browse the repository at this point in the history
  • Loading branch information
gilbert committed Nov 16, 2016
1 parent 374b24f commit 1a9c7d3
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 81 deletions.
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ You can take the same ideas (and standards), apply them directly server side, to
#### Define a component

```javascript
var customElements = require("server-components");
var components = require("server-components");

// Define a new class that extends a native HTML Element
class NewElement extends customElements.HTMLElement {
class NewElement extends components.HTMLElement {
// When the element is created during DOM parsing, you can transform the HTML inside it.
// This can be configurable too, either by setting attributes or adding HTML content
// inside it or elsewhere in the page it can interact with. Elements can fire events
Expand All @@ -39,21 +39,21 @@ class NewElement extends customElements.HTMLElement {
}

// Register the element with an element name
customElements.define("my-new-element", NewElement);
components.define("my-new-element", NewElement);
```

For examples of more complex component definitions, take a look at the [example components](https://github.com/pimterry/server-components/blob/master/component-examples.md)

#### Use your components

```javascript
var customElements = require("server-components");
var components = require("server-components");

// Render the HTML, and receive a promise for the resulting HTML string.
// The result is a promise because elements can render asynchronously, by returning
// promises from their callbacks. This allows elements to render content from
// external web services, your database, or anything else you can imagine.
customElements.renderPage(`
components.renderPage(`
<html>
<head></head>
<body>
Expand Down Expand Up @@ -83,15 +83,15 @@ There aren't many published sharable components to drop in quite yet, as it's st

### Top-level API

#### `customElements.HTMLElement`
#### `components.HTMLElement`

Creates a returns a new custom HTML element prototype, extending the HTMLElement prototype.

Note that this does *not* register the element. To do that, call `components.registerElement` with an element name, and options (typically including the prototype returned here as your 'prototype' value).

This is broadly equivalent to `Object.create(HTMLElement.prototype)` in browser land, and exactly equivalent here to `Object.create(components.dom.HTMLElement.prototype)`. You can call that yourself instead if you like, but it's a bit of a mouthful.

#### `customElements.define(componentName, Constructor)`
#### `components.define(componentName, Constructor)`

Registers an element, so that it will be used when the given element name is found during parsing.

Expand All @@ -103,25 +103,25 @@ This returns the constructor for the new element, so you can construct and inser

This is broadly equivalent to `document.registerElement` in browser land.

#### `customElements.renderPage(html)`
#### `components.renderPage(html)`

Takes an HTML string for a full page, and returns a promise for the HTML string of the rendered result. Server Components parses the HTML, and for each registered element within calls its various callbacks (see the Component API) below as it does so.

Unrecognized elements are left unchanged. When calling custom element callbacks any returned promises are collected, and this call will not return until all these promises have completed. If any promises are rejected, this renderPage call will be rejected too.

To support the full DOM Document API, this method requires that you are rendering a full page (including `<html>`, `<head>` and `<body>` tags). If you don't pass in content wrapped in those tags then they'll be automatically added, ensuring your resulting HTML has a full valid page structure. If that's not what you want, take a look at `renderFragment` below.

#### `customElements.renderFragment(html)`
#### `components.renderFragment(html)`

Takes an HTML string for part of a page, and returns a promise for the HTML string of the rendered result. Server Components parses the HTML, and for each registered element within calls its various callbacks (see the Component API) below as it does so.

Unrecognized elements are left unchanged. When calling custom element callbacks any returned promises are collected, and this call will not return until all these promises have completed. If any promises are rejected, this renderFragment call will be rejected too.

This method renders the content as a [Document Fragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment), a sub-part of a full document. This means if you there are any `<html>`, `<head>` or `<body>` tags in your input, they'll be stripped, as they're not legal within a fragment of a document. Note that this means the provided `document` object in your components will actually be a `DocumentFragment`, not a true `Document` object (although in most cases you can merrily ignore this). If you want to render a full page, take a look at `renderPage` above.

#### `customElements.dom`
#### `components.dom`

The DOM object (customElements.dom) exposes traditional DOM objects (normally globally available in browsers) such as the CustomEvent and various HTMLElement classes, typically use inside your component implementations.
The DOM object (components.dom) exposes traditional DOM objects (normally globally available in browsers) such as the CustomEvent and various HTMLElement classes, typically use inside your component implementations.

This is (very) broadly equivalent to `window` in browser land.

Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ exports.get = function (name) {
exports.whenDefined = function (name) {
return CustomElementRegistry.instance().whenDefined(name);
};
exports.reset = function (name) {
return CustomElementRegistry.instance().reset();
};


const _upgradedProp = '__$CE_upgraded';
Expand Down
22 changes: 11 additions & 11 deletions test/asynchrony-test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"use strict";
var expect = require('chai').expect;

var customElements = require("../src/index.js");
var components = require("../src/index.js");

describe("An asynchronous element", () => {
beforeEach(() => {
customElements.customElements.reset();
components.reset();
});

it("blocks rendering until they complete", () => {
class SlowElement extends customElements.HTMLElement {
class SlowElement extends components.HTMLElement {
connectedCallback() {
return new Promise((resolve, reject) => {
setTimeout(() => {
Expand All @@ -19,37 +19,37 @@ describe("An asynchronous element", () => {
});
}
}
customElements.define("slow-element", SlowElement);
components.define("slow-element", SlowElement);

return customElements.renderFragment("<slow-element></slow-element>").then((output) => {
return components.renderFragment("<slow-element></slow-element>").then((output) => {
expect(output).to.equal("<slow-element>loaded!</slow-element>");
});
});

it("throw an async error if a component fails to render synchronously", () => {
class FailingElement extends customElements.HTMLElement {
class FailingElement extends components.HTMLElement {
connectedCallback() {
throw new Error();
}
}
customElements.define("failing-element", FailingElement);
components.define("failing-element", FailingElement);

return customElements.renderFragment(
return components.renderFragment(
"<failing-element></failing-element>"
).then((output) => {
throw new Error("Should not successfully render");
}).catch(() => { /* All good. */ });
});

it("throw an async error if a component fails to render asynchronously", () => {
class FailingElement extends customElements.HTMLElement {
class FailingElement extends components.HTMLElement {
connectedCallback() {
return Promise.reject(new Error());
}
}
customElements.define("failing-element", FailingElement);
components.define("failing-element", FailingElement);

return customElements.renderFragment(
return components.renderFragment(
"<failing-element></failing-element>"
).then((output) => {
throw new Error("Should not successfully render");
Expand Down
34 changes: 17 additions & 17 deletions test/basics-test.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,88 @@
"use strict";
var expect = require('chai').expect;

var customElements = require("../src/index.js");
var components = require("../src/index.js");

describe("Basic component functionality", () => {
it("does nothing with vanilla HTML", () => {
var input = "<div></div>";

return customElements.renderFragment(input).then((output) => {
return components.renderFragment(input).then((output) => {
expect(output).to.equal(input);
});
});

it("replaces customElements with their rendered result", () => {
class NewElement extends customElements.HTMLElement {
it("replaces components with their rendered result", () => {
class NewElement extends components.HTMLElement {
connectedCallback() {
this.textContent = "hi there";
}
}
customElements.define("my-element", NewElement);
components.define("my-element", NewElement);

return customElements.renderFragment("<my-element></my-element>").then((output) => {
return components.renderFragment("<my-element></my-element>").then((output) => {
expect(output).to.equal("<my-element>hi there</my-element>");
});
});

it("can wrap existing content", () => {
class PrefixedElement extends customElements.HTMLElement {
class PrefixedElement extends components.HTMLElement {
connectedCallback() {
this.innerHTML = "prefix:" + this.innerHTML;
}
}
customElements.define("prefixed-element", PrefixedElement);
components.define("prefixed-element", PrefixedElement);

return customElements.renderFragment(
return components.renderFragment(
"<prefixed-element>existing-content</prefixed-element>"
).then((output) => expect(output).to.equal(
"<prefixed-element>prefix:existing-content</prefixed-element>"
));
});

it("allows attribute access", () => {
class BadgeElement extends customElements.HTMLElement {
class BadgeElement extends components.HTMLElement {
connectedCallback() {
var name = this.getAttribute("name");
this.innerHTML = "My name is: <div class='name'>" + name + "</div>";
}
}
customElements.define("name-badge", BadgeElement);
components.define("name-badge", BadgeElement);

return customElements.renderFragment(
return components.renderFragment(
'<name-badge name="Tim Perry"></name-badge>'
).then((output) => expect(output).to.equal(
'<name-badge name="Tim Perry">My name is: <div class="name">Tim Perry</div></name-badge>'
));
});

it("can use normal document methods like QuerySelector", () => {
class SelfFindingElement extends customElements.HTMLElement {
class SelfFindingElement extends components.HTMLElement {
connectedCallback(document) {
var hopefullyThis = document.querySelector("self-finding-element");
if (hopefullyThis === this) this.innerHTML = "Found!";
else this.innerHTML = "Not found, found " + hopefullyThis;
}
}
customElements.define("self-finding-element", SelfFindingElement);
components.define("self-finding-element", SelfFindingElement);

return customElements.renderFragment(
return components.renderFragment(
'<self-finding-element></self-finding-element>'
).then((output) => expect(output).to.equal(
'<self-finding-element>Found!</self-finding-element>'
));
});

it("wraps content in valid page content, if rendering a page", () => {
return customElements.renderPage("<empty-div></empty-div>").then((output) => {
return components.renderPage("<empty-div></empty-div>").then((output) => {
expect(output).to.equal(
"<html><head></head><body><empty-div></empty-div></body></html>"
);
});
});

it("strips <html>, <head> and <body> tags, if only rendering a fragment", () => {
return customElements.renderFragment("<html><body><empty-div><head></head></empty-div></body></html>").then((output) => {
return components.renderFragment("<html><body><empty-div><head></head></empty-div></body></html>").then((output) => {
expect(output).to.equal(
"<empty-div></empty-div>"
);
Expand Down
10 changes: 5 additions & 5 deletions test/element-validation-test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"use strict";
var expect = require('chai').expect;

var customElements = require("../src/index.js");
var components = require("../src/index.js");

describe("Custom element validation", () => {
it("requires a non-empty name", () => {
class InvalidElement {}
expect(() => {
customElements.define("", InvalidElement);
components.define("", InvalidElement);
}).to.throw(
/The element name '' is not valid./
);
Expand All @@ -16,7 +16,7 @@ describe("Custom element validation", () => {
it("requires a hyphen in the element name", () => {
class InvalidElement {}
expect(() => {
customElements.define("invalidname", InvalidElement);
components.define("invalidname", InvalidElement);
}).to.throw(
/The element name 'invalidname' is not valid./
);
Expand All @@ -25,7 +25,7 @@ describe("Custom element validation", () => {
it("doesn't allow elements to start with a hyphen", () => {
class InvalidElement {}
expect(() => {
customElements.define("-invalid-name", InvalidElement);
components.define("-invalid-name", InvalidElement);
}).to.throw(
/The element name '-invalid-name' is not valid./
);
Expand All @@ -34,7 +34,7 @@ describe("Custom element validation", () => {
it("requires element names to be lower case", () => {
class InvalidElement {}
expect(() => {
customElements.define("INVALID-NAME", InvalidElement);
components.define("INVALID-NAME", InvalidElement);
}).to.throw(
/The element name 'INVALID-NAME' is not valid./
);
Expand Down
28 changes: 14 additions & 14 deletions test/example-components.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
"use strict";
var expect = require('chai').expect;
var customElements = require("../src/index.js");
var components = require("../src/index.js");

var linkify = require("linkifyjs/element");

describe("An example component:", () => {
beforeEach(() => {
customElements.customElements.reset();
components.reset();
});

describe("using static rendering", () => {
beforeEach(() => {
class StaticElement extends customElements.HTMLElement {
class StaticElement extends components.HTMLElement {
connectedCallback() {
this.innerHTML = "Hi there";
}
}
customElements.define("my-greeting", StaticElement);
components.define("my-greeting", StaticElement);
});

it("replaces its content with the given text", () => {
return customElements.renderFragment("<my-greeting></my-greeting>").then((output) => {
return components.renderFragment("<my-greeting></my-greeting>").then((output) => {
expect(output).to.equal("<my-greeting>Hi there</my-greeting>");
});
});
Expand All @@ -30,21 +30,21 @@ describe("An example component:", () => {
beforeEach(() => {
var currentCount = 0;

class CounterElement extends customElements.HTMLElement {
class CounterElement extends components.HTMLElement {
connectedCallback() {
currentCount += 1;
this.innerHTML = "There have been " + currentCount + " visitors.";
}
}
customElements.define("visitor-counter", CounterElement);
components.define("visitor-counter", CounterElement);
});

it("dynamically changes its content", () => {
customElements.renderFragment("<visitor-counter></visitor-counter>");
customElements.renderFragment("<visitor-counter></visitor-counter>");
customElements.renderFragment("<visitor-counter></visitor-counter>");
components.renderFragment("<visitor-counter></visitor-counter>");
components.renderFragment("<visitor-counter></visitor-counter>");
components.renderFragment("<visitor-counter></visitor-counter>");

return customElements.renderFragment("<visitor-counter></visitor-counter>").then((output) => {
return components.renderFragment("<visitor-counter></visitor-counter>").then((output) => {
expect(output).to.equal(
"<visitor-counter>There have been 4 visitors.</visitor-counter>"
);
Expand All @@ -54,17 +54,17 @@ describe("An example component:", () => {

describe("parameterised by HTML content", () => {
beforeEach(() => {
class LinkifyElement extends customElements.HTMLElement {
class LinkifyElement extends components.HTMLElement {
connectedCallback(document) {
// Delegate the whole thing to a real front-end library!
linkify(this, { target: () => null, linkClass: "autolinked" }, document);
}
}
customElements.define("linkify-urls", LinkifyElement);
components.define("linkify-urls", LinkifyElement);
});

it("should be able to parse and manipulate it's content", () => {
return customElements.renderFragment(
return components.renderFragment(
"<linkify-urls>Have you heard of www.facebook.com?</linkify-urls>"
).then((output) => expect(output).to.equal(
'<linkify-urls>Have you heard of <a href="http://www.facebook.com" class="autolinked">www.facebook.com</a>?</linkify-urls>'
Expand Down
Loading

0 comments on commit 1a9c7d3

Please sign in to comment.