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

I spent yesterday adding support for ES6 Template Literals as an alternative to Mustache/Handlebars. It works well and obviates the need for a templating framework in your project. However, I got stuck when I decided to modify the UI rendering functionality. I am trying to make it more ReactJS-like, but without a compile step or a virtual DOM. I'm close, but where I am stuck is in adding event handlers to the template literals. Studying the issue, it occurs to me that this was probably a motivating reason behind creating a virtual DOM in the first place.

In short, if you want to add event handlers to a template literal, you have a couple of issues to deal with. First, you need to be able to reference functions in the View components to handle the events. Second, you need to be able to reference other data such as component state inside your event handlers. You can, with some difficulty, reference functions. Let's see how it works.

Creating a View component

Like in the ReactJS library, I am going to make a base function for the creation of views. We're not trying to replicate React, so a few basics are all we need to get started:

view.js
-----------

function View( props ){
	this.props = props;
	this.state = {};
  this.template = "";
}

View.prototype = {
  setState: function( args ){
    this.state = args;
    // setting state requires a re-render
    this.render();
  },
  render: function(){
    return this.template;
  }
};

This function gives us props, statetemplate, and setState and render functions in its prototype. To create a view, I am going to call this View function, like so:

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

  header.state = { user : props.user };
  header.template = `Welcome, ${this.state.user.firstName} <a name="signout">[ Sign out ]</a>`;
  return header;
}

Using this technique, I create a Header object by calling the View object. I am using a helper function in altseven that handles object creation for me. I will demonstrate why in the next article. In short, the Constructor function returns an object to the header variable that has the prototype of the View function. This technique was popularized by Douglas Crockford.

I then set the state and template variables. For the template, I use a template literal to insert firstName from the user object in header.state so I can offer a personalized greeting to the user.

The render function

You might wonder where the render function is in the Header object. If you go back to the View object's prototype, you will notice that the default render function of our prototype returns this.template. Since the template in the Header object requires no special processing, I don't need a custom render function to process it. But what if we need a more complex view, or we need to add event handlers on some of our rendered elements? That's where things get more complicated. Fortunately, we can programmatically generate template literal strings:

function TodoList( props ){
	var todolist = a7.components.Constructor( a7.components.View, [ props ], true );
	todolist.state = { items: props.items };
	todolist.render = TodoList.prototype.render;
	return todolist;
}

TodoList.prototype.render = function(){
	var str = `<ul>`;
	this.state.items.forEach( function( item ){
		str +=`<li>${item}</li>`;
	});
	str+=`</ul>`;
	return str;
};

Using plain JavaScript, I can iterate over an array passed into TodoList and generate a template literal with an unordered list of the items from the array. Putting it together:

testcomponents.js
-----------------------------

import {a7} from '/dist/a7.es6.js';

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

  header.state = { user : props.user };
  header.render = Header.prototype.render;
  //header.render();
  return header;
}

Header.prototype.render = function(){
    // render Header
    return `Welcome, ${this.state.user.firstName}`;

};

function TodoList( props ){
	var todolist = a7.components.Constructor( a7.components.View, [ props ], true );
	todolist.state = { items: props.items };
	todolist.render = TodoList.prototype.render;
	return todolist;
}

TodoList.prototype.render = function(){
		var str = `<ul>`;
		this.state.items.forEach( function( item ){
					str +=`<li>${item}</li>`;
		});
		str+=`</ul>`;
		return str;
};

export var header = Header;
export var todolist = TodoList;

 

testcomponents.htm
---------------------------------

<!doctype html>
<html>

<head>
	<title>alt-7 tests</title>
	<script type="module">
		import {a7} from '/dist/a7.es6.js';
                import {header, todolist} from '/test/testcomponents.js';


		var user = { firstName: "Robert" };
		var items = [
			'Get food',
			'Shovel snow',
			'Warm toes (cold outside)'
		];
		var greeting = a7.components.Constructor( header, [{user: user}]);
		var list = a7.components.Constructor( todolist, [{items: items}]);

                document.querySelector( "#greeting" ).innerHTML = greeting.render();
		document.querySelector( "#list" ).innerHTML = list.render();
	</script>

</head>

<body>
<div id="greeting">
</div>
<div id="list">
</div>
</body>

</html>

renders:

Welcome, Robert
  • Eat food
  • Shovel snow
  • Drink hot cocoa (cold outside)

 

Because TodoList inherits the prototype of the View function, I need to explicitly assign the render function from the TodoList prototype:

todolist.render = TodoList.prototype.render;

I could also put the render function directly in the TodoList function. For purposes of illustrating making the assignment, I have put it in the prototype.

 

Next Steps

This technique works for demonstration purposes, but it isn't sufficient in the context of a complete working application. If I want my UI framework to function like React, I need the render function on my view components to be called automatically by the framework, and to re-render when needed. I also still need a way to implement event handlers, which as I mentioned at the top of this post is a significant challenge. In Part II, I will demonstrate solutions to these challenges that have been implemented in altseven v 1.2.0.