Skip to content

A fast, secure, modern, light-weight, and simple JS library for creating web components and more!

License

Notifications You must be signed in to change notification settings

AegisJSProject/core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@aegisjsproject/core

A fast, secure, modern, light-weight, and simple JS library for creating web components and more!

CodeQL Node CI Lint Code Base

GitHub license GitHub last commit GitHub release GitHub Sponsors

npm node-current npm bundle size npm

GitHub followers GitHub forks GitHub stars Twitter Follow

Donate using Liberapay


Installation

CDN / unpkg.com

The preferred method of using Aegis is via import from https://unpkg.com/aegisjsproject/core[@:version]. You may use Subresource Integrity, but this is only recommended if you include a version in the script URL. For convenience, you maybe desire using <script type="importmap"> in your development setup and a plug-in such as @shgysk8zer0/rollup-import when bundling for production.

NPM/Node

Of course, you could always stick with the the more familiar package installation:

npm install aegisjsproject/core

That works just fine, should you prefer.

Git Submodule

A final option is installing via a git submodule.

git submodule add https://github.com/AegisJSProject/core.git [:destination_path]

Submodules ultimately make little difference compared to the CDN or installing the package, should you prefer that option. There are no dependencies, and you probably just end up using a different source/URL for your <script> or import. The one and only difference to consider is that this will not include the generated CommonJS (use for require()) version.

Security

This library is designed with security, and specifically strict Content-Security-Policy and TrustedTypes, in mind.

However, since the Sanitizer API is an in-development API and new CSStyleSheet() is not universally supported, the polyfills require some minimal adjustments to any Content-Security-Policy:

CSP - script-src

For the recommended polyfills, allowing script from https://unpkg.com/@shgysk8zer0/ (you can be more restrictive/specific to https://unkg.com/@shgysk8zer0/polyfills@:version) will be necessary.

CSP - style-src

The recommended polyfill for new CSSStyleSheet() and adoptedStyleSheets requires use of URL.createObejctURL(). As such, style-src is suggested to allow blob: URIs.

CSP - trusted-types

For the sake of the Sanitizer API polyfill, if you have trusted-types as part of your CSP, you must allow empty#html and empty#script (for trustedTypes.emptyHTML and trustedTypes.emptyScript).

Additional Security Considerations

  • All commits are PGP/GPG signed, ensuring the authenticity of the author
  • All releases use Package Provenance, proving that the generated & published package was created by a specific (and signed) commit

Compatibility

This library relies on the Sanitizer API proposal and Constructable Stylesheets. At minimum, there MUST be a polyfill for Element.prototype.setHTML.

Performance/Optimization Tips

HTML Templates / Fragments

The html function returns a DocumentFragment which cannot be directly reused in multiple places. Should you wish to reuse the fragment and only generate it once, please use cloneNode(true):

import { html } from '@aegisjsproject/core';

const tmp = html`<!-- Your HTML here -->`

document.querySelector('.my-component').append(tmp.cloneNode(true));

StyleSheets / CSS

On the other hand, css relies on adoptedStyleSheets for Document/ShadowRoot, which does allow for easy reuse without cloning. You may (and probably should) reuse where possible. You may use either rootOrShadow.adoptedStyleSheets = [sheet] or the provided addStyles(target, ...sheets) or replaceStyles(target, ...sheets) functions.

Use as ES Modules

Since import attributes / import attributes / CSS and HTML Modules are not yet standardized or implemented anywhere, this library allows for easy HTML templates and styles via plain old ES/JS Modules:

import { html, css } from '@aegisjsproject/core';

export const template = html`<!-- Your Markup Here -->`;

export const sheet = css`/* Your Styles Here */`;

Path to a Stable Release (v1.0.0)

Since this relies on Element.prototype.setHTML, which is a developing proposal, Aegis will remain in pre-1.0.0/stable release until that proposal is stable. Until then, you may use @shgysk8zer0/polyfills or any compatible polyfill of your choosing.

Basic Usage Notes

Outside of usage in web components/custom elements, it is important to understand how Constructable Stylesheets and adoptedStyleSheets work. When using css, stylesheets will be adopted by the Document and apply the same as any other stylesheet (not scoped). Should you want to scope your styles outside of the context of web components (using ShadowRoot), you should create a unique selector and utilize that when composing your stylesheets:

import { css, html, getUniqueSelector, addStyles } from '@aegisjsproject/core';

const scope = getUniqueSelector();

document.body.append(html`
  <div id="${scope}">
    <span class="my-class">Hello, World!</span>
  </div>
`);

addStyles(document, css`#${scope} .my-class {color: red;}`);

Basic Web Component Example - <dad-joke>

import { html, css, registerComponent, registerCallback, attachListeners, EVENTS } from '@aegisjsproject/core';

const styles = css`:host {
  display: block;
}

p.joke {
  font-family: system-ui;
  padding: 1.3rem;
  border-radius: 8px;
  border: 1px solid #cacaca;
}

button {
  cursor: pointer;
}`;

const render = registerCallback('dad-joke:render', event => event.target.getRootNode().host.render());

const template = html`<div part="container">
  <div id="joke-container"></div>
  <button type="button" class="btn btn-primary" part="update-btn" ${EVENTS.onClick}="${render}">Update</button>
</div>`;

registerComponent('dad-joke', class HTMLDataJokeElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.adoptedStyleSheets = [styles];
    this.shadowRoot.append(attachListeners(template.cloneNode(true)));
  }

  connectedCallback() {
    this.render();
  }

  async render({ signal } = {}) {
    this.shadowRoot.getElementById('joke-container').replaceChildren(
      html`<p class="joke">${await HTMLDataJokeElement.getJoke({ signal })}</p>`
    );
  }

  static async getJoke({ signal } = {}) {
    const resp = await fetch('https://icanhazdadjoke.com', {
      headers: { Accept: 'text/plain' },
      referrerPolicy: 'no-referrer',
      signal,
    });

    return await resp.text();
  }
});

Advanced Usage

The provided html uses the default white-list for elements and attributes, which does not allow for <svg>, <math> (MathML), or custom elements by default. Should you have need of such features, you may use createHTMLParser() with the desired config/options.

Note: Directions and documentation for config options are not provided in these early release, as the Sanitizer API is not stable and is subject to change.

There is also an additional createCSSParser({ media, disabled, baseURI }) function which returns a custom css parsing function.

Advanced Exampled

import { createHTMLParser, createCSSParser } from '@aegisjsproject/core';

const html = createHTMLParser({
  allowElements: ['h1', 'h2', 'span', 'a', 'img', 'blockquote'],
  allowAttributes: ['href', 'class', 'id', 'src'],
});

const css = createCSSParser({ media: '(prefers-color-scheme: dark)', baseURI: 'https://cdn.example.com' });

// `html` & `css` then function as the regular / exported function, but with their
custom white/black lists / config options.

Event handlers

Although onclick and other event attributes are stripped out by the sanitizer, you can still (more securely) add event listeners when authoring HTML.

This works by registering a callback (in a private Map object) which stores the name as the key and the registered callback/function as the value. registerCallback() adds the name and callback to the map and returns the key. attachListeners() can then be used to call addEventListener() for the given value of the attribute by retrieving the function from the map.

NOTE: The attribute name is subject to change, so you should only use EVENTS.onClick, etc, instead of setting the attribute manually.

import { registerCallback } from '@aegisjsproject/core/callbackRegistry.js';
import { EVENTS, attachListeners } from '@aegisjsproject/core/events.js';

const log = registerCallback('log', console.log);

const logBtn = attachListeners(html`<button ${EVENTS.onClick}="${log}">Click Me!</button>`);
const backBtn = attachListeners(html`<button ${EVENTS.onCLick}="${registerCallback('back', () => history.back())}">Back</button>`

SVG Generation

There is a provided svg function, but it should be noted that all SVGs MUST include <svg xmlns="http://www.w3.org/2000/svg">. This is due to the requirements of new DOMParser().parseFromString(svgContent, 'image/svg+xml'). You may also want to set viewBox as necessary.