Understanding the View Component of the altsevenJS framework

Today I am going to explore the View component of the altseven JavaScript framework. Developers use the View component to create HTML views for an altseven application. These views may be simple or complex, but they all follow a set pattern based on the View component.

First, let’s look at the beginning of the component. It starts with a function declaration that accepts a collection of properties called props, pretty straightforward. It stores the props and sets a variety of default values.


function View( props ){
	this.renderer = a7.model.get("a7").ui.renderer;
	this.type = 'View';
	this.timeout; // timeout value used for the timer before the view expires if isTransient = true
	this.timer; // timer to count down to expire the view if isTransient = true
	this.element; // html element the view renders into
	this.props = props;
	this.isTransient = props.isTransient || false; // views do not expire by default
	this.state = {}; // view state
	this.skipRender = false; // can be set to skip a re-render 
	this.children = {}; // child views
	this.components = {}; // register objects external to the framework so we can address them later
	this.config();
	this.fireEvent( "mustRegister" );
}

Let’s explain each of these properties.

this.renderer is the rendering engine used to render HTML from the template in a given view. By default, altseven uses Template Literals and renders HTML using its own internal rendering engine. The Handlebars and Mustache templating engines are also supported. Optional templating engines must be declared in the application options.

this.type is the type of component, ‘View’.

this.timeout is the value in milliseconds to set the timer to expire (unreigster) a given view from the application.

this.timer is the timer object created to count down to view expiration.

this.isTransient determies whether the view can be expired. By default, views do not expire, but in cases of applications with many small views, such as social media sites with doomscrolling features, it may be desirable to have views expire so they do not live in memory for the duration of the application session in the browser.

this.element is the HTML element where the view should render. This element is not passed into the view, it is generated by the View component from props.selector. A typical pattern for an altseven application is to start with an HTML skeleton and register selectors of elements that are available at the beginning of the application:

a7.ui.setSelector( 'auth', "#auth" );

Altseven applications, in this manner, can address the entire DOM of the current HTML document and are not limited to rendering within a single root element.

this.props is the props collection.

this.state is a collection of values representing the state of the view. State changes from form entry or other interactions are saved here.

this.skipRender is a flag that lets the application know not to re-render the view when a parent view is rendering its view chain. Skipping render of static views can significanly increase performance of an application with lots of nested views.

this.children is a collection of child views. The developer can register child views inside the view and they will render in a chain from the parent to the last child when the parent renders.

this.components is a collection of any JavaScript objects external to the altseven framework that the view needs to interact with. An example might be a WYSIWYG editor for an editing page. Here is an example using CodeMirror:

    editor.components.editor = CodeMirror.fromTextArea( document.querySelector( editor.props.selector +  " textarea[name='codeWindow']" ), {
		lineNumbers: true,
		mode: editor.state.mode,
		lineWrapping: true
    });

this.config calls the config function to set up the View.

this.fireEvent( “mustRegister” ) tells the newly configured view to register with the application. More on that in a minute.

As you can see, from here we work with the component’s prototype.

The config function sets up the view by setting event handlers for several events:

mustRegister: Instructs the view to register itself with the framework, and to register itself as a child of the parent view, if there is one.

mustRender: Instructs the view to ask the framework to render its HTML template. If skipRender is flagged, the render is cancelled and skipRender is reverted to false.

rendered: Informs the view that the HTML template has been rendered. Sets a timer to expire the view if it is set to transient, and calls the onRendered function of the view, if one exists.

registered: Informs the view that it has been registered with the framework, and fires the mustRender event if it is a root view in the application’s DOM.

mustUnregister: Instructs the view to unregister itself from the framework.



View.prototype = {
	config: function(){

		this.on( "mustRegister", function(){
			a7.log.trace( 'mustRegister: ' + this.props.id );
			a7.ui.register( this );
			if( a7.ui.getView( this.props.parentID ) ){
				a7.ui.getView( this.props.parentID ).addChild( this );
			}
		}.bind( this ) );

		this.on( "mustRender", function(){
			a7.log.trace( 'mustRender: ' + this.props.id );
			if( this.shouldRender() ){
				a7.ui.enqueueForRender( this.props.id );
			}else{
				a7.log.trace( 'Render cancelled: ' + this.props.id );
				// undo skip, it must be explicitly set each time
				this.skipRender = false;
			}
		}.bind( this ));

		this.on( "rendered", function(){
			if( this.isTransient ){
				// set the timeout
				if( this.timer !== undefined ){
					clearTimeout( this.timer );
				}
				this.timer = setTimeout( this.checkRenderStatus.bind( this ), a7.model.get( "a7" ).ui.timeout );
			}
			this.onRendered();
		}.bind( this ));

		this.on( "registered", function(){
			if( this.props.parentID === undefined || this.mustRender ){
				// only fire render event for root views, children will render in the chain
				this.fireEvent( "mustRender" );
			}
		}.bind( this ));

		this.on( "mustUnregister", function(){
			a7.ui.unregister( this.props.id );
		}.bind( this ));
	},

The events are listed in the events collection for reference.

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

The other functions on the prototype:

setState: Sets the view’s state collection.

getState: Retrieves the view’s state collection.

addChild: Adds a child view to the this.children collection of child views.

removeChild: Removes a child view.

clearChildren: Clears the entire this.children collection.

getParent: Returns the parent view of the current view.

render: The heart of the view, this function renders the template content into the innerHTML property of the view’s HTML element as defined by the selector property. It then attaches event listeners defined by the HTML’s data-onXX properties. It performs a number of steps to verify the existence of the element, the selector, and clear the view if the element does not exist in the current HTML document.

shouldRender: Based on the value of the skipRender flag, returns true or false whether the view should render its content.

onRendered: Instructs child views to render after the current view has been rendered.

checkRenderStatus: Checks for the existence of the view’s selector and, if null, unregisters the view. Otherwise checks if the isTransient flag is set to true and if so, sets the timeout function to time out the view.


  	setState: function( args ){
    this.state = args;
    // setting state requires a re-render
		this.fireEvent( 'mustRender' );
	},
	getState: function(){
		return Object.assign( this.state );
	},
	addChild: function( view ){
		this.children[ view.props.id ] = view;
		// force a render for children added
		//this.children[ view.props.id ].mustRender = true;
	},
	removeChild: function( view ){
		delete this.children[ view.props.id ];
	},
	clearChildren: function(){
		this.children = {};
	},
	getParent: function(){
		return ( this.props.parentID ? a7.ui.getView( this.props.parentID ) : undefined );
	},
	render: function(){
		a7.log.info( 'render: ' + this.props.id );
		if( this.element === undefined || this.element === null ){
			this.element = document.querySelector( this.props.selector );
		}
		if( !this.element ){
			a7.log.error( "The DOM element for view " + this.props.id + " was not found. The view will be removed and unregistered." );
			// if the component has a parent, remove the component from the parent's children
			if( this.props.parentID !== undefined ){
				a7.ui.getView( this.props.parentID ).removeChild( this );
			}
			// if the selector isn't in the DOM, skip rendering and unregister the view
			this.fireEvent( 'mustUnregister' );
			return;
		}
		//throw( "You must define a selector for the view." );
		this.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.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" );
	},
	shouldRender: function(){
    if( this.skipRender ){
      return false;
    }else{
      return true;
    }
	},
	// after rendering, render all the children of the view
	onRendered: function(){
		for( var child in this.children ){
			this.children[ child ].element = document.querySelector( this.children[ child ].props.selector );
			this.children[ child ].render();
		}
	},
	// need to add props.isTransient (default false) to make views permanent by default
	checkRenderStatus: function(){
		if( document.querySelector( this.props.selector ) === null ){
			a7.ui.unregister( this.id );
		}else{
			if( this.isTransient ){
				this.timer = setTimeout( this.checkRenderStatus.bind( this ), a7.model.get( "a7" ).ui.timeout );
			}
		}
	}
};