PatternFlyElements

    Property definitions provide a standard, streamlined way to declare the properties and attributes your component needs. Property definitions provide the following features:

    • Two-way binding between property and attribute values.
    • Default value initialization.
    • Data types (for properties).
    • Observer function to be run when a value changes.
    • Optional namespacing prefix for attribute names.
    • Aliasing properties to one another, for phased deprecation.
    • And more!

    Introduction

    To introduce the topic, consider a component that counts how many times something has happened. It may have a tag of <pfe-count>, and a count attribute (<pfe-count count="0">) which sets the initial count. We might also want a local property this.count for convenient referencing and updating of the count (ex: this.count += 1 vs this.setAttribute(this.getAttribute("count") + 1)).

    A property definition for count:

    static get properties() {
      return {
        count: {
          type: Number,
          default: 0
        }
      };
    }
    

    With this property definition for count we get:

    • A property: The property’s name is count, so this.count is created and initialized to the default value, the number 0.
    • An attribute: An attribute, count is also created, and will be kept in sync with this.count. The `` prefix is explained below.
    • And many other options for watching the property for changes, deprecating attributes, and more.

    All the options for property definitions are explained below, but first, a bit about how property/attribute pairs are synchronized.

    Property / attribute binding

    As mentioned in the introduction, defining a property also creates an attribute, and the two values are kept in sync. The attribute name is generated by converting the property name from camelCase into snake-case, and applying a prefix. For instance, a property named firstName results in first-name which is then prefixed with `` to form first-name. These behaviors can be configured with prefix and attr.

    type

    Default: String

    Three property types are currently supported: String, Number, and Boolean.

    Defines the data type for the property. Values are stored in attributes as Strings, but are cast into the specified type when accessed via the property.

    String

    To store a string, create a property definition with type: String.

    name: {
      type: String
    }
    

    Valid values: any string, null, and undefined. Though all attribute values are technically strings, if you are assigning a true/ false paradigm or checking that a variable exists (implicit true/false, i.e., hidden or required), please assign that property a type of Boolean. This will ensure the update and injection of that value is handled correctly by the base class.

    Number

    Similar to boolean, though attribute values are strings, if you are assigning a numerical value to your attribute, please use this type for your property. Consider the use-case of tabindex; it will output a quoted string when assigned to the attribute but use a numerical type when you query for the property. To store a number, create a property definition with type: Number.

    count: {
      type: Number
    }
    

    In addition to number literals like 4, the Number type supports some special values. This table lists a few examples of property / attribute equivalence.

    Property Attribute
    4 "4"
    -10 "-10"
    NaN "NaN"
    Infinity "Infinity"
    -Infinity "-Infinity"
    undefined removes attribute
    null removes attribute

    Examples: this.count = NaN (or Number.NaN) will result in count="NaN".

    Valid values: any Number, NaN, null, and undefined.

    Boolean

    Boolean properties behave just like HTML boolean attributes you might be familiar with, like disabled or checked.

    isLoading: {
      type: Boolean
    }
    
    Property Attribute
    true ""
    false removes attribute
    "hello I am true" ""
    undefined removes attribute
    null removes attribute

    When referencing a Boolean attribute, use hasAttribute(name) rather than getAttribute(name).

    Handling null and undefined

    If a property is assigned to null or undefined, its associated attribute will be removed. This is true regardless of the property’s type.

    Example: this.count = null will result in the count attribute being removed.

    default

    Default: undefined

    During the component’s connectedCallback, values with defined defaults will be initialized in both properties and attributes.

    count: {
      type: Number,
      default: 0
    }
    

    Example: since count has a default of 0, when the component is connected and wasn’t given a count attribute already, one will be created with the default value 0. In other words, this.count === 0 and this.getAttribute("count") === "0".

    observer

    Default: undefined

    Observers provide a quick way to wire up a function to be called whenever a value changes.

    You may provide an observer for any property, which is the string name of a (non-static) function inside the web component which will be called whenever the property changes. Here’s an example wherein changes to count will trigger the handleCount function.

    class PfeCount extends PFElement {
    
      /* some boilerplate omitted */
    
      static get properties() {
        return {
          count: {
            type: Number,
            observer: "handleCount"
          }
        }
      }
    
      handleCount(oldVal, newVal) {
        this.log(`count changed from ${oldVal} to ${newVal}`);
      }
    
    }
    

    Observer functions are called with arguments (oldVal, newVal). Like property values, both arguments will be cast to the appropriate type, i.e. String, Number, or Boolean.

    cascade

    Default: undefined

    cascade allows an attribute value to be automatically copied to one or more child elements in the Light DOM or Shadow DOM[1]. The value of cascade is a CSS-style selector which is used to match the children that should receive the values. The selector is applied to the element’s children in both the Light DOM and the Shadow DOM.

    [1] There is not a way to target only the Light DOM or only the Shadow DOM. If this is necessary, we recommend adding a unique identifier to the Shadow DOM elements and making use of the :not selector.

    Example, cascading an attribute foo to any h1, h2, or h3 child elements.

    static get properties() {
      return {
        foo: {
          type: String,
          cascade: "h1,h2,h3"
        }
      };
    }
    

    With this property definition, any h1, h2, or h3 children will get the same attribute and value, such as:

    Before connectedCallback

    <pfe-foo foo="hello">
        <h1></h1>
    </pfe-foo>
    

    After connectedCallback

    <pfe-foo foo="hello">
        <h1 foo="hello"></h1>
    </pfe-foo>
    

    Attribute values are only copied from parent to child, never the other way around.

    alias

    Default: undefined

    The alias field can be used to link two properties to each other. alias expects to be given the name of another property, to which it will copy its value. When a property value is changed, if it has an alias, the new value will be sent to the alias.

    Aliasing is intended to help when migrating to a new property/attribute name. It allows for two different properties to be simultaneously “active”.

    Aliased values do not need to have a new set of definitions for cascade or observer as those states will be managed by the attribute to which it is being aliased (see example below).

    Example, an old property whose values will be forwarded along to a new property.

    static get properties() {
      return {
        new: {
          type: Number,
          default: 0,
        },
        old: {
          type: Number,
          alias: "new"
        }
      };
    }
    

    With these property definitions, changes to old will be “forwarded” along to new.

    el.old = 4;
    el.new === 4; // true, because setting el.old caused the value to be copied into el.new
    

    Two-way aliases are allowed.

    static get properties() {
      return {
        a: {
          alias: "b"
        },
        b: {
          alias: "a"
        }
      };
    }
    

    attr

    Default: undefined

    The attr field overrides the property’s default generated attribute name and specifies a custom name to be used as this property’s bound attribute.

    The example below shows a property definition resulting in a property named foo (i.e., js: this.foo) and an attribute named the-one-and-only-foo (i.e., html: <pfe-foo the-one-and-only-foo>).

    static get properties() {
      return {
        foo: {
          attr: "the-one-and-only-foo"
        },
      };
    }
    

    attr, along with alias, are primarily intended to help when migrating to a new property/attribute name. It allows for two different properties to be simultaneously “active”.

    When using attr, prefix: false is implied, meaning that there will never be a prefix added to an attribute name specified with attr.

    title

    Default: undefined

    The title field defines a human-friendly title for the property, which will be used to label the property’s form fields in the Storybook-powered demo site.

    observedAttributes & attributeChangedCallback

    Property definitions are driven by PFElement’s attributeChangedCallback and observedAttributes. Due to this, any PatternFly Element using property definitions should not define an attributeChangedCallback (use observer instead) or observedAttributes (attributes created by property definitions are automatically observed).

    If an element must have a custom attributeChangedCallback, it must call super.attributeChangedCallback(...arguments) within it. The best practice is to not provide an attributeChangedCallback.

    Reserved and pre-existing properties

    Because property names are attached directly to the HTMLElement, rather than a namespace (you could imagine this.props.foo instead of this.foo), care needs to be taken to not override existing properties except in special circumstances.

    HTMLElement objects have many properties, so there is a decent chance of name collision. So why put PatternFly Elements' properties in the same namespace? To allow binding observer functions to built-in attributes and properties. For example, the system allows specifying an observer function for aria-hidden.

    static get properties() {
      return {
        ariaHidden: {
          observer: "hiddenHandler",
          default: "true",
          prefix: false
        },
        style: {
          observer: "styleHandler",
          prefix: false
        },
      };
    }
    

    It’s important to note that there is already a property on HTMLElements called ariaHidden, which is automatically mapped to the attribute aria-hidden (in fact, this is the model that PatternFly Elements property/attribute pair bindings are based on). Allowing use of “reserved” property names allows easy wiring of observer functions to native attributes, like style and aria-*.

    When overriding reserved property names:

    • property/attribute two-way binding will not be initialized, with the assumption that a property/attribute pair for your property already exists in HTMLElement, as is the case with aria*, style, and many others.
      • as a result, type will be ignored because the value of the property and attribute are out of PFE’s hands.
    • all other property definition features work as expected:
      • default values will be honored
      • observer functions will be wired up
      • cascade will copy attribute values to children
      • alias will copy values to other properties
      • attr can specify the exact attribute name to be used

    If PFElement.debugLog(true) has been called, the overriding of reserved property (and many other things) will be logged to the console.

    Move to Step 2: Develop (Develop a Structure)