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

Update

------------

Parts of this post are no longer accurate, as altseven 3.2.x has changed the way components queue for rendering. See The Altseven View Component and the Rendering Queue in 3.2.x for updated information about the rendering queue.

------------

 

I first started building the altseven JavaScript framework in order to bring together various bits of functionality that I had built for earlier applications. I started with some general ideas about how I wanted to do things and a few tools in my toolbox with which to do them.

The Constructor

I had previously built a component to create objects in a consistent way, and I wanted to use its ability to bind custom events to created objects in altseven. The component creates objects using a given prototype and Object.create. It then assigns custom event handlers to the created object based on the object prototype. Let's look at the Constructor function:

function Constructor( constructor, args, addBindings ) {
	var returnedObj,
		obj;

	// add bindings for custom events
	// this section pulls the bindings ( on, off, fireEvent ) from the
	// EventBindings object and add them to the object being instantiated
	if( addBindings === true ){
		//bindings = EventBindings.getAll();
 		EventBindings.getAll().forEach( function( binding ){
			if( constructor.prototype[ binding ] === undefined ) {
				constructor.prototype[ binding.name ] = binding.func;
			}
		});
	}

	// construct the object
	obj = Object.create( constructor.prototype );

	// this section adds any events specified in the prototype as events of
	// the object being instantiated
	// you can then trigger an event from the object by calling:
	// <object>.fireEvent( eventName, args );
	// args can be anything you want to send with the event
	// you can then listen for these events using .on( eventName, function(){});
	// <object>.on( eventName, function(){ })
	if( addBindings === true ){
		// create specified event list from prototype
		obj.events = {};
		if( constructor.prototype.events !== undefined ){
			constructor.prototype.events.forEach( function( event ){
				obj.events[ event ] = [ ];
			});
		}
	}

	returnedObj = constructor.apply( obj, args );
	if( returnedObj === undefined ){
		returnedObj = obj;
	}
	//returnedObj.prototype = constructor.prototype;
	return returnedObj;
}

The Constructor function pulls the methods from the EventBindings mixin object. ( The mixin provides for a way to assign the methods of one object to an arbitrary object or prototype ). Next, if the given prototype has an array of events defined, the Constructor assigns those events to the generated object as bindable events.

When the Constructor is done, it returns an object with the methods and properties of the prototype, the on(), off(), and fireEvent() methods of the EventBindings mixin, and bindable events listed in the object prototype.

The View function

As of v. 3.x of altseven, the View function, which provides the base for rendered UI elements in altseven, has been expanded to handle some of the functionality that was centralized in the a7.ui.setView() method. The events for the View function:

events : ['mustRender','rendered', 'mustRegister', 'registered']

provide hooks for registering and rendering components, as well as hooks to execute functions when registration and rendering finish. View now has a prototype method called config that registers event handlers for these events:

	config: function(){

		this.on( "mustRegister", function( parent ){
			this.props.parentID = parent.props.id;
			a7.ui.register( this );
		}.bind( this ) );

		this.on( "mustRender", function(){
			// only render root views from here, children will be rendered by parents through bubbling of events
			if( this.props.parentID === undefined ){
				this.render();
			}
		}.bind( this ));

		this.on( "rendered", function(){
			this.onRendered();
		}.bind( this ));

		this.on( "registered", function(){
			// register children
			if( this.props !== undefined ){
				for( var prop in this.props ){
					if( this.props[ prop ] !== null && this.props[ prop ].type !== undefined && this.props[ prop ].type === "View" ){
						if( a7.ui.getView( this.props[ prop ].props.id ) === undefined ){
							this.props[ prop ].fireEvent( "mustRegister", this);
						}
					}
				}
			}
			if( this.props.parentID === undefined ){
				// only fire render event for root views, children will render in the chain
				this.fireEvent( "mustRender" );
			}
		}.bind( this ));

		// bubble up event
		if( this.props !== undefined ){
			for( var prop in this.props ){
				if( this.props[ prop ].type !== undefined && this.props[ prop ].type === 'View' ){
					this.props[ prop ].on( "mustRender", function(){
						this.fireEvent( "mustRender" );
					}.bind( this ));
				}
			}
		}
	},

We have four event handlers in the config() function corresponding to our four events- mustRegister, mustRender, registered, and rendered. I've thought about making views self-registering, so every view, when instantiated, would fire mustRegister. At the moment, views in the root of a DOM subtree  (those with no parents in altseven) must be registered manually, while children of root views are registered automatically when the root view handles the registered event, as you can see above. I am assigning a parentID to child views in each DOM subtree.

But isn't that a virtual DOM? I don't think it qualifies, at least not quite yet. I'm keeping track of each view, it's HTMLElement, and its parent. If I add a render queue, I should probably start keeping track of whether state is dirty or not. Does that make it a virtual DOM? I'm not getting stuck on semantics ...

Looking at the event handlers, you might notice that only root views actually call this.render() inside the mustRender event handler. Child views are handled elsewhere because they need the rendered HTML from the parent to exist in order for their selectors to return an HTMLElement using document.querySelector(). They are handled inside the onRendered() function, which is called from the rendered event handler.

You might also notice that the registered event handler fires mustRender for root views. It doesn't need to fire it for child views; remember, they are rendered in turn by their parents.

The last thing to notice is the note about bubbling up events. Parent views are bound to the mustRender events of their children, and they in turn fire a mustRender event. The net effect of this bubbling is that a mustRender event fired in the lowest child view will bubble up and eventually fire the root view in the chain. If you are familiar with React, you'll understand why React uses a rendering queue to enhance performance.

Without any changes to this code, every event in a DOM subtree is going to re-render the entire subtree. That might not matter in a small application, but in a large one it could create significant performance issues. Adding a rendering queue would allow us to check if a render process is already queued and if so whether there are duplicates being queued and in what order they should be called.

OK, so it looks a lot more like a virtual DOM with those features. I'm still not getting stuck on semantics.

Nested Rendering

The last thing I want to do for this post is show the onRendered() function.

	onRendered: function(){
		if( this.props !== undefined ){
			for( var prop in this.props ){
				if( this.props[ prop ].type !== undefined && this.props[ prop ].type === "View" ){
					this.props[ prop ].props.element = document.querySelector( this.props[ prop ].props.selector );
					this.props[ prop ].render();
				}
			}
		}
	}

What this bit of code does is to loop through the props of a view to see if there are any child views. If there are, it pulls the HTML element from the DOM using the cached selector string. Remember, we need to wait until this point to guarantee that the selector, when queried, will pull an element. It then calls the child view's render() function, which in turns will run this code again to check for children and render them as well.

That's it for now. As you might have guessed, I'm working on the rendering queue to improve performance and eliminate duplicate rendering. Watch my Twitter feed (@robertdmunn) for news about the next iteration of the altseven codebase.