Attributes / Callbacks

Sometimes writing attribute value to DOM upon attribute change might not be enough. Let's say you want to display a warning to the user whenever button becomes disabled, so the user isn't too frustrated as to why that happened (perhaps, it happened because he clicked that button to send the form and we don't want him to click it again and re-send the form accidentally).

We'd then write the following code for our component:

export class MyButtonComponent extends extend_as("MyButtonComponent").mix(Component).with() {
  constructor() {
    this.attribute_names = ["disabled"];
    this.attribute_callbacks["disabled"] = (attr_name, self) => {
      alert(`${self.constructor.name} ${attr} has changed, new value is ${self.get(attr_name)}`);
    }
}

Now, if anywhere in the code we write button1.set("disabled", "disabled") (assuming button1 contains our component instance), it will display the alert saying "MyButtonComponent disabled has changed, new value is disabled". The problem with this code, however, is that because we defined a custom callback for attribute disabled, the default one stopped working. And here's the default callback for all attributes, as defined by the code in Webface.js's Component class:

this.attribute_callbacks = {
  'default' : (attr_name, self) => {
    this.constructor.attribute_callbacks_collection['write_attr_to_dom'](attr_name, self);
  }
};

As you can see, the default behavior here is to write value to DOM. But it will not run since indeed we defined a custom callback for the disabled attribute. To have it both ways - display our alert and update DOM - we can add this write_attr_to_dom() call very easily to our callback:

export class MyButtonComponent extends extend_as("MyButtonComponent").mix(Component).with() {
  constructor() {
    this.attribute_names = ["disabled"];
    this.attribute_callbacks["disabled"] = (attr_name, self) => {
      self.constructor.attribute_callbacks_collection['write_attr_to_dom'](attr_name, self);
      alert(`${self.constructor.name} ${attr} has changed, new value is ${self.get(attr_name)}`);
    }
}

Note how we used self instead of this because this would not refer to current component in this context.

Theoretically, we could've written something like

export class MyButtonComponent extends extend_as("MyButtonComponent").mix(Component).with() {
  constructor() {
    this.attribute_names = ["disabled"];
    this.attribute_callbacks["disabled"] = (attr_name, self) => {
      self.dom_element.setAttribute("disabled", "disabled");
      alert(`${self.constructor.name} ${attr} has changed, new value is ${self.get(attr_name)}`);
    }
}

which looks nicer, but the problem is that it becomes very component specific, while this line

self.constructor.attribute_callbacks_collection['write_attr_to_dom'](attr_name, self);

offers a standard way of updating DOM that goes through all the necessary hoops.

Defining your custom callback functions

DISCLAIMER: attribute callback functions look very complex right now (too much code!), and it is currently in the TODO to simplify it a little bit, so it starts making more sense. Consider current state of things a preliminary low-level version of Webface.js API.

As you could've probably guessed, you can define your own callback functions, which can be applied to many different attributes. For example, we could've had this function defined:

export class MyButtonComponent extends extend_as("MyButtonComponent").mix(Component).with() {
  constructor() {
    this.constructor.attribute_callbacks_collection['display_warning'] = (attr_name, self) => {
      alert(`${self.constructor.name} ${attr} has changed, new value is ${self.get(attr_name)}`);
    };
    ...
  }
}

Now we can call this function inside callbacks for various fields:

export class MyButtonComponent extends extend_as("MyButtonComponent").mix(Component).with() {
  constructor() {
    this.attribute_names = ["disabled", "caption"];
    this.attribute_names.forEach((attr_name) => {
      this.attribute_callbacks[attr_name] = (attr_name, self) => {
        self.constructor.attribute_callbacks_collection['write_attr_to_dom'](attr_name, self);
        self.constructor.attribute_callbacks_collection['display_warning'](attr_name, self);
      }
    });
  }
}

Now all attributes will call write_attr_to_dom AND display_warning callbacks when changes to component attributes are detected.

There isn't any specific reason why you can't have your callback function stored in elsewhere and not in self.constructor.attribute_callbacks_collection, however it's just a convenient standard place to put those functions and therefore it is recommended you put it there.

To be fair, it is not very often that you'd actually need to define your custom attribute callback functions. More often than not you would just find yourself writing custom callbacks and including the self.constructor.attribute_callbacks_collection['write_attr_to_dom'](attr_name, self); in those callbacks when needed.