In this article I will show how I built the commenting component for SyntaxSuccess.com using Aurelia.
A while back I wanted to experiment with Aurelia, so I figured I'd use it to build a commenting feature for my blog.
Getting started
Modern web frameworks require a fair amount of setup before you can even start writing code. Luckily, most of this pain has been taken care of by great starter projects like this one.
I really recommend using a starter project to get the project off the ground. Otherwise, configuring all the dependencies might become a painful barrier to entry.
Building the Comment Component
I actually built this commenting component a year ago - on an early version of Aurelia. Needless to say, I was a bit worried that my old code wouldn't be compatible with the latest version. However, I was pleased to see that my code was for the most part still working.
The starter made it really easy to get started. I basically just installed it and replaced all the sample code with my own comment component stuff.
Bootstrapping an Aurelia application is pretty straightforward. We start by creating a main.js file where we “start” the application
export function configure(aurelia) {
aurelia.use.defaultBindingLanguage()
.defaultResources();
aurelia.start().then(() => aurelia.setRoot());
}
Next we have to create a root level component in app.js. This is the entry point for the comment component where we fetch exiting comments for display.
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
export class App{
activate(){
let client = new HttpClient();
return client.fetch('someUlr/' + articleId)
.then(response => response.json())
.then(comments => this.comments = comments);
}
}
We also have to define some basic markup in app.html to support the root component.
<template>
<require from='./comments/comments-section'></require>
<comments-section comments.bind="comments"></comments-section>
</template>
Working with templates in Aurelia components is slightly different from the approach in Angular. Instead of an explicit templateUrl reference in the JavaScript class, the convention is to have a js file and an html file with the same name. Together the two files make up the component.
Next we will define the comment section component. Here we display a list of existing comments and an editor for creating new comments.
import {bindable} from 'aurelia-framework';
export class CommentsSection{
@bindable comments;
}
Notice how the sub component's tree-view and add-comment are imported using require elements in the markup.
<template>
<require from='./tree-view'></require>
<require from="./add-comment"></require>
<div>
<div class="add-comments">
<add-comment></add-comment>
</div>
</div>
<div>
<div>
<tree-view comments.bind="comments"></tree-view>
</div>
</div>
</template>
The tree-view component is pretty straight forward. It's nothing more than a repeater over existing comments.
<template>
<require from='./tree-node'></require>
<tree-node repeat.for="comment of comments" current.bind="comment"></tree-node>
</template>
The tree-node component is a bit more interesting though.
import {bindable} from 'aurelia-framework';
export class TreeNode {
@bindable current;
constructor(){
this.replying = false;
}
created(date){
return new Date(date).toDateString();
}
reply(){
this.replying = true;
}
}
The self referencing tree-node element makes this a recursive template, but this doesn't really change much since the Aurelia template engine knows how to handle this.
<template>
<require from="./add-comment"></require>
<ul>
<li>
<div class="comment">
<div class="commentDate">${created(current.created)}</div>
<h5>${current.author}</h5>
<div>
${current.text}
</div>
<div if.bind="!replying" class="reply-link"><a click.trigger="reply()">Reply</a></div>
<add-comment parent.bind="current" if.bind="replying"></add-comment>
</div>
<tree-node repeat.for="node of current.children" current.bind="node"></tree-node>
</li>
</ul>
</template>
The last part is to create a component for adding new comments.
import {inject,bindable} from 'aurelia-framework';
import {HttpClient, json} from 'aurelia-fetch-client';
export class AddComment{
@bindable parent = null;
author = '';
commentText = '';
saved = false;
error = false;
errorMessage = '';
save(){
let client = new HttpClient();
var parentId = null;
if(!this.author || !this.commentText){
this.errorMessage = 'All fields are required';
this.error = true;
return;
}
if(this.parent){
parentId = this.parent.id;
}
this.error = false;
client.fetch('someUrl/', {
method: 'post',
body: json({parentId:parentId,articleId:articleId,author:this.author,text:this.commentText})
})
.then(() => this.saved = true)
.catch(() => {
this.error = true;
this.errorMessage = 'I am sorry, but there was an error saving your comment';
});
}
}
On save the component makes a post request using the fetch api.
<template>
<div if.bind="!saved">
<div class="form-group">
<input type="text" class="form-control" value.bind="author" id="inputName" placeholder="Name">
</div>
<div class="form-group" style="margin-top: 5px;">
<textarea maxLength="1000" value.bind="commentText" rows="5" class="form-control" id="inputComment" placeholder="Comment - no markup please"></textarea>
</div>
<button type="button" class="btn btn-primary" click.trigger="save()">Save</button>
</div>
<div if.bind="saved" class="alert alert-success">Thanks for your feedback! Your comment will post as soon as I review it</div>
<div style="margin-top: 10px;" if.bind="error" class="alert alert-danger">${errorMessage}</div>
</template>
I made it a point to bundle up the dependencies and reduce the footprint of Aurelia itself. Check out the requests in the network tab if you are interested in the request sizes I ended up when using the bundle task from the starter project.
To tweak the configuration to match the exact needs of my component I modified bundles.js to the following
module.exports = {
"bundles": {
"dist/app-build": {
"includes": [
"[**/*.js]",
"**/*.html!text"
],
"options": {
"inject": true,
"minify": true,
"depCache": true,
"rev": false
}
},
"dist/aurelia": {
"includes": [
"aurelia-framework",
"aurelia-bootstrapper",
"aurelia-fetch-client",
"aurelia-templating-binding",
"aurelia-polyfills",
"aurelia-templating-resources",
"aurelia-loader-default",
],
"options": {
"inject": true,
"minify": true,
"depCache": false,
"rev": false
}
}
}
};
This ensures that we don't bundle in unnecessary dependencies like the router etc. You application might need different dependencies though, so tweak this to meet the needs of your application. All you have to do to generate a bundle is call gulp bundle from the command line.
If you want to test this out, you can give it a try by leaving me a comment below.