• Stars
    star
    180
  • Rank 213,097 (Top 5 %)
  • Language
    JavaScript
  • License
    BSD 2-Clause "Sim...
  • Created over 3 years ago
  • Updated about 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Web component server-side rendering

🌊 Ocean

Web component HTML rendering that includes:

  • Rendering to Declarative Shadow DOM, requiring no JavaScript in the client.
  • Automatic inclusion of the Declarative Shadow DOM polyfill for browsers without support.
  • Streaming HTML responses.
  • Compatibility with the most popular web component libraries (see a compatibility list below).
  • Lazy partial hydration via special attributes: hydrate on page load, CPU idle, element visibility, or media queries. Or create your own hydrator.

Table of Contents

Overview

An ocean is an environment for rendering web component code. It provides an html function that looks like the ones you're used to from libraries like uhtml and Lit. Instead of creating reactive DOM in the client like those libraries, Ocean's html returns an async iterator that will stream out HTML strings.

Ocean is somewhat low-level and is meant to be used with a higher-level framework. Typical usage looks like this:

import 'https://cdn.spooky.click/ocean/1.3.1/shim.js?global';
import { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

const { HTMLElement, customElements, document } = globalThis;

class AppRoot extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  connectedCallback() {
    let div = document.createElement('div');
    div.textContent = `This is an app!`;
    this.shadowRoot.append(div);
  }
}

customElements.define('app-root', AppRoot);

const { html } = new Ocean({
  document,
  polyfillURL: '/webcomponents/declarative-shadow-dom.js'
});

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My app</title>

  <app-root></app-root>
`;

let code = '';
for await(let chunk of iterator) {
  code += chunk;
}
console.log(chunk); // HTML string

The above will generate the following HTML:

<!doctype html>
<html lang="en">
<title>My app</title>

<script type="module">const o=(new DOMParser).parseFromString('<p><template shadowroot="open"></template></p>',"text/html",{includeShadowRoots:!0}).querySelector("p");o&&o.shadowRoot||async function(){const{hydrateShadowRoots:o}=await import("/webcomponents/declarative-shadow-dom.js");o(document.body)}()</script>
<app-root>
  <template shadowroot="open">
    <div>This is an app!</div>
  </template>
</app-root>

Modules

Ocean comes with its main module and a DOM shim for compatible with custom element code.

Main module

The main module for Ocean is available in two forms: bundled and unbundled.

  • If you are using Ocean in a browser context, such as a service worker, use the bundled version.
  • If you are using Ocean in Deno, use the unbundled version.

Unbundled

import { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

Bundled

import { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.bundle.js';

DOM shim

Ocean's DOM shim is backed by linkedom, a fast DOM layer. The shim also bridges compatibility with popular web component libraries.

It's important to import the DOM shim as one of the first imports in your app.

import 'https://cdn.spooky.click/ocean/1.3.1/shim.js?global';

Notice that this includes in the ?global query parameter. This makes the shim available on globals; you get document, customElements, and other commonly used global variables.

If you do not want to shim the global environment you can omit the ?global query parameter and instead get the globals yourself from the symbol Symbol.for('dom-shim.defaultView'). This is advanced usage.

import 'https://cdn.spooky.click/ocean/1.3.1/shim.js';

const root = globalThis[Symbol.for('dom-shim.defaultView')];
const { HTMLElement, customElements, document } = root;

Hydration

Partial hydration is the practice of only hydrating (via running client JavaScript) components that are needed for interactivity. Ocean does not automatically add scripts for components by default. However Ocean does support both full and partial hydration. This means you can omit the component script tags from your HTML and Ocean will automatically add them for you.

In order to add script tags you have to provide Ocean a map of tag names to URLs to load. You do this through the elements Map that is returned from the constructor.

let { html, elements } = new Ocean({
  document
});

elements.set('app-sidebar', '/elements/app-sidebar.js');

Note: Ocean only adds script tags for elements that are server rendered. If you are not server rendering an element you will need to add the appropriate script tags yourself.

Full hydration

Full hydration means added script tags to the <head> for any components that are server rendered. You can enable full hydration by passing this in the constructor:

let { html, elements } = new Ocean({
  document,
  hydration: 'full'
});

elements.set('app-sidebar', '/elements/app-sidebar.js');

customElements.define('app-sidebar', class extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  connectedCallback() {
    let div = document.createElement('div');
    div.textContent = `My sidebar...`;
    this.shadowRoot.append(div);
  }
});

Then when you render this element, it will include the script tags:

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My app</title>

  <app-sidebar></app-sidebar>
`;

let out = '';
for(let chunk of iterator) {
  out += chunk;
}

Will produce this HTML:

<!doctype html>
<html lang="en">
<title>My app</title>
<script type="module" src="/elements/app-sidebar.js"></script>

<app-sidebar>
  <template shadowroot="open">
    <div>My sidebar...</div>
  </template>
</app-sidebar>

Partial hydration

By default Ocean uses partial hydration. In partial hydration script tags are only added when you explicitly tell Ocean to hydration an element. This means that by default elements will be rendered to HTML only, and never iteractive on the client.

This allows you to use the web component libraries you love both to produce static HTML and for interactive content.

To declare an element to be hydrated, use the ocean-hydrate attribute on any element. The value should be one of:

  • load: Hydrate when the page loads. Ocean will add a <script type="module"> tag for the element's script.
  • idle: Hydrate when the CPU becomes idle. Ocean will add an inline script that waits for requestIdleCallback and then loads the element's script.
  • media: Hydrates on a matching media query. This allows you to have some elements which only hydrate for certain screen sizes. Use the ocean-query attribute to specify the media query.
  • visible: Hydrate when the element becomes visible. This is useful for elements which are shown further down the page. Ocean will add an inline script that uses Intersection Observer to determine when the element is visible and then loads the script.

Using one of these hydrators looks like:

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="idle"></app-sidebar>
`;

Hydrator options

You can specify which hydrators you want to use by providing the hydrators option to Ocean. Each of the default hydrators are included by default, but can also be imported.

import {
  HydrateIdle,
  HydrateLoad,
  HydrateMedia,
  HydrateVisible,
  Ocean
} from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';
Load

To specify to hydrate on load, pass load into the ocean-hydrate attr:

let { html } = new Ocean({ document });

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="load"></app-sidebar>
`;

HydrateLoad does not take any options because it only adds a script tag to the head. You can create an instance by calling new on it:

import { HydrateLoad, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

let { html } = new Ocean({
  document,
  hydrators: [
    new HydrateLoad()
  ]
});
Idle

To specify to hydrate on idle, pass idle into the ocean-hydrate attr:

let { html } = new Ocean({ document });

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="idle"></app-sidebar>
`;

HydrateIdle uses a custom element to perform hydration when the CPU is idle. By default that custom element name is ocean-hydrate-idle. You can specify a different custom element name by passing it into the constructor.

import { HydrateIdle, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

let { html } = new Ocean({
  document,
  hydrators: [
    new HydrateIdle('my-app-hydrate-idle')
  ]
});
Media

To hydrate on a media query, pass media into the ocean-hydrate attr, and also provide a ocean-query attr with the media query to use:

let { html } = new Ocean({ document });

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="media" ocean-query="(max-width: 700px)"></app-sidebar>
`

HydrateMedia uses the custom element ocean-hydrate-media to hydrate your custom element. You can customize this, and also the attribute used for the query by passing those arguments into the constructor:

import { HydrateMedia, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

let { html } = new Ocean({
  document,
  hydrators: [
    new HydrateMedia('my-app-hydrate-media', 'app-query')
  ]
});

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="media" app-query="(max-width: 700px)"></app-sidebar>
`;
Visible

To specify to hydrate on element visibility, pass visible into the ocean-hydrate attr:

let { html } = new Ocean({ document });

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="visible"></app-sidebar>
`;

HydrateVisible uses the custom element ocean-hydrate-visible to track when your element is visible. You can customize this custom element tag name by passing in something else into the constructor:

import { HydrateVisible, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

let { html } = new Ocean({
  document,
  hydrators: [
    new HydrateVisible('my-app-hydrate-visible')
  ]
});

Custom hydrator

A hydrator is an object that specifies how to hydrate the element. You can create a custom hydrator and pass it to the hydrators option.

The following is a hydrator that hydrates whenever the element is clicked.

const clickHydrator = {
  condition: 'click',
  tagName: 'my-click-hydrator',
  renderMultiple: true,
  script() {
    return /* js */ `customElements.define('${this.tagName}', class extends HTMLElement {
  connectedCallback() {
    let el = this.previousElementSibling;
    let src = this.getAttribute('src');
    el.addEventListener('click', () => import(src), { once: true });
  }
})`;
  }
};

let { html } = new Ocean({
  document,
  hydrators: [
    clickHydrator
  ]
})

Which you would use like so:

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="click"></app-sidebar>
`;

The properties of a hydrator are (all required):

  • condition: This is the value used with ocean-hydrate to trigger the hydrator to be used.
  • tagName: Hydrators are implemented as custom elements. The tagName is the custom element tag name.
  • renderMultiple: This says that the custom element should be rendered for each element that uses the hydrator. Use false when hydrating is done without regard for the element. For example idle is false because it always just waits for CPU idle, so this only needs to be done once.
  • script(): A function which returns the custom element definition.

The following are optional properties:

  • mutate(customElement, node): Gives you a change to modify the hydration custom element being rendered, for example to add information needed to perform hydration. HydrateMedia uses this method to add the query to the custom element.

Relative links

When performing hydration or adding the declarative shadow DOM polyfill, Ocean adds links that you provide it. You can provide full URLs or pathnames like /js/dsd-polyfill.js. If you'd like for these links to be relative, you can use the relativeTo function to create an html that will produce relative links. Here's how you might use it in a service worker context:

import 'https://cdn.spooky.click/ocean/1.3.1/shim.js?global';
import { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

let { relativeTo } = new Ocean({
  document,
  polyfillURL: '/js/dsd-polyfill.js'
})

addEventListener('fetch', event => {
  let html = relativeTo(event.request.url);
  let iter = html`
    <!doctype html>
    <html lang="en">
    <!-- ... -->
  `;
});

The script tags added for the polyfill and for any element hydration will be relative to the event's URL.

Plugins

Ocean parses HTML into a DOM tree. Using plugins you can mutate the tree before it gets turned back into strings, allowing you to implement advanced behavior like syntax highlighting.

For the most part custom elements should be the way you customize HTML rendering; plugins are here for cases where you need to modify built-in elements.

The interface for a plugin is a function that returns an object with a handle method. The function is called during Ocean's internal optimization step:

class MyHighlighter {
  handle(node, head) {
    // Mutate this node, add anything to the head that you need.
  }

  static createInstance() {
    return new MyHighlighter();
  }
}

let ocean = new Ocean({
  document,
  plugins: [MyHighlighter.createInstance]
});

Compatibility

Ocean is tested against popular web component libraries. These tests are not all inclusive, test contributions are very much welcome.

Library Compatible Notes
Vanilla ✔
Lit ✔
Stencil ✔
Haunted ✔
Atomico ✔
uce ✔
Preact ✔
petite-vue ✔
Wafer ✔
FAST ✖ Heavily relies on DOM internals.
Lightning Web Components ✖ I can't figure out how to export an LWC, if you can help see #11

More Repositories

1

haunted

React's Hooks API implemented for web components 👻
TypeScript
2,599
star
2

robot

🤖 A functional, immutable Finite State Machine library
JavaScript
1,906
star
3

corset

Declarative data bindings, bring your own backend.
JavaScript
276
star
4

position--sticky-

Polyfill for position: sticky;
JavaScript
175
star
5

fs

File system abstraction layer for the browser
JavaScript
164
star
6

gazel

Key/value store for the browser
JavaScript
119
star
7

script-type-module

A <script type=module> polyfill
JavaScript
97
star
8

custom-attributes

Defining custom attributes that provide rich mixin behavior
HTML
95
star
9

bram

Web components, live bound templates, in 4kB
JavaScript
68
star
10

flora

Streaming templates for Node.js
JavaScript
63
star
11

astro-fastify

A Fastify adapter for Astro
JavaScript
61
star
12

fritz

Take your UI off the main thread
JavaScript
40
star
13

views-the-hard-way

JavaScript views; done as painfully as possible
JavaScript
32
star
14

text

Cross browser DOM inner text manipulation
JavaScript
18
star
15

react-robot

Robot plugin for React
17
star
16

read-time

A component for calculating how long it will take to read an article.
JavaScript
17
star
17

if-else

HTML
16
star
18

astro-in-php-example

Example PHP project running Astro inside with the Container API
CSS
16
star
19

fs-cheerio

Read and write files to/from cheerio objects.
JavaScript
15
star
20

gv-app

Google Voice command line client
JavaScript
14
star
21

thingjs

Thing.js - A tiny object that aids with creating other objects
JavaScript
12
star
22

comic-reader

A comic reader web component
JavaScript
12
star
23

beach

Web components framework for Deno
JavaScript
12
star
24

es6modulesnow

ES6 modules for ES5 browsers
JavaScript
11
star
25

lucy

Not yet ready for consumption
JavaScript
11
star
26

fly-out

A fly out menu
JavaScript
11
star
27

inline-confirmation

A web component to confirm something.
JavaScript
10
star
28

robot-hooks

Hooks for Robot 🤖
10
star
29

is-night

Is it night time?
JavaScript
9
star
30

haunted-starter-app

Created with CodeSandbox
JavaScript
8
star
31

astro-flyio-fastify-starter

Starter for using Fly.io and Fastify with Astro
TypeScript
8
star
32

theda

Classes in Web Workers
JavaScript
7
star
33

phonograph.js

A player/manager for the <audio> tag in JavaScript.
JavaScript
7
star
34

canread-sharp

Implementation of readability.js in C#
C#
7
star
35

analog-clock

A clock, analog style
JavaScript
7
star
36

leni

Yet another web worker library by me!
HTML
6
star
37

astro-lit-demo

Just a demo of the Lit renderer
CSS
6
star
38

editable-title

A title element that can be edited
JavaScript
6
star
39

lit-robot

LitElement bindings for Robot
6
star
40

astro-renderer-webcomponent

JavaScript
6
star
41

beepboop

JavaScript
6
star
42

babel-plugin-modules-web-compat

A Babel plugin that makes modules web compatible
JavaScript
6
star
43

weston

CanJS templates with ease
JavaScript
5
star
44

lit-labs-ssr-bundle

A bundle of @lit-labs/ssr
JavaScript
5
star
45

String-Serializer-Sharp

Serialize/deserialize strings to objects.
C#
5
star
46

kill-on-exit

Ensure child processes are killed when the parent exits
JavaScript
5
star
47

xhr

Tiny, simple, HTTP requests
JavaScript
5
star
48

librobot

C library for creating Finite State Machines with Statecharts support
C
4
star
49

ts-types-html-parser

HTML parser in TS types
TypeScript
4
star
50

attr

Attribute manipulation
JavaScript
4
star
51

aremodulesready

https://aremodulesready.com/
JavaScript
4
star
52

google-search-cli

Bash script to search Google from the command line using curl and html2text
4
star
53

event-wait-until

Implements waitUntil on Event
HTML
4
star
54

on-page

A router for web components. Render a template when the page matches a route. When you're on a page. That's it.
HTML
4
star
55

jspm-bower

Bower endpoint for jspm
JavaScript
3
star
56

bind-state

Bind the state of your application to elements.
HTML
3
star
57

b2g-phoney

A phone dialer app for Boot 2 Gecko
JavaScript
3
star
58

actordom

TypeScript
3
star
59

corset-site

Astro
3
star
60

bitjsx

A fork of bitjs, for speed
JavaScript
3
star
61

system-bower

SystemJS and StealJS Bower plugin
JavaScript
3
star
62

mocha-test

A custom element for running mocha/chai tests
HTML
3
star
63

display-drawer

A drawer that opens and closes, where some stuff is visible
HTML
3
star
64

multientry-shim

Shim for multiEntry functionality in IndexedDB.
JavaScript
3
star
65

keys

Cross-browser Object.keys implementation
JavaScript
2
star
66

raj-fritz

Fritz bindings for Raj
JavaScript
2
star
67

preact-robot

Preact hooks for use with Robot
2
star
68

parse-utils

Parsing and serializing HTML
JavaScript
2
star
69

spawn-mochas

Spawn mocha processes
JavaScript
2
star
70

lua-boilerplate

Boilerplate project for lua cli apps
Lua
2
star
71

do-modules-work

Do Modules Work?
JavaScript
2
star
72

can-ajax

Ajax can.Component
JavaScript
2
star
73

lon

DynamoDB query builder
JavaScript
2
star
74

JustServe

Bare minimum web server written in C#
C#
2
star
75

matthewp.github.com

JavaScript
2
star
76

finger.js

Finger protocol implementation in node
JavaScript
2
star
77

blog

My blog
JavaScript
2
star
78

FileMakerConnect

An abstraction layer for querying a FileMaker database via ODBC.
C#
2
star
79

webcomponent-pack

Packs together HTML, JavaScript, and CSS for web components.
JavaScript
2
star
80

robot-logo

The Robot logo
2
star
81

conditional-view

A conditional
JavaScript
2
star
82

haunted-robot

Haunted hooks for use with Robot
2
star
83

lit-html-extensions

My personal extensions for lit-html
JavaScript
2
star
84

html-apps

JavaScript
2
star
85

canrocks

A CanJS plugin search engine; components, attributes and other plugins
HTML
2
star
86

wow-tabs

The last tabs you'll ever need.
HTML
2
star
87

browser-socket

Abstraction library for Chrome and Mozilla socket implementations
JavaScript
2
star
88

matthew-pmo

JavaScript
1
star
89

dotfiles

Emacs Lisp
1
star
90

ls-deps

List dependencies of an StealJS or SystemJS project.
JavaScript
1
star
91

astroterm

TUI for Astro
Go
1
star
92

text-to-module

Convert arbitrary text to a module
JavaScript
1
star
93

dropbox-component

Dropbox.js as a component
1
star
94

promise-child

Create a promise from a ChildProcess
JavaScript
1
star
95

xhrerror

Cross-browser Error object for use with XHR requests
JavaScript
1
star
96

decorators

Exploring the ideas of web component decorators
HTML
1
star
97

on-line

A web component for online/offline notification
JavaScript
1
star
98

rel-template

A very simple template loaded utilizing the link tag.
JavaScript
1
star
99

autopuz

A bash script and C program combination for printing the daily crossword puzzle.
C
1
star
100

yumm-android

Yumm application for android
Java
1
star