Events / Native vs Custom events

In Webface.js, you actually have two types of events - native and custom. Native events are the ones that the browser generates, such as click mouseover, mouseup etc. The full list of those can be found on the MDN Event reference page. But custom events can be generated using Webface.js methods. We've already seen an example of native events above, and handlers used for them in the previous section of this reference.

Native events

The important thing to remember is that all native events need to be listed in the this.native_events property of the component class or the handlers won't work. You must set in constructor:

export class MyButtonComponent extends extend_as("MyButtonComponent").mix(Component).with() {
  constructor() {
    super();
    this.native_events = ["click"];
  }
}

By default, Webface.js automatically prevents any default browser handler from being invoked if an event is listed in this.native_events. For example, if a button is a link or a submit button on a form, then the link would not redirect and the form wouldn't be submitted. You can redefine this behavior by prepending the native event name with ! like this:

this.native_events = ["!click"];

For ButtonComponent provided by Webface.js you can just set prevent_native_click_event attrubute to achieve this behavior (most likely, you'd want it set through the html attribute data-prevent-native-click-event. Or, if you're using webface_rails, use WebfaceComponentHelper#button_link and WebfaceForm#_submit (f.submit) to generate buttons with this attribute set.

Native events on component parts

Sometimes, your components may be rather complex in terms of DOM elements that construct their visual representation, but it's not always reasonable to break them down into sub-components. By introducing component parts, we can still catch native events from those parts without creating additional Component classes. To listen to a native event on component part, just prefix event name with a part name:

this.native_events = ["!clickable_part.click"];

Let's suppose all forms on our website need to have two buttons "Submit" and "Cancel" - say, we're absolutely certain about that requirement. Perhaps we then need a single component called FormButtonComponent, which will consist of two parts - the two button elements, each with a distinct name. First, let's look at the html code:

And now let's define a handlers for the events on those parts:

 export class FormButtonComponent extends extend_as("FormButtonComponent").mix(Component).with() {
  constructor() {
    super();

    this.native_events = ["!submit.click", "!cancel.click"];

    this.event_handlers.addForEvent("click", {
      // In this case reference to "self" has a suffix
      // which is the name of the part receiving the event.
      "self.submit" : (self,event) => self.parent.dom_element.submit(), // actually submits the form
      "self.cancel" : (self,event) => console.log("Form submission cancelled")
    }); 
  }
}

Note that on line 10 we did something that's actually not very good idea: we started trying to control the parent of the button, presumably a UserFormComponent we wrote (code omitted here), by calling methods on its dom element. This isn't the best solution: child component should normally be dumb. A better idead would be to catch a custom submit event published by the button in the parent, and let it decide what to do with it. That is explained below.

Custom events

Custom events are events published by Webface.js components, which can be handled by their parent components or even the publisher component himself.

To improve the previous example with FormButtonComponent we'll make it publish custom events in response to clicks on its parent and will let the parent component decide what to do about it. But first we need to add a role to the FormButtonComponent html:

Next, publish custom events in native event handlers:

 export class FormButtonComponent extends extend_as("FormButtonComponent").mix(Component).with() {
  constructor() {
    super();

    this.native_events = ["!submit.click", "!cancel.click"];

    this.event_handlers.addForEvent("click", {
      "self.submit" : (self,event) => self.publishEvent('submit'),
      "self.cancel" : (self,event) => self.publishEvent('cancel')
    }); 
  }
}

And, finally, we'll handle submit and cancel events inside its parent UserFormComponent.

 export class UserFormComponent extends extend_as("UserFormComponent").mix(Component).with() {
  constructor() {
    super();
    this.event_handlers.addForRole("submitter", {
      submit: (self,event) => self.dom_element.submit(),
      cancel: (self,event) => console.log("Form submission cancelled")
    }); 
  }
}

This code works the same as before, but it allows us to reuse the FormButtonComponent with other types forms, which may want to react to button clicks differently (for instance some forms may prompt user for a password before submitting the form - as a security measure).

Another example

Suppose we have a special custom text field component we call LimitedTextFieldComponent for publishing tweets. When a user types more than 140 characters (yes, we know you can now tweet more than 140!) it generates a custom event called filled_up, so that our PageComponent becomes aware of the situation and maybe disables the "Send" button so user can't actually send the tweet. And when user removes a character we check if the field length is less then 140 chararcters, and if it is, it publishes a freed_up event, and then its parent, PageComponent, once again, picks it up. The idea here is that our custom text field component doesn't know what kind of warning to show, it just informs the parent that it's full.

Let's start by writing some html:

And now the LimitedTextFieldComponent class:

export class LimitedTextFieldComponent extends extend_as("LimitedTextFieldComponent").mix(Component).with() {
  constructor() {

    super();
    // We want both of these native events to which we'll assign
    // the same handler, because they're triggered in different cases.
    this.native_events = ["keyup", "change"];

    this.event_handlers.addForRole("#self", {
      // We're not passing `self` and `event` as arguments here
      // because we don't need them.
      keyup:  () => this.checkIfFull(),
      change: () => this.checkIfFull()
    });
  }

  checkIfFull() {
    console.log(this.dom_element.value.length);
    if(this.dom_element.value.length > 140)
      this.publishEvent("filled_up");
    else
      this.publishEvent("freed_up");
  }

}

This code is not terribly optimized though. We publish the filled_up / freed_up events on practically every keypress. To keep it simple, though, optimization of this example code should be out of the scope of this reference.

We can now write the PageComponent class, which decides what do when those events are published:

export class PageComponent extends extend_as("PageComponent").mix(Component).with() {

  constructor() {
    super();
    this.event_handlers.addForRole("tweet_field", {
      filled_up: (self,event) => self.findFirstChildByRole("submit").behave("disable"),
      freed_up:  (self,event) => self.findFirstChildByRole("submit").behave("enable")
    });
  }

}

On lines 6-7 we use the standard Component.findFirstChildByRole() method and then the Component.behave() method dicussed in the Behaviors section. But even without knowing too much about those methods, it's pretty easy to see what this code accomplishes: we lock and unlock the submit button based on whether our field is over the character limit.

Default #all role

There's a special #all role that is used for all events of a particular name that were published by any of the child components - but only if an event handler isn't defined for this said event/role pair. For example:

export class UserFormComponent extends extend_as("UserFormComponent").mix(Component).with() {

  constructor() {
    super();
    this.event_handlers.addForRole("#all", {
      change: (self,event) => self.validate()
    });
  }

}

will cause our form to call a validate() method whenever any of the children reports a change.