Attributes / Casting

Because DOM can only store values as strings (well, almost), we need a reliable and predictable way to convert any type that a component attribute value holds into a string and then be able to to cast that string into an appropriate type of value when reading an html-attribute from DOM. This is what the casting mechanism does.

Default casting rules

There are default rules any Webface.js component will follow when casting attributes.

When saving to DOM

  • if value is null or false, html attribute will be set to an empty string ""
  • in any other case, a toString() will be called on the value

When reading from DOM

  • if the html-attribute string equals "true" or "false" that value will be cast to a Boolean type, either true or false respectively.
  • if the html-attribute string equals "null", that value will be cast to null.
  • if the html-attribute string only contains digits (and no decimal points), it will be cast to integer with parseInt().
  • if the html-attribute string contains digits and a decimal point, it will be cast to float with parseFloat().
  • if the html-attribute string only contains whitespace or is empty, it will be cast to null.
  • if no html-attribute exists with the name that corresponds to the component attribute's name, value of the component attribute becomes undefined upon read attempt from DOM.
  • in all other cases, the html-attribute is cast to string.

Define your own casting for specific attributes

You can define your own casting for specific attributes or even replace the default one for all attributes. Let's make it so that the value of the message attribute for MyNotificationComponent always has an exclamation mark at the end when saved to DOM, but upon reading the value from DOM this exclamation mark is again removed. To do that, we'll employ the Component.attribute_casting object and change it in the constructor:

export class MyNotification extends extend_as("MyNotification").mix(Component).with() {
  constructor() {
    this.attribute_names = ["message"];
    this.attribute_casting["to_dom"]["message"]   = (v) => v + "!";
    this.attribute_casting["from_dom"]["message"] = (v) => v.replace(/!$/, "");
  }
}

And component's corresponding HTML code will initially look like this:

But be careful here. Casting values from html requires you to think of potential errors. In this example, if there's no html-attribute for some reason and the value fetched from DOM is undefined, you'd get an Uncaught TypeError: Cannot read property 'replace' of undefinederror for line 5,
thus line 5 better be improved this way:

this.attribute_casting["from_dom"]["message"] = (v) => v.replace(/!$/, "");

Now, when we say notification1.set("message", "hello world") (assuming component's instance is in the variable notification1) the attribute is saved to DOM, and you'll get the following html:

Reading the html-attribute back from DOM will result in removal of the exclamation mark:

notification1.updateAttrsFromNodes(["message"]);
notification1.get("message"); // => "hello world"


Re-define default casting

You can also redefine casting for all attributes:

export class MyNotification extends extend_as("MyNotification").mix(Component).with() {
  constructor() {
    this.attribute_names = ["message"];
    this.attribute_casting["to_dom"]["default"]   = (v) => v + "!";
    this.attribute_casting["from_dom"]["default"] = (v) => v ? v.replace(/!$/, "") : null;
  }
}

Ideally, you'd want to run default casting functions and then run your own on top. You can do it in this way:

export class MyNotification extends extend_as("MyNotification").mix(Component).with() {
  constructor() {
    this.attribute_names = ["message"];
    this.attribute_casting["to_dom"]["default"]   = (v) => v + "!";

    // We need to assign the default casting function to another variable, as we're about to change it.
    let default_from_dom_casting = this.attribute_casting["from_dom"]["default"];

    this.attribute_casting["from_dom"]["default"]   = (v) => {
      let casted_v = this.attribute_casting["from_dom"]["default"](v);
      return (typeof casted_v == "string") ? casted_v.replace(/!$/, "") : casted_v;
    }

  }
}

We had to change our test for undefined/null values on line 11 here and instead of of doing this:

v ? v.replace(/!$/, "") : null;

we now check whether the returning value is string, because default casting function may, as we know, return numbers and boolean values and replace() will not work on those. So, typeof casted_v == "string" is more appropriate.