Skip to content

Releases: lume/element

v0.16.0 - stay classy

26 May 08:16
Compare
Choose a tag to compare

What's Changed

  • @element options via static class fields, @jsonAttribute, and improvements by @trusktr in #46

    Add a new @jsonAttribute decorator for element attributes that accept JSON strings, add an object-based format for @element decorator options, accept both static elementName and static autoDefine options from static class fields as an alternative to options passed to the @element decorator, fall back to using the class constructor name to derive an element name when the name has not been specified, and improve the underlying attribute definition implementation to use half the number of objects which in turn no longer requires the non-decorator attribute types used in static observedAttributeHandlers to be called as functions.

    • BREAKING: To make the new object-based @element(options) format work well, along with static class field options, passing no arguments to @element (or passing an empty string) no longer causes the customElements definition to be skipped. @element class MyClass ... will now automatically define the element with the name my-el derived from the dash-cased constructor name instead of skipping definition.
      • Migration: to prevent automatic element definition, explicitly set autoDefine to false rather than relying on zero arguments or an empty string argument, f.e. @element('', false) class MyEl ..., @element({autoDefine: false}) class MyEl ..., or @element class MyEl extends Element { static autoDefine = false; ...}. For example, before:
        @element
        class MyEl extends Element {}
        MyEl.defineElement('with-any-name')
        after (recommended method):
        @element
        class MyEl extends Element {
          static autoDefine = false
        }
        MyEl.defineElement('with-any-name')
    • BREAKING: The attribute.* types for use with the non-decorator static observedAttributeHandlers API are no longer functions.
      • Migration: Convert attribute.type() to attribute.type. For example, before:
        class MyEl extends Element {
          static observedAttributeHandlers = {
            count: attribute.number(),
            hasGold: attribute.boolean(),
          }
        }
        after:
        class MyEl extends Element {
          static observedAttributeHandlers = {
            count: attribute.number,
            hasGold: attribute.boolean,
          }
        }

    Examples:

    New, options for @element via static class fields:

    @element
    class MyElement extends Element {
      static elementName = 'my-el'
    }
    @element
    class MyElement extends Element {
      static elementName = 'my-el'
      static autoDefine = false
    }
    
    MyElement.defineElement()

    New @jsonAttribute decorator for json-handling attributes:

    Given this element definition,

    @element
    class MyEl extends Element {
      @jsonAttribute data: SomeType = {someValue: 123}
    }

    we can pass JSON strings for deserialization via HTML/DOM attributes:

    <my-el id="myEl" data='{"someValue": 456}'></my-el>
    
    <script>
      console.log(myEl.data.someValue) // 456
    </script>

    New object-based options for @element:

    @element({elementName: 'my-el'})
    class MyElement extends Element {}
    @element({elementName: 'my-el', autoDefine: false})
    class MyElement extends Element {}
    MyElement.defineElement()

    Automatically derive the dash-cased element name from the class constructor:

    @element
    class MyEl extends Element {}
    
    console.log(MyEl.elementName) // 'my-el'
    console.log(document.createElement('my-el') instanceof MyEl) // true
    @element class MyEl extends Element {
      static autoDefine = false
    }
    
    const el = document.createElement('my-el')
    console.log(el instanceof MyEl) // false
    MyEl.defineElement() // defines <my-el>
    console.log(el instanceof MyEl) // true

    Simplified non-decorator static observedAttributeHandlers:

    element(class MyEl extends Element {
      static elementName = 'my-el' // This field is not necessary, unless your minifier renames classes or you want to re-use the string type in TypeScript.
    
      static observedAttributeHandlers = {
        someValue: attribute.number,
      }
    
      someValue = 123;
    })

Full Changelog: v0.15.0...v0.16.0

v0.15.0 - write once use anywhere

06 Mar 07:25
Compare
Choose a tag to compare

What's Changed

  • improve the JSX type helpers, add type helpers and examples helpers for Preact, Vue, Svelte, Stencil.js, and Angular by @trusktr in #35

Details:

feat: Attribute handlers for static observedAttributeHandlers or @attribute now accept new options:

export type AttributeHandler<T = any> = {
  // ...

  /**
   * A side effect to run when the value is set on the JS property. It also
   * runs once with the initial value. Avoid this if you can, and instead use
   * Solid effects. One use case of this is to call addEventListener with
   * event listener values immediately, just like with native `.on*`
   * properties.
   */
  sideEffect?: (instance: Element, prop: string, propValue: T) => void

  /**
   * Whether to convert the property name to dash-case for the attribute name.
   * This option is ignore if the `name` option is set.
   *
   * The default is `true`, where the attribute name will be the same as the
   * property name but dash-cased (and all lower case). For example, `fooBar`
   * becomes `foo-bar` and `foo-bar` stays `foo-bar`.
   *
   * If this is set to `false`, the attribute name will be the same as the
   * property name, but all lowercased (attributes are case insensitive). For
   * example `fooBar` becomes `foobar` and `foo-bar` stays `foo-bar`.
   *
   * Note! Using this option to make a non-standard prop-attribute mapping
   * will result in template type definitions (f.e. in JSX) missing the
   * customized attribute names and will require custom type definition
   * management.
   */
  dashcase?: boolean

  /**
   * The name of the attribute to use. Use of this options bad practice to be
   * avoided, but it may be useful in rare cases.
   *
   * If this is not specified, see `dashcase` for how the attribute name is
   * derived from the property name.
   *
   * Note! Using this option to make a non-standard prop-attribute mapping
   * will result in template type definitions (f.e. in JSX) missing the
   * customized attribute names and will require custom type definition
   * management.
   */
  name?: string

  /**
   * Whether to suppress warnings about the attribute attribute name clashes
   * when not using default `dashcase` and `name` settings. This is
   * discouraged, and should only be used when you know what you're doing,
   * such as overriding a property that has `dashcase` set to `false` or
   * `name` set to the same name as the attribue of another property.
   */
  noWarn?: boolean
}

feat: New on* event handler properties that are similar to native on*

properties such as el.onclick.

When a builtin click event is dispatched, the element's el.onclick() method
is called automatically if it exists. The same can now be achieved with custom
events, so that for example el.dispatchEvent(new Event('myevent')) will also
cause el.onmyevent() to be called if it exists.

Example:

import {Element, element, eventAttribute} from '@lume/element'

// This element will dispatch "foo" events, and `el.onfoo = () => {}` can be
// used for listening to them.
@element('my-el')
export class MyEl extends Element {
  @eventAttribute onfoo = null

  connectedCallback() {
    super.connectedCallback()

    el.dispatchEvent(Object.assign(new FooEvent(), {num: 456}))
  }
}

// A good practice is to make a specific Event subclass for any custom events:
export class FooEvent extends Event {
  num = 123

  constructor() {
    super('foo')
  }
}
import './my-el.js'

const el = new MyEl()

el.onfoo = event => console.log('foo event!', event.num)

document.body.append(el) // logs "foo event! 456"

If not using decorators, define the event attribute with static observedAttributeHandlers:

import {Element, element, attribute} from '@lume/element'

// This element will dispatch "foo" events, and `el.onfoo = () => {}` can be
// used for listening to them.
export const MyEl = element('my-el')(
  class extends Element {
    static observedAttributeHandlers = {onfoo: attribute.event()}
    onfoo = null

    // ...
  },
)

If you're using TypeScript, define the event property with EventListener<EventType>:

import {Element, element, eventAttribute, type EventListener} from '@lume/element'

@element('my-el')
export class MyEl extends Element {
  @eventAttribute onfoo: EventListener<FooEvent> | null = null

  // ...
}

feat: improved JSX type helpers:

  • Type helpers include ElementAttribute (Solid.js JSX), ReactElementAttributes (React and Preact), SvelteElementAttributes, StencilElementAttributes, and VueElementAttributes
  • See examples/kitch-sink-* folders for examples of defining an element with type checking and intellisense in various frameworks.
  • on* event properties (for example onmy-event) can now be specified to
    define event props for JSX. The README has an example, plus the
    framework-types/*.test.tsx files show type test cases for JSX frameworks.
    • Their type will be mapped with string values, as they can accept code
      strings just like native events (f.e. onmy-event="console.log('my-event')".
  • For Solid specifically, all the on* properties are also mapped to on:* JSX
    props with function types.
  • Any boolean JSX props will also accept "true" and "false" string values.
  • Any number JSX props will also accept number strings, f.e. "1.23".
  • For Solid specifically, automatically map prop types for attr:, prop:, and
    bool: prefixed JSX props.
    • attr: props mapped from boolean JS properties will specifically accept
      "true" and "false" strings, or actual booleans (they become strings).
    • attr: props mapped from number JS properties will specifically accept
      number strings, f.e. "1.23", or actual numbers (they become strings).
    • attr:on* props mapped from event JS properties will specifically accept
      any string (it should be code), f.e. "console.log('my-event')".
    • Only boolean JS properties will map to bool: JSX props, and bool:
      will not be available for props of any other type.
      • bool: props will accept only actual booleans, and not the "true" or
        "false" strings (otherwise Solid will set the attribute to always exist
        regardless of the value string values, and this is not an issue with React
        props because React props always map to JS properties never to attributes
        when it comes to Lume Elements).
      • The attr: props will accept only "true" or "false" strings, or actual
        booleans (they get converted to strings).
      • The non-prefixed JSX props, and prop: props, will accept both booleans
        and "true" or "false" strings.
    • Number JS properties are mapped to JSX props that accept numbers as well as
      number strings:
      • The attr: props accept number strings, or actual numbers (they get
        converted to strings).
      • There are no bool: props mapped for number properties.
  • POSSIBLY BREAKING: This update adds to and improves JSX types in
    various ways, which has a chance of being breaking. For example a
    @ts-expect-error comment could start to throw an error, or if you had a
    typed JSX prop like onerous in Solid.js for an element whose JSX types you
    defined with ElementAttributes, there might be a type error because the type
    definition will expect a function that handles an Event (named "erous"), but this scenario is unlikely. To migrate, use attr: or prop:
    prefixes to be explicit, for example prop:onerous={notAnEventHandler}. If
    any issues, please find our community chat or forum at https://lume.io and
    reach out!

Full Changelog: v0.14.0...v0.15.0

v0.14.0 - JSX Intellisense

10 Oct 22:12
Compare
Choose a tag to compare

What's Changed

feat: improve the ElementAttributes and ReactElementAttributes JSX type helpers. by @trusktr in #34

This improves JSX types for Custom Elements in Solid JSX, React JSX, and Preact JSX, especially in React/Preact JSX whereas previously the React/Preact JSX prop types only accepted string values for dash-cased attributes.

If you have a getter/setter property in your element class, you can now define a dummy property prefixed with __set__<name-of-the-setter> to specify the type of the setter, and this will be picked up and lead to improved types in JSX. For example, you can start using like so:

Before:

@element('some-element')
class SomeElement extends Element {
    @attribute get someProp(): number {...}
    @attribute set someProp(n: number | 'foo' | 'bar') {...}
}

declare module 'react' {
    namespace JSX {
        interface IntrinsicElements {
            'some-element': ReactElementAttributes<SomeElement, 'someProp'>
        }
    }
}

and this JSX would have a type error:

return <some-element some-prop={'foo'} /> // Error: string is not assignable to number

After:

@element('some-element')
class SomeElement extends Element {
    @attribute get someProp(): number {...}
    @attribute set someProp(n: this['__set__someProp']) {...}

    /** don't use this property, it is for JSX types. */
    __set__someProp!: number | 'foo' | 'bar'
}

// ... the same React JSX definition as before ...

and now JSX prop types will allow setting the setter types:

return <some-element someProp={'foo'} /> // No error, yay!

Note, the property is camelCase instead of dash-case now.

BREAKING: This may introduce type errors into existing JSX templates, tested with React 19 (not tested with React 18 or below), but it is an inevitable upgrade for the better.
To migrate, there's likely nothing to do in Solid JSX, but in React JSX the selected properties are no longer converted to dash-case, so you'll want to use the original JS property names in React JSX templates. For example this,

return <some-element some-prop={...} />

becomes

return <some-element someProp={...} />

If you have any issues, please reach out here on GitHub or on Discord!


Full Changelog: v0.13.1...v0.14.0

v0.13.1

27 Sep 08:11
Compare
Choose a tag to compare

What's Changed

  • document the new static observedAttributeHandlers by @trusktr in #28
  • improve the static LumeElement.defineElement() method by @trusktr in #29
  • improve attribute handling by @trusktr in #30
  • rename root to templateRoot, update docs by @trusktr in #31

Details

feat:

Update classy-solid, which now requires placing getter/setter attribute decorators to be placed on both the getter and setter.

BREAKING: If you previously use an attribute decorator on a getter/setter, you need to ensure that you write the decorator on both the getter and the setter:

// BAD:
@element('my-el')
class MyEl extends Element {
    #foo = 123

    @numberAttribute
    get foo() {return this.#foo}
    set foo(n) {this.#foo = n}
}

// GOOD:
@element('my-el')
class MyEl extends Element {
    #foo = 123

    @numberAttribute
    get foo() {return this.#foo}
    @numberAttribute
    set foo(n) {this.#foo = n}
}

If you're using @noSignal, make sure to duplicate that on any getter/setter too:

@element('my-el')
class MyEl extends Element {
    #foo = 123

    @numberAttribute
    @noSignal
    get foo() {return this.#foo}
    @numberAttribute
    @noSignal
    set foo(n) {this.#foo = n}
}

feat:

Increase interoperability by enforcing that JS values null and string values coming from attributes are treated the same as the values being set on to the respective JS properties. Basically, any null or string value assigned to an attribute JS property will be handled the same way as if removing the espective attribute or setting the respective attribute's value.

BREAKING: If you previously relied on setting an attribute-decorated JS property to a null value or string value without the string being coerced, you'll need to find another approach. All null values and string values assigned to attribute JS properties are now coerced just the same as string values from the attributes are. This means that this

el.someBoolean = 'false'

now behaves the same as this

el.setAttribute('some-boolean', 'false')

when the someBoolean property was decorated with @booleanAttribute decorator. Previously, the JS property value would have been the string literal "false" but now it will be false.

This improves support for frameworks like Preact which pass string values as-is from JSX props to custom element JS properties. Before this change, unexpected string values would break Lume, but now string values are properly coerced no matter how they arrive to the JS property (it'll work better anywhere, f.e. Vue, Svelte, Angular, etc).

feat:

A new .shadowOptions instance property allows defining the options for an element's ShadowRoot. For example:

@element('my-el')
class MyEl extends Element {
    shadowOptions = { mode: 'closed' }
}

deprecation:

root has been renamed to templateRoot to make it more obvious what it is for. The old root property will be deleted in an upcoming release.

Full Changelog: v0.12.3...v0.13.1

v0.12.3

15 Sep 08:06
Compare
Choose a tag to compare

What's Changed

  • feat: Deprecate the ability to define static observedAttributes as a map of attribute names to attribute handlers for non-decorator users, and instead create a new static observedAttributeHandlers for that purpose. by @trusktr in #27
    • This keeps the type of static observedAttributes clean and aligned with the DOM spec, while the new static observedAttributeHandlers has the type definition for the library-specific feature. Plus this prevents type issues downstream because the @element decorator (callable as a function when not using decorators) coerces the object format into the standard array format, yet the object type will still sppear in subclasses despite that it is never an object in subclasses.
      • deprecation: static observedAttributes = {} is deprecated, and only the standard static observedAttribu tes = [] should be used. The object format will be removed in a breaking version bump.
    • infra: Unrestrict the typescript version so we can keep it updated with other packages in the lume repo.

Full Changelog: v0.12.1...v0.12.3

v0.12.1

12 Sep 02:21
Compare
Choose a tag to compare

What's Changed

Full Changelog: v0.12.0...v0.12.1

v0.12.0 - check yoself befo you wreck yoself

10 Sep 06:11
Compare
Choose a tag to compare

Fixes:

  • fix: enable noUncheckedIndexedAccess in tsconfig and fix type errors, preventing errors in downstream projects when they've enabled noUncheckedIndexedAccess too. 20893e7
  • fix: improve ElementAttributes type helper so that union string prop types are not converted to string. Thanks to @bigmistqke for the tip in #21! 5f216f8
    • BREAKING: This could introduce a type error for someone, due to it making union string types more accurate.
  • fix: guard against missing DOM API in element decorator (f.e. when an app imports things server-side in SSR setups like Solid Start, Astro, etc). 7a67b74

Full Changelog: v0.11.9...v0.12.0

v0.11.9

10 Sep 06:05
Compare
Choose a tag to compare
  • Limit TS to 5.2.x for now. If you have any errors, try "skipLibCheck": true in tsconfig.

Infrastructure:

  • Improve automated tests

Full Changelog: v0.11.8...v0.11.9

v0.11.8

10 Sep 05:59
Compare
Choose a tag to compare

What's Changed

  • fix: apply css property style after template content so that it overrides any styles in a template… by @trusktr in #13
    • This is useful because if the template contains <link> elements to import other styles, we typically want our element's local styles (the ones written in the css property) to override or use the imported styles, rather than the imported styles overriding our local styles.
      Example:
      class MyEl extends Element {
        template = () => html`
          <link href="./path/to/card.css" />
          <div class="card">
            ...
          </div>
        `
        
        css = `
          /* we want this to override the imported style */
          .card { color: blue }
        `
      }

Full Changelog: v0.11.7...v0.11.8

v0.11.7 - *Effectively* manage side effects!

01 Mar 01:25
Compare
Choose a tag to compare

Features

  • feat: include Effectful from Lume's classy-solid package as a feature of LumeElement, adding a convenient createEffect() class method for making effects and not having to manually clean them up in disconnectedCallback().

Before:

// ...
import {createRoot, createEffect} from 'solid-js'

@element('my-el')
class MyEl extends LumeElement {
  // ...
  
  #stop = () => {}

  connectedCallback() {
    super.connectedCallback()
    
    createRoot(stop => {
      this.#stop = stop
      
      // create effects
      createEffect(() => {...})
      createEffect(() => {...})
    })
  }
  
  disconnectedCallback() {
    super.disconnectedCallback()
    
    this.#stop()
  }
}

After:

// ...
import {createRoot, createEffect} from 'solid-js'

@element('my-el')
class MyEl extends LumeElement {
  // ...

  connectedCallback() {
    super.connectedCallback()
    
    // create effects
    this.createEffect(() => {...})
    this.createEffect(() => {...})
  }
}

Full Changelog: v0.11.0...v0.11.7