Embracing ReactJS-style UI rendering in altseven with ES6 Template Literals - Part II

Eighteen days seems like forever in the Internet age. It was only eighteen days ago that I wrote Part I of this article about challenges solving some problems with building a React-style rendering solution in the altseven JavaScript framework. I promised to blog again about solutions to some of these challenges that I created in altseven v 1.20. Well, eighteen days and two additional versions of altseven later, I've solved all of the challenges I wanted to solve and added some useful new features to the framework.

Event Handlers

The first challenge I faced was to add event handlers to ES6 template literals and have them bind to the DOM when rendered. The key there is that you have to wait until the template literal is rendered into the DOM to bind the events. Once I realized that bit of truth, it was easy enough to devise a means of writing event handlers that could be bound after rendering. Let's look at a Header component written using ES6 template literals:

    function Header(props) {
      var header = a7.components.Constructor(a7.components.View, [props], true);

      header.state = {
        user: props.user
      };

      header.eventHandlers = {
	  logout: function(){
            a7.events.publish( 'auth.logout', { callback: app.auth.authenticate }) ;
	  }
	};

      header.template = function(){
		return `Welcome, ${header.state.user.firstName} <a name="signout" data-onclick="logout">[ Sign out ]</a>`;
	};

      return header;
    }

As a reminder, I'm using Douglas Crockford's method of stuffing a function's prototype methods into another object using a constructor function. From there, I create the initial state of the object. Then we see a couple of new methods and the removal of another. header.eventHandlers is an object with named keys of functions that respond to events in the template literal. In this case, I have a logout function that responds to the click event in header link.

However, because we're dealing with a template literal and not an actual rendered DOM node ( yet ), I can't just bind the handler to the event. Instead, and in order to stay compliant with standard HTML and JavaScript ( one of my keys goals ), I have added a "data-onclick" attribute to the <a name="signout"> link. When the view renders, a process will examine all the "data-on" attributes in the rendered HTML and bind the event handlers to the corresponding elements. Did you notice that render() is what was missing from the Header function? The View component now houses the render function, and that's where a lot of action occurs. Let's look at it:

	render: function(){
		if( this.props.element === undefined || this.props.element === null ){
			this.props.element = document.querySelector( this.props.selector );
		}
		if( !this.props.element ) throw( "You must define a selector for the view." );
		this.props.element.innerHTML = ( typeof this.template == "function" ? this.template() : this.template );

		var eventArr = [];
		a7.ui.getEvents().forEach( function( eve ){
			eventArr.push("[data-on" + eve + "]");
		});
		var eles = this.props.element.querySelectorAll( eventArr.toString() );

		eles.forEach( function( sel ){
			for( var ix=0; ix < sel.attributes.length; ix++ ){
				var attribute = sel.attributes[ix];
				if( attribute.name.startsWith( "data-on" ) ){
					var event = attribute.name.substring( 7, attribute.name.length );
					sel.addEventListener( event, this.eventHandlers[ sel.attributes["data-on" + event].value ] );
				}
			}
		}.bind( this ));

		this.fireEvent( "rendered" );
	}

I'll gloss over the section that sets the rendered HTML for now. After it gets set, the function iterates over a7.ui.getEvents() to build a long (very long) query selector that pulls all elements with data-on* attributes from the rendered HTML. By default, a7.us.getEvents() contains a list of all standard DOM events listed on the Mozilla Developer Network browser Events page. Since most applications will only use a fraction of those events, you can set an array of only the events used in your application in the altseven configuration object. Doing so will improve performance in your application.

Once the elements are pulled into the eles variable, the function iterates over the elements and then loops over its attributes looking for data-on* attributes. While nested looping isn't ideal, it gets the job done. An improvement to this process, if possible, would be good for performance, especially on HTML like rendered lists of data with click handlers on each row/cell.

When the function finds a data-on* attribute, it checks the value of the attribute against eventHandler keys in the created object. If there is a match, it adds a listener for the event and sets the handler to the proper eventHandler function.

Going back to the Header object, you can see in the eventHandlers.logout function that, because the function is defined inside the Header object, it has access to the a7 namespace and the app namespace (the namespace of the application). Although it isn't used in this example, it also has access to the object namespace, in this case header. Also not listed, since logout is an event handler, it could be written as:

logout: function( event )

Since the event will be passed to the handler by the browser, giving us access to the Event object as well.

By writing the event handlers inline but deferring binding them to the DOM until we know the HTML for the object is rendered - we can write our functions using standard syntax and scopes and still be guaranteed that they will be available in the rendered HTML.

I hedged about using data-on* attributes to hold the values of the eventHandlers required for a given element, but ultimately, I decided that, since it met my goal of using standard HTML and Javascript, it would be an acceptable solution.

In Part III, I will go over how altseven v. 3.x uses events to automate several processes including rendering on state change.