├── .editorconfig ├── .gitignore ├── README.md ├── docs └── comparison.md ├── index.d.ts ├── index.js ├── notify.d.ts ├── notify.js ├── package-lock.json ├── package.json ├── sync.d.ts ├── sync.js └── test └── index.html /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = LF 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Change notification helpers for LitElement 2 | 3 | [![npm version](https://img.shields.io/npm/v/@morbidick/lit-element-notify.svg)](https://www.npmjs.com/package/@morbidick/lit-element-notify) 4 | 5 | Small helpers for LitElement to dispatch change notifications and two-way binding. For a comparison to PolymerElement and pure LitElement see [comparison section](docs/comparison.md) in the docs. 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install @morbidick/lit-element-notify 11 | ``` 12 | 13 | ## Notify mixin 14 | 15 | Small mixin for LitElement to get easy change events via the `properties` getter. 16 | 17 | This mixin adds the `notify` option to the property definition. Similar to the LitElement `attribute` option (which reflects a property to the dom) it fires an event as soon as the property value changes. The event name depends on the following conditions: 18 | 19 | 1. `notify: true`: the property gets lowercased and `-changed` is appended (note: contrary to PolymerElement and similar to LitElements attribute handling no camelCase to kebap-case conversion is done). 20 | 2. the notify option contains a string: `notify: 'success-event'` fires an event named `success-event`. 21 | 3. `notify: true` is set and the attribute option is a string (`attribute: 'attribute-name'`): the attribute name will be suffixed with `-changed`. 22 | 23 | The updated value of the property is available in `event.detail.value`. 24 | 25 | ```javascript 26 | import { LitElement, html } from 'lit-element'; 27 | import LitNotify from '@morbidick/lit-element-notify/notify.js'; 28 | 29 | class NotifyingElement extends LitNotify(LitElement) { 30 | static get properties() { 31 | return { 32 | 33 | // property names get lowercased and the -changed suffix is added 34 | token: { 35 | type: String, 36 | notify: true, // fires token-changed 37 | }, 38 | camelCase: { 39 | type: String, 40 | notify: true, // fires camelcase-changed 41 | }, 42 | 43 | // an explicit event name can be set 44 | thing: { 45 | type: String, 46 | notify: 'success-event', // fires success-event 47 | }, 48 | 49 | // if an attribute value is set, -changed is appended 50 | myMessage: { 51 | type: String, 52 | attribute: 'my-message', 53 | notify: true, // fires my-message-changed 54 | }, 55 | 56 | }; 57 | } 58 | } 59 | ``` 60 | 61 | ## Sync directive 62 | 63 | lit-html directive to synchronize an element property to a childs property, adding two-way binding to lit-element. 64 | The directive takes two parameters, the property name and an optional event name on which to sync. 65 | 66 | ### Usage 67 | 68 | ```javascript 69 | import { LitElement, html } from 'lit-element'; 70 | import LitSync from '@morbidick/lit-element-notify/sync.js'; 71 | 72 | class SyncElement extends LitSync(LitElement) { 73 | 74 | // Syncing the child property `token` with the parent property `myProperty` when `token-changed` 75 | // is fired or `myProperty` set. 76 | render() { return html` 77 | 78 | `} 79 | 80 | // Syncing the child property `myMessage` with the event explicitly set to `my-message-changed` 81 | // (mainly used to map from the camelCase property to the kebap-case event as PolymerElement does). 82 | render() { return html` 83 | 84 | `} 85 | 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/comparison.md: -------------------------------------------------------------------------------- 1 | # Comparison 2 | 3 | This section tries to make the individual use cases clearer. 4 | 5 | ## Firing an event on property change 6 | 7 | ### PolymerElement 8 | 9 | ```js 10 | class NotifyingElement extends PolymerElement { 11 | static get properties() { 12 | return { 13 | message: { 14 | type: String, 15 | notify: true 16 | }}}} 17 | ``` 18 | 19 | ### LitElement 20 | 21 | ```js 22 | class NotifyingElement extends LitElement { 23 | static get properties() { 24 | return { 25 | message: { 26 | type: String 27 | }}} 28 | update(props) { 29 | super.update(props); 30 | if (props.has('message')) { 31 | this.dispatchEvent(new CustomEvent('message-changed', { 32 | detail: { 33 | value: this.message, 34 | }, 35 | bubbles: false, 36 | composed: true 37 | })); 38 | } 39 | } 40 | ``` 41 | 42 | ### LitElement with LitNotify mixin 43 | 44 | ```js 45 | class NotifyingElement extends LitNotify(LitElement) { 46 | static get properties() { 47 | return { 48 | message: { 49 | type: String, 50 | notify: true 51 | }}}} 52 | ``` 53 | 54 | mapping a camelCase property to a kebap-case event (as PolymerElement does automaticly) 55 | 56 | ```js 57 | class NotifyingElement extends LitNotify(LitElement) { 58 | static get properties() { 59 | return { 60 | myMessage: { 61 | type: String, 62 | notify: 'my-message-changed' 63 | }}}} 64 | ``` 65 | 66 | ## Two-way data binding 67 | 68 | Synchronizing a parent property with a childs property. 69 | 70 | ### PolymerElement* 71 | 72 | ```js 73 | html`` 74 | ``` 75 | 76 | ### Lit Element - upwards binding only 77 | 78 | ```js 79 | html` this.myProperty = e.detail.value}>` 80 | ``` 81 | 82 | ### LitElement* 83 | 84 | ```js 85 | html` this.myProperty = e.detail.value}>` 86 | ``` 87 | 88 | ### LitElement with sync directive* 89 | 90 | ```js 91 | html`` 92 | ``` 93 | 94 | * two-way binding so also updating the child when the parent property changes 95 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export { LitNotify } from './notify'; 2 | export { LitSync } from './sync'; 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {LitNotify} from "./notify.js"; 2 | export {LitSync} from "./sync.js"; 3 | -------------------------------------------------------------------------------- /notify.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyDeclaration, LitElement, UpdatingElement } from 'lit-element'; 2 | 3 | type Constructor = new (...args: any[]) => T; 4 | 5 | interface AugmentedPropertyDeclaration extends PropertyDeclaration { 6 | /** When true will notify. Pass a string to define the event name to fire. */ 7 | notify: string|Boolean 8 | } 9 | 10 | declare class NotifyingElement { 11 | static createProperty(name: string, options: AugmentedPropertyDeclaration): void 12 | } 13 | 14 | export function LitNotify(baseElement: Constructor): T & NotifyingElement 15 | -------------------------------------------------------------------------------- /notify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the event name for the given property. 3 | * @param {string} name property name 4 | * @param {PropertyDeclaration} options property declaration 5 | * @return event name to fire 6 | */ 7 | export function eventNameForProperty(name, { notify, attribute } = {}) { 8 | if (notify && typeof notify === 'string') { 9 | return notify; 10 | } else if (attribute && typeof attribute === 'string') { 11 | return `${attribute}-changed`; 12 | } else { 13 | return `${name.toLowerCase()}-changed`; 14 | } 15 | } 16 | 17 | // eslint-disable-next-line valid-jsdoc 18 | /** 19 | * Enables the nofity option for properties to fire change notification events 20 | * 21 | * @template TBase 22 | * @param {Constructor} baseElement 23 | */ 24 | export const LitNotify = (baseElement) => class NotifyingElement extends baseElement { 25 | /** 26 | * check for changed properties with notify option and fire the events 27 | */ 28 | update(changedProps) { 29 | super.update(changedProps); 30 | 31 | for (const prop of changedProps.keys()) { 32 | const declaration = this.constructor._classProperties.get(prop) 33 | if (!declaration || !declaration.notify) continue; 34 | const type = eventNameForProperty(prop, declaration) 35 | const value = this[prop] 36 | this.dispatchEvent(new CustomEvent(type, { detail: { value } })); 37 | } 38 | } 39 | }; 40 | 41 | export default LitNotify; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@morbidick/lit-element-notify", 3 | "version": "1.1.1", 4 | "description": "Small helpers for LitElement to dispatch change notifications and two-way binding", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/morbidick/lit-element-notify" 9 | }, 10 | "main": "index.js", 11 | "module": "index.js", 12 | "peerDependencies": { 13 | "lit-element": "^2.0.1", 14 | "lit-html": "^1.0.0" 15 | }, 16 | "devDependencies": { 17 | "@webcomponents/webcomponentsjs": "^2.2.6", 18 | "lit-element": "^2.0.1", 19 | "lit-html": "^1.0.0", 20 | "polyserve": "^0.27.15" 21 | }, 22 | "scripts": { 23 | "start": "npm run serve", 24 | "serve": "polyserve --npm --module-resolution=node" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sync.d.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, UpdatingElement } from 'lit-element'; 2 | 3 | type Constructor = new (...args: any[]) => T; 4 | 5 | declare class SyncElement { 6 | sync(property: string, eventName?: string): void 7 | } 8 | 9 | export function LitSync(baseElement: Constructor): T & SyncElement 10 | -------------------------------------------------------------------------------- /sync.js: -------------------------------------------------------------------------------- 1 | import {directive} from "lit-html/lib/directive.js"; 2 | import {eventNameForProperty} from "./notify.js"; 3 | 4 | // eslint-disable-next-line valid-jsdoc 5 | /** 6 | * Mixin that provides a lit-html directive to sync a property to a child property 7 | * 8 | * @template TBase 9 | * @param {Constructor} baseElement 10 | */ 11 | export const LitSync = (baseElement) => class extends baseElement { 12 | constructor() { 13 | super(); 14 | 15 | /** 16 | * lit-html directive to sync a property to a child property 17 | * 18 | * @param {string} property - The property name 19 | * @param {string} [eventName] - Optional event name to sync on, defaults to propertyname-changed 20 | */ 21 | this.sync = directive((property, eventName) => (part) => { 22 | part.setValue(this[property]); 23 | 24 | // mark the part so the listener is only attached once 25 | if (!part.syncInitialized) { 26 | part.syncInitialized = true; 27 | 28 | const notifyingElement = part.committer.element; 29 | const notifyingProperty = part.committer.name; 30 | const notifyingEvent = eventName || eventNameForProperty(notifyingProperty); 31 | 32 | notifyingElement.addEventListener(notifyingEvent, (e) => { 33 | const oldValue = this[property]; 34 | this[property] = e.detail.value; 35 | if (this.__lookupSetter__(property) === undefined) { 36 | this.updated(new Map([[property, oldValue]])); 37 | } 38 | }); 39 | } 40 | }); 41 | } 42 | } 43 | 44 | export default LitSync; 45 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 58 | 59 | --------------------------------------------------------------------------------