Key Takeaways
- Components implemented with Native HTML are barely reusable, and implementation of the component can be complicated.
- ReactJS has better reusability compared to Native HTML, though the grammar for reusing a component is unnecessarily heavy.
- The minimal reuse unit in Binding.Scala is an ordinary method.
- Binding.Scala is advanced in implementing a component with higher reusability.
- The lighter-weight reuse units in Binding.Scala leads to smoother programming experience.
In the last article of More than React series, More Than React: Why You Shouldn't Use ReactJS for Complex Interactive Front-End Projects, Part I, we listed all of the pain points in front-end development.
In this article, we'll have a detailed discussion about one of the pain points, reusability. For comparison, we'll use native DHTML API, ReactJS and Binding.scala to implement a reusable tag editor component, from which we'll compare the difficulty and convenience of implementation.
Requirements of the tag editor
Let's assume that we are planning to implement a blog system. Like most blog systems, we'd like to allow authors to add tags to their articles.
In this case, we'll provide a tag editor to authors.
As shown in the image, the tag editor requires two UI rows.
The first row shows all the added tags and, next to each tag, there's a 'x' button for deletion. The second row is a text editor with an 'Add' button which creates new tags with the content in the text editor. Each time 'Add' is clicked, the tag editor should avoid duplicates by checking if the tag already exists. After a tag is successfully added, the text editor should be cleared in preparation for the next input.
In addition to UI, the tag editor should also provide an API. The web page that contains the tag editor should be able to fill in initial tags using the API. If the user adds or deletes a tag, there should be a notification mechanism for the rest of the page.
Native DHTML version
First, let's implement it using native DHTML API, without any front-end framework:
<!DOCTYPE html>
<html>
<head>
<script>
var tags = [];
function hasTag(tag) {
for (var i = 0; i < tags.length; i++) {
if (tags[i].tag == tag) {
return true;
}
}
return false;
}
function removeTag(tag) {
for (var i = 0; i < tags.length; i++) {
if (tags[i].tag == tag) {
document.getElementById("tags-parent").removeChild(tags[i].element);
tags.splice(i, 1);
return;
}
}
}
function addTag(tag) {
var element = document.createElement("q");
element.textContent = tag;
var removeButton = document.createElement("button");
removeButton.textContent = "x";
removeButton.onclick = function (event) {
removeTag(tag);
}
element.appendChild(removeButton);
document.getElementById("tags-parent").appendChild(element);
tags.push({
tag: tag,
element: element
});
}
function addHandler() {
var tagInput = document.getElementById("tag-input");
var tag = tagInput.value;
if (tag && !hasTag(tag)) {
addTag(tag);
tagInput.value = "";
}
}
</script>
</head>
<body>
<div id="tags-parent"></div>
<div>
<input id="tag-input" type="text"/>
<button onclick="addHandler()">Add</button>
</div>
<script>
addTag("initial-tag-1");
addTag("initial-tag-2");
</script>
</body>
</html>
To implement the tag editor's functionality, it takes 45 lines of JavaScript code for UI logic, in addition to several HTML <div> elements and two lines of JavaScript code for filling in initial data.
The HTML file contains several hard coded <div> elements which are used as containers for other dynamically created components.
The code will dynamically update the website content in these <div> elements, hence, when we need two or more tag editors in one page, the id will conflict. In this case, the code above is not reusable.
Even if we replace DHTML API with jQuery, it's still hard to reuse the code. To reuse the UI, jQuery developers usually would have to add extra code, scan the whole page during onload, find those elements that have a specific class attribute, then modify it. For complex webpages, these functions that run during onload would easily have conflicts. For example, if one function modifies an HTML element, this would very likely effect another piece of code which leads to corrupted internal status.
Tag editor component implemented in ReactJS
ReactJS provides reusable component React.Component. If we implement it in ReactJS, the code would look like this:
class TagPicker extends React.Component {
static defaultProps = {
changeHandler: tags => {}
}
static propTypes = {
tags: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
changeHandler: React.PropTypes.func
}
state = {
tags: this.props.tags
}
addHandler = event => {
const tag = this.refs.input.value;
if (tag && this.state.tags.indexOf(tag) == -1) {
this.refs.input.value = "";
const newTags = this.state.tags.concat(tag);
this.setState({
tags: newTags
});
this.props.changeHandler(newTags);
}
}
render() {
return (
<section>
<div>{
this.state.tags.map(tag =>
<q key={ tag }>
{ tag }
<button onClick={ event => {
const newTags = this.state.tags.filter(t => t != tag);
this.setState({ tags: newTags });
this.props.changeHandler(newTags);
}}>x</button>
</q>
)
}</div>
<div>
<input type="text" ref="input"/>
<button onClick={ this.addHandler }>Add</button>
</div>
</section>
);
}
}
The 51 lines of ECMAScript 2015 code and JSX above implemented a tag editor component TagPicker. Though it takes more lines of code than DHTML, it is much more reusable.
The number of lines of code would be even bigger if you didn't use ECMAScript 2015. Furthermore, you'd have to deal with some pitfalls in JavaScript, such as not being able to use this
in callback function.
ReactJS developers can use the ReactDOM.render function anytime to render TagPicker to any empty element. Also, the ReactJS framework triggers the render function whenever state and props changes, which saves the manual work of modifying the existing DOM.
If we put the duplication of the key attribute aside, ReactJS is doing okay with the internal interaction of a single component. But, a complex webpage architecture tends to need multiple layers of nested components. It would be much worse for ReactJS in the case of interaction between parent and child component.
For example, if we need to display all the tags outside of TagPicker, and these tags should be automatically synchronized when user delete or add a tag. To implement this, we'll need to pass changeHandler callback to TagPicker, as shown below:
class Page extends React.Component {
state = {
tags: [ "initial-tag-1", "initial-tag-2" ]
};
changeHandler = tags => {
this.setState({ tags });
};
render() {
return (
<div>
<TagPicker tags={ this.state.tags } changeHandler={ this.changeHandler }/>
<h3>All tags:</h3>
<ol>{ this.state.tags.map(tag => <li>{ tag }</li> ) }</ol>
</div>
);
}
}
In the newly created Page component, there should be a changeHandler callback function, which internally calls Page's setState in order to trigger the Page's re-render.
In the example above, we notice that ReactJS can solve simple problems simply. But on webpages with complex layers and frequent interactions, the implementation becomes complicated. A front-end project using ReactJS would have xxxHandler all over the place just for passing messages. In my experience on an overseas client project, each component would need around five callbacks on average. For a deeper nesting with multiple layers, we'd need to pass the callback through each layer, from the root component to the leaf, when creating a webpage. Vice versa, we need to pass an event message out layer by layer when an event is triggered. At least half of the code in the whole front-end project is trivial boilerplate code like this.
The basic usage of Binding.scala
Before we step into the implementation of tag editor using Binding.scala, I'd like to introduce some Binding.scala basics.
The smallest reusable unit in Binding.scala is data binding expression, i.e. @dom method. Each @dom method is a piece of HTML template, for example:
// Two HTML line breaks
@dom def twoBr = <br/><br/>
// An HTML heading
@dom def myHeading(content: String) = <h1>{content}</h1>
Each template can contain other child template using bind grammar, such as:
@dom def render = {
<div>
{ myHeading("Feature of Binding.scala").bind }
<p>
Shorter code
{ twoBr.bind }
Less concepts
{ twoBr.bind }
More functionality
</p>
</div>
}
Please check out Appendix: Quick start tutorial for Binding.scala for detailed information about learning Binding.scala.
Also, the fourth article More than React IV: How to statically compile HTML? in the More than React series will list all the features for HTML template that Binding.scala supports.
Implement the tag editor template using Binding.scala
Now, we'll show you how to create a tag editor using Binding.scala.
This tag editor is more complicated than the Binding.scala HTML template we learnt earlier; it contains interactions instead of just being a static template.
@dom def tagPicker(tags: Vars[String]) = {
val input: Input = <input type="text"/>
val addHandler = { event: Event =>
if (input.value != "" && !tags.get.contains(input.value)) {
tags.get += input.value
input.value = ""
}
}
<section>
<div>{
for (tag <- tags) yield <q>
{ tag }
<button onclick={ event: Event => tags.get -= tag }>x</button>
</q>
}</div>
<div>{ input } <button onclick={ addHandler }>Add</button></div>
</section>
}
This tag editor HTML template is done in 18 lines.
Since the tag editor needs to display all of the tags, we use tabs:Vars[String] to save the tag data, then use a for/yield loop to render each tag in tags into UI component.
Vars is a list container that supports data binding; UI will change automatically when the data in list container changes. Hence, when the onclick event on x button deletes the data in tags, the tag displayed would disappear correspondingly; vice versa for additions.
Binding.scala not only simplifies the implementation of tag editor, but also simplifies the usage.
val tags = Vars("initial-tag-1", "initial-tag-2")
<div>
{ tagPicker(tags).bind }
<h3>All tags:</h3>
<ol>{ for (tag <- tags) yield <li>{ tag }</li> }</ol>
</div>
}
All you need to do is to write another HTML template in 9 lines and call the existing tagPicker in it.
For a complete version of the Demo code, please refer to ScalaFiddle
Binding.scala does not require the same kind of callback like changeHandler in ReactJS. When the tags Var is changing, the page will reflect the change automatically.
After comparing the code between ReactJS and Binding.scala, we would find the following difference:
- Developers using Binding.scala can use @dom functions such as tagPicker to provide an HTML template instead of using component.
- Developers using Binding.scala can pass parameters like tags between functions, without needing the concept of props.
- Developers using Binding.scala can use local variables inside of a function, rather than using state.
In general, Binding.scala is much more concise than ReactJS.
If you happen to have experience with server side webpage templating language such as ASP, PHP, JSP, you'll see that Binding.scala and HTML template are very similar to them.
Using Binding.scala doesn't require any knowledge about functional programming. All that's required is to copy the HTML prototype generated by design tools into code, then replace the dynamic part with curly brackets, replace the repeating part with for/yield, That's all.
Conclusion
This article compares the difficulty of implementing and using a reusable tag editor with different tech stacks.
Native HTML |
ReactJS |
Binding.scala |
|
Lines of code required to implement the tag editor |
45 LOC |
51 LOC |
18 LOC |
Difficulty encountered during implementation |
The implementation of dynamically updating HTML page is too complicated. |
The grammar used for implementing component is too heavy |
None |
Lines of code required to reuse tag editor and display the tag list |
Hard to reuse |
21 LOC |
8 LOC |
Blockers for reuse |
Hard to modularize the static HTML element |
It's too complicated to transfer callback function through multiple layers. |
None |
Binding.scala does not try to reinvent concepts like 'components'. Instead, we encourage you create the ordinary 'method' as minimal reuse unit, because the lighter-weight reuse unit will lead to smoother programming experience, and increase the reusability.
In the next article of the More than React series, we will compare the virtual DOM mechanism of ReactJS with the precise data binding mechanism of Binding.scala, unveil the different algorithms beneath the veneer of similar usages.
Reference
- Binding.scala project page
- Binding.scala • TodoMVC project page
- Binding.scala • TodoMVC DEMO
- Binding.scala • TodoMVC other DEMO
- From JavaScript to Scala.js guideline
- Scala.js project page
- Scala API references
- Scala.js API references
- Scala.js DOM API references
- Binding.scala guideline for beginners
- Binding.scala API references
- Binding.scala's Gitter chat room
About the Author
Yang Bo is a ten-year working experience developer and works at ThoughtWorks China as a Lead Consultant now. He's an active open-source contributor in Scala, ActionScript, and Haxe community.