mjo-functions

Functions to handle html events in Polymer Web Components

Usage no npm install needed!

<script type="module">
  import mjoFunctions from 'https://cdn.skypack.dev/mjo-functions';
</script>

README

<mjo-functions>

Mixin to be able to use html events in mjo-polymer web components. Because components render to a shadowDOM we can sometimes find that we have to write a lot of extra code to listen for an event from an element within the shadowDOM through javascript.

With mjo-functions this work is simplified enough to make elements inside our components can call methods that are outside the shadowDOM.

Consider the following example:

import { PolymerElement, html } from "../mjo-polymer/polymer/polymer-element.js";

class MyComponent extends PolymerElement {
  static get template() {
    return html`
      <style>
        :host {
          position: relative;
          display: block;
        }
        :host([unresoved]) {
          display: none;
        }
        .text {
          position: relative;
          padding: 10px;
        }
      </style>
      <div class="text">
        Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem enim totam voluptatibus
        repudiandae in quo temporibus recusandae cum soluta harum inventore eveniet incidunt officia
        blanditiis voluptatum, itaque magni. Reiciendis, architecto.
      </div>
      <button on-click="_handleClick">MY BUTTON</button>
    `;
  }

  /**
   * @method  connectedCallback
   */
  connectedCallback() {
    super.connectedCallback();

    //Resolvemos el componente
    this.removeAttribute("unresolved");
  }

  /**
   * @method  _handleClick
   *
   * Listen click on button element
   *
   * @param {event} ev
   */
  _handleClick(ev) {
    console.log("Target is " + ev.target);
  }
}

window.customElements.define("my-component", MyComponent);

In the example above, if we click on the component's button, the _handleClick method of the component itself will be called. This is the expected performance of Polymer web components.

But now suppose that by clicking on the button we want to call a function that is outside the component. This will require us to write javascript code outside of the component and prepare the component to send the event outside of it. Which can be a problem when reusing the component.

We can also create a CustomEvent and listen to that event. It is not a bad solution, but if we have many components we can end up having a large number of custom events whose names can be difficult to remember and we can end up repeating event names if we do not follow a very orderly logic.

With mjo-functions we try to simplify this problem by declaring functions in the global scope.

Now let's add the mixin mjo-functions to our component.

components/my-component.js

import { PolymerElement, html } from "../mjo-polymer/polymer/polymer-element.js";
import { MjoFunctions } from "/node_modules/mjo-functions/mjo-functions.js";

class MyComponent extends MjoFunctions(PolymerElement) {
  static get template() {
    return html`
      ...
      <button on-click="_handleClick">MY BUTTON</button>
    `;
  }

  ...
  _handleClick(ev) {
    // Check is `this.onClick` is a function. If true, call them
    if(typeof this.onClick === "function" ) {
        this.onClick(ev.target, this, ev);
    }
  }
}

window.customElements.define("my-component", MyComponent);

What happened here? The MjoFunctions mixin adds properties to our component for most html events and touch events with a camelCase format. This means that our component right now has the following properties onClic, onMouseenter, onMouseleave, onTouchstart .... You will only have to be careful not to add properties to the component that do not match the properties that are added by the MjoFunctions mixin

All available events are defined here Events Attributes, Touch Events. Obviously the Events attributes belonging to the window have been omitted.

Note well that the rule that MjoFunctions follows when adding the properties of the events is to use a camelCase, so that the html attribute on-mouseenter that we will add to our component, becomes the onMouseenter property within the component. This is a feature of Polymer which is specified in your documentation.

So now we can use our component

<script type="module" src="components/my-component.js"></script>
<my-component onclick="componentClicked" on-click="buttonClicked"></my-component>
<script>
  function componentClicked(ev) {
    console.log(ev);
  }
  function buttonClicked(target, component, ev) {
    console.log(target);
    console.log(component);
    console.log(ev);
  }
</script>

As we can see in the example above, we can see that two html events have been added but with a slightly different syntax. With the onclick we will call the componentClicked function, only this event will be called when we click anywhere in our component. However, with the on-click we will call the buttonClicked and this event will only be fired when the button inside our component is clicked.

This works fine because the buttonClicked function is declared in a global scope. But suppose we want to listen to a function that is inside a javascript module. The classes and functions that we create within javascript modules will not be available in the global scope. Or we can also have another component that does not contain the my-component component inside, but that nevertheless we want it to have a logic linked to the button inside our component and to do something when we click on our component.

We are going to create a javascript module and another component and in both we will create a logic linked with the button of our my-component component.

/modules/my-module.js

import { GlobalsFunctions } from "/node_modules/mjo-functions/mjo-functions.js";

export default class MyModule {
  constructor() {
    GlobalsFunctions.add("MyModuleButtonEnter", this.buttonMyComponentClicked.bind(this));
    console.log(window.$mjo);
  }

  buttonMyComponentEnter(target, component, ev) {
    console.log(target);
    console.log(component);
    console.log(ev);
  }
}

/components/my-second-component.js

import { PolymerElement, html } from "../mjo-polymer/polymer/polymer-element.js";
import { GlobalsFunctions } from "/node_modules/mjo-functions/mjo-functions.js";

class MySecondComponent extends PolymerElement {
  static get template() {
    return html` ... `;
  }

  connectedCallback() {
    super.connectedCallback();

    GlobalsFunctions.add("SecondComponentButtonClicked", this.buttonMyComponentClicked.bind(this));
    console.log(window.$mjo);
  }

  disconectedCallback() {
    super.disconectedCallback();
    GlobalsFunctions.remove("SecondComponentButtonClicked");
  }

  buttonMyComponentClicked(target, component, ev) {
    console.log(target);
    console.log(component);
    console.log(ev);
  }
}

window.customElements.define("my-second-component", MySecondComponent);

What happened here?

With the GlobalsFunctions object, we can create functions that pass to the global scope that can be called within the component since they are now in the global scope. The GlobalsFunctions.add method creates a global function that always has thiswindow. $ Mjo [functionName]structure that is accessible from any component or module since it is called within the window object. In addition, you make sure that there can never be functions with repeated names in the window. $ Mjo object since it triggers an error in the console when trying to enter two functions with the same name.

Now let's modify our component so that it can call these functions when interacting with the button

components/my-component.js

import { PolymerElement, html } from "../mjo-polymer/polymer/polymer-element.js";
import { MjoFunctions } from "/node_modules/mjo-functions/mjo-functions.js";

class MyComponent extends MjoFunctions(PolymerElement) {
  static get template() {
    return html`
      ...
      <button on-click="_handleClick" on-mouseenter="_handleMouseenter">MY BUTTON</button>
    `;
  }

  ...
  __handleMouseenter(ev) {
    // Check is `this.onClick` is a function. If true, call them
    if(typeof this.onMouseenter === "function" ) {
        this.onMouseenter(ev.target, this, ev);
    }
  }

  _handleClick(ev) {
    // Check is `this.onClick` is a function. If true, call them
    if(typeof this.onClick === "function" ) {
        this.onClick(ev.target, this, ev);
    }
  }
}

window.customElements.define("my-component", MyComponent);

And now let's use our component

<script type="module" src="components/my-component.js"></script>
<my-component
  on-click="$mjo.SecondComponentButtonClicked"
  on-mouseenter="$mjo.MyModuleButtonEnter"
></my-component>

Now our component will call the methods that are inside the my-second-component component and the MyModule module

You can see a working example in CODESANDBOX