0.0.9 • Published 3 years ago

@sifrr/dom v0.0.9

Weekly downloads
14
License
MIT
Repository
github
Last release
3 years ago

sifrr-dom · npm version Doscify

A ~6KB :zap: fast DOM framework for creating web user interfaces using Custom Elements with state management, one way/two way data bindings etc.

Sifrr-Dom is best of both worlds: write components in pure HTML, CSS, JS with ease-of-use and features of a full fledged JS framework like css scoping (shadow root), events, reconciliation etc.

Size

TypeSize
Minified (dist/sifrr.dom.min.js)npm.io
Minified + Gzipped (dist/sifrr.dom.min.js)npm.io

Tradeoffs

  • :+1: Uses @sifrr/template and adds custom elements, prop/state management on top of it
  • :+1: Use latest web API standards (custom elements v1)
  • :+1: CSS scoping with shadow root
  • :-1: hence will not work in older browsers without polyfills
  • :+1: In-built Synthetic event listeners and custom events

Performance Comparison

Check Performance Here

Note: These might not be exact and should only be taken as a reference.

How to use

Directly in Browser using standalone distribution

Add script tag in your website.

<!-- Sifrr.Template is also required -->
<script src="https://unpkg.com/@sifrr/template@{version}/dist/sifrr.template.min.js"></script>
<script src="https://unpkg.com/@sifrr/dom@{version}/dist/sifrr.dom.min.js"></script>

Browser API support needed for

APIscaniusepolyfills
Custom Elements v1https://caniuse.com/#feat=custom-elementsv1https://github.com/webcomponents/custom-elements
Promises APIhttps://caniuse.com/#feat=promiseshttps://github.com/stefanpenner/es6-promise
Shadow DOM v1https://caniuse.com/#feat=shadowdomv1https://github.com/webcomponents/shadydom
ES6 Modules (if you use type='module' on script tag)https://caniuse.com/#feat=es6-modulehttps://github.com/ModuleLoader/es-module-loader
Fetch API (if you use Sifrr.Dom.load)https://caniuse.com/#feat=fetchhttps://github.com/github/fetch

If custom elements v1 API is supported by browsers, it is very likely that other APIs are supported as well.

Using npm

Do npm i @sifrr/template @sifrr/dom or yarn add @sifrr/template @sifrr/dom or add the package to your package.json file.

Put in your frontend js module (compatible with webpack/rollup/etc).

Importing

// node require
const { setup } = require('@sifrr/dom');

// es6 module - supports both named and default export
import { setup } from '@sifrr/dom';
import SifrrDom from '@sifrr/dom';
const { setup } = SifrrDom;

// if using script tag
const { setup } = Sifrr.Dom;

Basic API usage

Setting Up

// index.js

// Default Setup Config for Sifrr Dom
const config = {
  events: ['input', 'change', 'update'], // synthetic event listerners to add, read more in Synthetic Events section
  // config below will be used by `load` to figure out url of the element name given
  urls: {
    'element-name': '/element/name.js' // key-value pairs or element name and urls
  },
  url: null // function to get url of an element
};
// Set up Sifrr-Dom
Sifrr.Dom.setup(config);

Sifrr element

import { html } from '@sifrr/template';
import { Element } from '@sifrr/template';

class CustomTag extends Element {
  static get template() {
    return html`
      <style media="screen">
        p {
          color: blue;
        }
      </style>
      <p>${el => el.data()}</p>
    `; // el is the element instance
  }
  // other methods for the custom element
  data() {
    return this.getAttribute('data');
  }
}
Sifrr.Dom.register(CustomTag); // you should register in file itself to keep this file independently usable/downloadable
module.exports = CustomTag;

Template

template should be return value of Sifrr.Template.html. check out sifrr-template for more details. Bindings work as it does in Sifrr.Template, difference being instead of props, Sifrr.Dom passes element itself in binding functions' first argument

Loading element

Note: Sifrr.Dom.load requires Fetch API to work.

  1. Sifrr.Dom.load() - downloads element file (recommended for async loading)
// 3 ways to declare download url for an element with load

// key-value map
const config = {
  urls: {
    'custom-tag': '/custom-tag.js'
  }
};
Sifrr.Dom.load('custom-tag'); // downloads `/custom-tag.js`
// returns a promise resolved after loading the file

// url function
const config = {
  url: name => `/elements/${name}.js`
};
Sifrr.Dom.load('custom-tag'); // downloads `/elements/custom-tag.js`

// url in load
Sifrr.Dom.load('custom-tag', 'https://www.elements.com/custom-tag.js'); // download `https://www.elements.com/custom-tag.js`

Priority Order: url in load function call, url from urls in config, then url from url function.

// controlling order of loading elements
class DependantElement extends Sifrr.Dom.Element {}

// `DependantElement` will be registered after `some-element` is loaded
Sifrr.Dom.load('some-element').then(() => {
  Sifrr.Dom.register(DependantElement);
});
  1. As module
// index.html

<script type="module">
  import '/elements/custom-tag';
</script>
<script src="/elements/custom-tag" type="module">
  1. Normal script tag (recommended for best browser support)
// index.html
<script src="/elements/custom-tag" />

Rendering

This html

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8" />
    <title></title>
  </head>
  <body>
    <!-- Put custom tag anywhere to render that element -->
    <custom-tag></custom-tag>
    <script src="custom-tag.js" charset="utf-8"></script>
    <script src="index.js" charset="utf-8"></script>
  </body>
</html>

with

// custom-tag.js

class CustomTag extends Element {
  static get template() {
    return html`
      <style media="screen">
        p {
          color: blue; // Only applies to p inside this element
        }
      </style>
      <p>${el => el.state.}</p>
      <p attr=${el => el.state.attribute}>${el => el.state.number}</p>
    `; // el is the element instance
  }

  constructor() {
    super();
    this.state = {
      number: 1,
      attribute: 'abcd'
    }
  }
}
Sifrr.Dom.register(CustomTag);

will render to

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8" />
    <title></title>
  </head>
  <body>
    <custom-tag>
      #shadow-root
      <!-- Content will be rendered in shadow root by default -->
      <style media="screen">
        p {
          color: blue; // Only applies to p inside this element
        }
      </style>
      <p attr="abcd">1</p>
    </custom-tag>
    <script src="index.js" charset="utf-8"></script>
  </body>
</html>

Changing state of element

const customtag = window.querySelector('custom-tag');
customtag.setState({ number: 2, attribute: 'xyz' });
// Note: state is functionally immutable, hence doing `customtag.state.id = 2` won't work
// You need to set state to new value every time you need to change state, but don't
// worry. Only new values provided are updated, other values remain same as they were.

This will change custom-tag to

<custom-tag>
  #shadow-root
  <style media="screen">
    p {
      color: blue;
    }
  </style>
  <p attr="xyz">2</p>
</custom-tag>

Changing state automatically triggers element.update() which updates the bindings.

Force update element bindings

customtag.update();

Components Without shadow root

<!-- ${baseUrl}/elements/custom/tag.html -->
<template>
  <style media="screen">
    // Style here will be global
  </style>
  <!-- content -->
</template>
<script type="text/javascript">
  class CustomTag {
    static get useShadowRoot {
      return false;
    }
  }
  // or
  CustomTag.useShadowRoot = false;
</script>

props

  • props do not trigger re-renders, unless set by : or :: prop bindings of Sifrr.Template
  • first argument in props binding function is parent sifrr element
  • : or :: prop bindings don't work when the element has no parent sifrr element
  • props are case insensitive
  • props in hyphen-case will be conveted to camel-case property name, i.e. some-thing => someThing
<custom-tag :prop="${parentElement => parentElement.data}"></custom-tag>

<!-- then you can access property in customTag with `this.prop` -->

Sifrr Element (Sifrr.Dom.Element) Methods

Callbacks

Sifrr wraps some of the original callbacks of Custom Elements API

class CustomTag extends Sifrr.Dom.Element {
  // same as original
  static observedAttributes() {
    return ['custom-attr']; // these attributes will be observed for changes
  }

  onConnect() {
    // called when element is connected to dom
    // A good place to manipulate dom inside the custom element like adding event listeners, etc.
  }

  onDisconnect() {
    // called when element is disconnected to dom
  }

  onAttributeChange(attrName, oldVal, newVal) {
    // called when an attribute in observedAttrs array is changed
  }

  onStateChange(newState) {
    // called when element's state is changed
  }

  beforeUpdate() {
    // called before updates are rendered
  }

  onUpdate() {
    // called when element is updated
  }
}

Clearing state of element (use only if you know what you are doing)

customtag.clearState(); // Not recommended to avoid blank/undefined bindings

Query selectors for custom element content

// querySelector
customtag.$(selector /* shadowRoot = default: true if element uses shadow root else false */);
// querySelectorAll
customtag.$$(selector /* shadowRoot = default: true if element uses shadow root else false */);
// If shadowRoot is true, it selects elements inside element shadowRoot else it will select elements inside it

Sifrr adds $ and $\$ as alias for querySelector and querySelectorAll to all HTMLElements and document. eg. document.$('div').$$('p')

Synthetic events

// example for adding 'click' event listeners, can be replaced with any type of event (even custom events)

// Add synthetic event listerner (only need to be called once for one type of event)
// Can be given in options in Sifrr.Dom.Setup
Sifrr.Dom.Event.add('click');

// Adding event callback on an element (any html element), works inside shadowRoots also (for bubbling events)
// or `:_click` prop binding in Template
el._click = fxn;
// fxn will be called with two arguments `fxn(event, target)` and `this` inside function will be it's parent custom element if available, else window.

// Add _click attribute to html directly
// <a _click="console.log(this, event, target)"></a>

// Adding a generic event callback
Sifrr.Dom.Event.addListener('click', selector, fxn);
// or
Sifrr.Dom.Event.addListener('click', element, fxn);
// fxn will be called with same two arguments as before if event target matches the selector provided

// Triggering custom events
Sifrr.Dom.Event.trigger(target, 'custom:event', options);
// options are same as options for new window.Event(target, 'custom:event', options);

Note: Synthetic event listeners are always passive, hence, event.preventDefault() can not be called inside the function. Use html event listener properties (eg. onclick) if you need event.preventDefault().

More complex apis

Controlled inputs

import { memo } from '@sifrr/template'

<!-- inside template -->
<input :value="${el => el.state.input}" :_input=${memo(el => value => el.setState({ input: value }))} />
<select :value="${el => el.state.select}" :_input=${memo(el => value => el.setState({ select: value }))}>
  <!-- options -->
</select>
<textarea :_input=${memo(el => value => el.setState({ textarea: value }))} :value=${el => el.state.textarea}></textarea>
<div contenteditable :_input=${memo(el => value => el.setState({ elements: value }))}>
  ${el => el.state.elements}
</div>

<!-- One Way bindings to value, updates value/content when state is changed -->
<!-- Use only :value prop without setting :_input_ -->

<!-- One Way bindings from value, updates state when value/content is changed (on input/change event) -->
<!-- Use only :_input prop without setting :value -->

Controlled State

<!-- inside template -->
<!-- One Way bindings to `some-element`'s state, updates state of `some-element` when parent's state is changed -->
<some-element :state="${el => el.state.someElementState}"></some-element>

<!-- One Way bindings from `some-element`'s state, updates parent's state when state of `some-element` is changed -->
<some-element
  :_update="${Sifrr.Template.memo(el => newState => el.setState({ someElementState: newState }))}"
></some-element>

<!-- Both together -->
<!-- This automatically syncs parent's state.someElementState and `some-element`'s state' -->
<some-element
  :state="${el => el.state.someElementState}"
  :_update="${Sifrr.Template.memo(el => newState => el.setState({ someElementState: newState }))}"
></some-element>

Creating another sifrr element programmatically

Sifrr.Dom.createElement(Sifrr.Dom.Element class or Tag name, props to be set, oldValue)

// example binding
`${(parent, oldValue) =>
  Sifrr.Dom.createElement(CustomTag || 'custom-tag', { prop: 'value' }, oldValue)}`;

Extending another html element (doesn't work in safari yet)

Sifrr element can extend other html elements also, eg: CustomTag extends HTMLButtonElement here, note that register call has { extends: 'button' } as second argument

class CustomTag extends Sifrr.Dom.Element.extends(HTMLButtonElement) {}
Sifrr.Dom.register(SifrrSmaller, {
  extends: 'button'
});

then you can use custom-tag as button in html like:

<button is="custom-tag"></button>

Global Stores

import { html, Store } from '@sifrr/template';
import { Element } from '@sifrr/template';

// create a new store
const someStore = new Sifrr.Template.Store({ a: 'b' });

class CustomTag extends Element {
  static get template() {
    return html`
      <style media="screen">
        p {
          color: blue;
        }
      </style>
      <p>${() => someStore.value.a}</p>
    `; // el is the element instance
  }

  constructor() {
    super();
    someStore.addListener(() => {
      this.update(); // update this element whenevr stpre is updated
    });
  }
}
Sifrr.Dom.register(CustomTag); // you should register in file itself to keep this file independently usable/downloadable
module.exports = CustomTag;

// update value, now this will re-render all instances of CustomTag
someStore.set({ a: 'c' });

Execute JS File

Execute a JS file

Sifrr.Dom.Loader.executeJS(url);

slots

  • Slots work same as it would in web components, but note that bindings in slot elements won't work

Example elements

More readings

Special thanks to

0.0.9

3 years ago

0.0.8

3 years ago

0.0.7

3 years ago

0.0.6

5 years ago

0.0.5

5 years ago

0.0.4

5 years ago

0.0.3

5 years ago

0.0.2-alpha

5 years ago

0.0.1-alpha2

5 years ago

0.0.1-alpha1

5 years ago

0.0.1-alpha

5 years ago

0.1.0-alpha4

5 years ago

0.1.0-alpha3

5 years ago

0.1.0-alpha2

5 years ago

0.1.0-alpha1

5 years ago

0.1.0-alpha

5 years ago