0.3.1 • Published 5 years ago

html-element-mixins v0.3.1

Weekly downloads
-
License
MIT
Repository
-
Last release
5 years ago

# html-element-mixins

A collection of mixins enhancing HTMLElement, powering CustomElements.

Installation

$ npm install html-element-mixins

Introduction

Set menu or à la carte?

Custom Elements are a powerful part of the Web Components standard. As its built-in functionality is limited, web developers need a little sugar to make life easier.

Custom base classes like LitElement offer a great deal of convenience, but they introduce a new problem.

Using a custom base class is like ordering a set menu in a restaurant: The cook (code author) serves a fixed amount of dishes (functionality) for a fixed price (bundle size, execution time). Set menus are convenient, and the food can be really tasty. However, one may eat too much or dislike some of the dishes.

html-element-mixins is like ordering à la carte. It's a collection of mixins (dishes) that can be added to HTMLElement. As a developer, it's up to you which ones to add (order). As a result, one gets exactly the functionality (dishes) they need, therefore allowing you to build your own base class (eat à la carte!), eating only the dishes you love (minimal overhead, bundle size & execution time).

Collection

html-element-mixins is a collection of mixins, extending the power of the HTMLElement class. In short, this is what they are about:

  1. ObservedProperties enables observed properties (just like built-in observedAttributes).
  2. DOMProperties enables attribute to property synchonisation.
  3. ReflectedProperties enables property to attribute synchonisation.

Furthermore, we created a bunch of addons:

  1. PropertiesChangedCallback Debounces / batches property changes for efficient DOM-rendering.
  2. PropertyChangedHandler enables change handlers methods on property changes.
  3. PropertiesChangedHandler adds change handlers methods for multiple property changes.
  4. PropertyChangedEvent dispatches events upon property changes.
  5. Property wraps all property mixins a single configuration.
  6. EnsuredAttributes adds default values for (non-observing) attributes.
  7. KeyBindings adds default values for (non-observing) attributes.

ObservedProperties

import { ObservedProperties } from 'html-element-mixins';

Observing

By default, Custom Elements can observe attribute value changes whitelisted in the observedAttributes Array. ObservedProperties offers a similar solution for DOM properties using observedProperties. When a property has changed, propertyChangedCallback is called, passing the property name, the old value and the new value.'

class DemoElement extends ObservedProperties(HTMLElement) {

  static get observedProperties() {
    return ['firstName', 'lastName', 'age']
  }

  propertyChangedCallback(propName, oldValue, newValue) {
    console.info(`${propName} changed from ${oldValue} to ${newValue}`);
  }
  
}

If you like you can add your own getter / setter pairs:

  static get observedProperties() {
    return ['initials']
  }

  get initials() {
    return this._initials;
  }

  set initials(val) {
    this._initials = val.toUpperCase();
  }

  constructor() {
    this.initials = 'a.b.c.';
  }

  propertyChangedCallback(propName, oldValue, newValue) {
    console.info(`${propName} changed to ${newValue}`); //initials changed to A.B.C;
  }

Accessors don't require a getter / setter pair. Keep in mind though that by default, private property values are assigned using the following pattern: #${propName}.

static get observedProperties() {
  return ['firstName']
}

get firstName() {
  return this['#firstName'].toLowerCase()
}

DOMProperties

Some native properties (e.g. input value) can be set using a DOM attribute. This mixin adds exactly this behavior: attribute to property sync:

  import { DOMProperties } from 'html-element-mixins';
  
  class DemoElement extends DOMProperties(HTMLElement) {

    static get DOMProperties() {
      return ['firstname', 'lastname']
    }

  }
<demo-element
  id="demo"
  firstname="Adewale"
  lastname="King"
></demo-element>

<script>
  console.info(demo.firstname, demo.lastname); // Adewale, King
</script>

By default, attributes are lowercased property names (e.g. 'myPropName' becomes 'mypropname'). You can configure custom attribute mappings using 'propertyAttributeNames':

    
    static get DOMProperties() {
      return ['myBestFriend']
    }

    static get propertyAttributeNames() {
      return {
        myBestFriend: 'my-best-friend',
      }
    }
  <demo-element
    id="demo"
    my-best-friend="Hellen"
  ></demo-element>

Attribute Getters

Attribute values are always strings. For boolean, numeric or JSON like property values, this may result in property values. Using the 'propertyAttributeGetters' config, one can add getter functions for attributes when properties are set:

static get reflectedProperties() {
  return ['married', 'friends']
}

static get propertyAttributeGetters() {
  return {
    married: function(value) {
      if(value === '') return true;
      return false;
    },
    friends: function(value) {
      if(!value) return null;
      return JSON.parse(value);
    }
  }
}
<demo-element
  id="demo"
  married
  friends='["Gabriella","Anik","Linda"]'
></demo-element>

<script>
  console.info(demo.married, demo.friends); //true, ['Gabriella','Anik','Linda'];
</script>

html-element-mixin comes with a set of attribute getters for Boolean, String, Number and JSON compatible types:

  import { ParseString, ParseNumber, ParseBoolean, ParseJSON } from 'html-element-mixins/utils/attribute-accessors';

  static get propertyAttributeGetters() {
    return {
      firstName: ParseString.fromAttribute,
      age: ParseNumber.fromAttribute,
      married: ParseBoolean.fromAttribute,
      friends: ParseJSON.fromAttribute
    }
  }

ReflectedProperties

import { ReflectedProperties, ObservedProperties } from 'html-element-mixins';

This enables property to attribute sync. Using the 'reflectedProperties' object, one can map properties (keys) to attributes (values). The ObservedProperties mixin is required.

class DemoElement extends ReflectedProperties(ObservedProperties(HTMLElement)) {

  static get observedProperties() {
    return ['firstname', 'lastname', 'age']
  }

  static get reflectedProperties() {
    return ['firstname', 'lastname', 'age']
  }

  constructor() {
    this.firstname = 'Amira';
    this.firstname = 'Arif';
    this.age = 24;
  }

}

By default, attributes are lowercased property names (e.g. 'myPropName' becomes 'mypropname'). You can configure custom attribute mappings using 'propertyAttributeNames':

    
    static get reflectedProperties() {
      return ['firstName']
    }

    static get propertyAttributeNames() {
      return {
        firstName: 'first-name',
      }
    }
  <demo-element first-name="Amira"></demo-element>

Attribute Setters

Attribute values are always strings. For boolean, numeric or JSON like property values, this may result in undesired attribute reflections. Using the 'propertyAttributeSetters' config, one can add setter functions for reflected properties. Attributes are set based on the return value of these functions: when false or undefined, the removeAttribute is called. Otherwise, setAttribute is called using the return value:

static get reflectedProperties() {
  return ['married', 'friends']
}

static get propertyAttributeSetters() {
  return {
    married: function(value) {
      return value === true;
    },
    friends: function(value) {
      if(!value) return;
      return JSON.stringify(value);
    }
  }
}

constructor() {
  this.married = false;
  this.friends = ['Gabriella', 'Anik', 'Linda'];
}
<demo-element
  married
  friends='["Gabriella","Anik","Linda"]'
></demo-element>

html-element-mixins come with a set of attribute parsers for Boolean, String, Number and JSON compatible types:

  import { ParseString, ParseNumber, ParseBoolean, ParseJSON } from 'html-element-mixins/utils/attribute-accessors';

  static get propertyAttributeSetters() {
    return {
      firstName: ParseString.toAttribute,
      age: ParseNumber.toAttribute,
      married: ParseBoolean.toAttribute,
      friends: ParseJSON.toAttribute
    }
  }

PropertiesChangedCallback

import { PropertiesChangedCallback, ObservedProperties } from 'html-element-mixins';

When declaring observed properties using the observedProperties array, property changes are fired each time a a property changes using the propertyChangedCallback. For efficiency reasons (e.g. when rendering DOM), the propertiesChangedCallback (plural!) can be used. This callback is debounced by cancel / requestAnimationFrame on every property change. In the following example, render is invoked only once:

import { PropertiesChangedCallback } from 'html-element-mixins/src/properties/addons';
import { ObservedProperties } from 'html-element-mixins';

class DemoElement extends PropertiesChangedCallback(ObservedProperties(HTMLElement)) {

  constructor() {
    super();
    this._renderCount = 0;
  }

  static get observedProperties() {
    return ['firstName', 'lastName', 'age'];
  }

  propertiesChangedCallback(propNames, oldValues, newValues) {
    this._renderCount++;
    this.render();
  }

  render() {
    this.innerHTML = `
      Hello, ${this.firstName} ${this.lastName} (${this.age} years).<br>
      Render Count = ${this._renderCount}. 
    `
  }

  constructor() {
    super();
    this.firstName = 'Amina';
    this.lastName = 'Hamzaoui';
    this.age = 24;
  }

}

PropertyChangedHandler

import { ObservedProperties } from 'html-element-mixins';
import { PropertyChangedHandler } from 'html-element-mixins/src/properties/addons';

Value changes to properties whitelisted in the observedProperties array are always notified using propertyChangedCallback. PropertyChangedHandler provides for custom callbacks for property changes:

class DemoElement extends PropertyChangedHandler(ObservedProperties((HTMLElement)) {
  static get observedProperties() {
    return ['firstName']
  }

  static get propertyChangedHandlers() {
    return {
      firstName: function(newValue, oldValue) {
        console.info('firstName changed!', newValue, oldValue);  
      }
    }
  }
}

Alternatively, callbacks can be passed as string references:

static get propertyChangedHandlers() {
  return { firstName: '_firstNameChanged' }
}

_firstNameChanged(newValue, oldValue) {
  console.info('firstName changed!', newValue, oldValue);
}

Note: PropertyChangedHandler should always be used in conjunction with ObservedProperties.

PropertiesChangedHandler

import { ObservedProperties } from 'html-element-mixins';
import { PropertiesChangedHandler } from 'html-element-mixins/src/properties/addons';

Its plural companion propertiesChangedHandlers can be used to invoke a function when one of many properties have changed. Key / value pairs are now swapped. A key refers to the handler function, the value holds an array of the observed properties.

class DemoElement extends PropertiesChangedHandler(ObservedProperties((HTMLElement)) {
  static get observedProperties() {
    return ['firstName', 'lastName']
  }

  static get propertiesChangedHandlers() {
    return {
      _nameChanged: ['firstName', 'lastName']
    }
  }

  _nameChanged(propNames, newValues, oldValues) {
    console.info(newValues.firstName, newValues.lastName);
  }

}

Note: PropertiesChangedHandler should always be used in conjunction with ObservedProperties.

PropertyChangedEvent

import { ObservedProperties } from 'html-element-mixins';
import { PropertiesChangedEvent } from 'html-element-mixins/src/properties/addons';

This mixin dispatches Events for changed properties whitelisted in the propertiesChangedEventNames object.

class DemoElement extends PropertyChangedEventDispatcher(ObservedProperties((HTMLElement)) {
  static get observedProperties() {
    return ['firstName', 'age']
  }

  static get propertyChangedEventNames() {
    return {
      firstName: 'first-name-changed',
    }
  }
  
  constructor() {
    super();
    this.firstName = 'John';
  }
}
<demo-element id="demo"></demo-element>
<script>
demo.addEventListener('first-name-changed', (e => {
  console.info(e.detail); //{value: 'John'}
})
</script>

You can also configure your own event, using propertyChangedEvent. This enables you to fully control the event params:

class DemoElement extends PropertyChangedEventDispatcher(ObservedProperties((HTMLElement)) {
  static get observedProperties() {
    return ['firstName', 'age']
  }

  static get propertiesChangedEventNames() {
    return {
      firstName: 'first-name-changed',
      ageChanged: 'age-changed'
    }
  }

  static get propertyChangedEvent() {
    return (eventName, propName, oldValue, newValue) => {
      return new CustomEvent(eventName, {
        detail: {eventName, propName, oldValue, newValue},
        bubbles: true,
        composed: true
      });            
    };
  }
  
  constructor() {
    super();
    this.firstName = 'John';
  }
}  
<demo-element id="demo"></demo-element>
<script>
demo.addEventListener('first-name-changed', (e => {
  console.info(e.detail); //{propName: 'age', oldValue: undefined, newValue: 'John'}
})
</script>

Note: PropertyChangedEventDispatcher should always be used in conjunction with ObservedProperties.

Property

import { Property } from 'html-element-mixins';

This wraps all property mixins into a single properties configuration object.

class DemoElement extends Property(HTMLElement) {

  static get properties() {
    return {
      firstName: {
        observe: true, //add to `observedProperties` array
        DOM: true, //add to `DOMProperties` array
        reflect: true, //add to `reflectedProperties` array
        attributeName: 'first-name', //custom attribute map
      }
    }
  }

}

Attribute accessors can be added using attributeGetter and attributeSetter:

  static get properties() {
    return {
      firstName: {
        observe: true,
        DOM: true,
        reflect: true,
        attributeSetter: ParseString.toAttribute,
        attributeGetter: ParseString.fromAttribute
      }
    }
  }

You may very well use the propertyChangedHandler and propertyChandedEvent addons:

import { PropertyChangedHandler, PropertyChangedEvent } from 'html-element-mixins/properties/addons';

static get properties() {
  return {
    firstName: {
      observe: true,
      DOM: true,
      reflect: true,
      changedHandler: function(oldValue, newValue) { console.info(`firstname changed to ${newValue}`); },
      changedEventName: 'first-name-changed'
    },    
  }
}
<demo-element id="demo"></demo-element>
<script>
  demo.addEventListener('last-name-changed', (e) => {
    console.info('last name changed!', e.detail);
  })
</script>

DefaultAttributes

import { DefaultAttributes } from 'html-element-mixins/src/misc';

This ensures attributes are added to an element when added to a DOM tree, if no value is set. Attribute key/value pairs are configured via defaultAttributes:

class DemoElement extends DefaultAttributes(HTMLElement) {

  static get defaultAttributes() {
    return {
      tabIndex: 0,
      role: 'button',
      active: ''
    }
  }
  
}
<demo-element tabindex="0" role="button" active></demo-element>

Keep in mind that these attributes are only set once (after connectedCallback) and should only be used for non-observing attributes.

KeyBindings

import { KeyBindings } from 'html-element-mixins/src/misc';
      
class DemoElement extends KeyBindings(HTMLElement) {

  static get keyDownBindings() {
    return {
      'save': [
        {key: 's', metaKey: true},
        {key: 's', ctrlKey: true}
      ],
    };
  }

  static get keyPressBindings() {
    return {
      'close': [{code: 'Escape'},
      ],
    };
  }

  static get keyUpBindings() {
    return {
      'delete': [
        {code: 'Backspace'}
      ]
    };
  }
  save(e) {
    e.preventDefault();
    alert('Save!');
  }

  close() {
    alert('Close!');
  }

  delete() {
    alert('delete!')
  }
  
}
0.3.1

5 years ago

0.3.0

5 years ago

0.2.3

5 years ago

0.2.2

5 years ago

0.2.4

5 years ago

0.2.1

5 years ago

0.2.0

5 years ago

0.15.2

5 years ago

0.15.1

5 years ago

0.15.0

5 years ago

0.14.0

5 years ago

0.13.0

5 years ago

0.12.0

5 years ago

0.11.0

5 years ago

0.10.1

5 years ago

0.10.0

5 years ago

0.9.1

5 years ago

0.9.0

5 years ago