In the previous six sessions, you’ve built a blogging system from the ground up. Everything’s working, and that’s great! However, the code itself is quite messy—we’ve been bootstrapping things together and left a lot of repetitive code and temporary solutions behind. This session is going to focus on how you can clean things up and fix a few issues we’ve been having.
1. Merge index.html and admin.html
First of all, since we have a router now (if you missed that session, check out Part 5: Router), we no longer need two separate .html
and .js
files. Let’s merge them together.
Step 1: Merge Files
At this point, I would suggest merging admin.html
and admin.js
and renaming them as index.html
and blog.js
because they have more logic and code, but you can do it either way. This should be fairly simple.
If you are renaming the files, just make sure you link to blog.js
in the new index.html
(previously admin.html
). Also, remember to copy over #blogs-tpl
from the old index.html
file to the new one and copy over BlogsView
from the old blog.js
file to the new one.
Now visit http://localhost/your-directory/ and you should be seeing the login screen by default (or the admin screen if you are logged in already).
Step 2: Update Router
Then, we can add a new URL pattern in the router to match the root URL to a new function; we can call it index()
:
routes: { '': 'index', ... },
This index()
function should render what’s previously on the homepage.
index: function() { this.blogs.fetch({ success: function(blogs) { var blogsView = new BlogsView({ collection: blogs }); blogsView.render(); $('.main-container').html(blogsView.el); }, error: function(blogs, error) { console.log(error); } }); }
And to see it work, let’s default redirect to that URL when the router starts:
start: function(){ Parse.history.start({pushState: true}); this.navigate('', { trigger: true }); }
Step 3: Update Nav
The next thing is to update the navigation bar on the top. Let’s change those HTML files to URLs here:
<nav class="blog-nav"> <a class="blog-nav-item active" href="">Home</a> <a class="blog-nav-item" href="admin">Admin</a> </nav>
And for those to work, we need to add an event to .blog-nav-item
to use blogRouter.navigate()
rather than the default link event:
$(document).on('click', '.blog-nav-item', function(e) { e.preventDefault(); var href = $(e.target).attr('href'); blogRouter.navigate(href, { trigger: true }); });
And let’s add logic to toggle the .active
class, too:
$(document).on('click', '.blog-nav-item', function(e) { e.preventDefault(); var href = $(e.target).attr('href'); blogRouter.navigate(href, { trigger: true }); $(this).addClass('active').siblings().removeClass('active'); });
Now if you click around, everything should be working!
2. Merge Add and Edit
Moving on, we can see AddBlogView
and EditBlogView
are pretty similar. So are update()
and create()
function in the Blog
class. Let’s merge those as well.
Step 1: Merge #add-tpl and #edit-tpl
First, let’s merge the two templates in index.html
to be #write-tpl
.
<script id="write-tpl" type="text/x-handlebars-template"> <h2>{{form_title}}</h2> <form class="form-write" role="form"> <div class="form-group"> <label for="title">Title</label> <input name="title" type="text" class="form-control" id="title" value="{{title}}"></input> </div> <div class="form-group"> <label for="content">Content</label> <textarea name="content" class="form-control" rows="20">{{{content}}}</textarea> </div> <button class="btn btn-lg btn-primary btn-block" type="submit">Submit</button> </form> </script>
You can see that it’s basically #edit-tpl
with class name changes and a dynamic form title. We will just pass ""
into title
and content
when adding a new blog.
Step 2: Merge update() and create() Functions
Next, let’s merge the update()
and create()
functions in the Blog class. We can chain this.set().save()
functions for both update()
and create()
. For the fields that don’t need to be touched by the update()
function, we can fill with the current value:
update: function(title, content) { this.set({ 'title': title, 'content': content, // Set author to the existing blog author if editing, use current user if creating // The same logic goes into the following three fields 'author': this.get('author') || Parse.User.current(), 'authorName': this.get('authorName') || Parse.User.current().get('username'), 'time': this.get('time') || new Date().toDateString() }).save(null, { success: function(blog) { alert('You updated a new blog: ' + blog.get('title')); }, error: function(blog, error) { console.log(blog); console.log(error); } }); }
Step 3: Merge AddBlogView and EditBlogView
Now, it’s time to merge the two views:
WriteBlogView = Parse.View.extend({ template: Handlebars.compile($('#write-tpl').html()), events: { 'submit .form-write': 'submit' }, submit: function(e) { e.preventDefault(); var data = $(e.target).serializeArray(); // If there's no blog data, then create a new blog this.model = this.model || new Blog(); this.model.update(data[0].value, data[1].value); }, render: function(){ var attributes; // If the user is editing a blog, that means there will be a blog set as this.model // therefore, we use this logic to render different titles and pass in empty strings if (this.model) { attributes = this.model.toJSON(); attributes.form_title = 'Edit Blog'; } else { attributes = { form_title: 'Add a Blog', title: '', content: '' } } this.$el.html(this.template(attributes)).find('textarea').wysihtml5(); } })
Notice how you can use if(this.model)
to pivot between add and edit functions.
Step 4: Update Router
Finally, let’s link to this new WriteBlogView
in the router. Just change both views to WriteBlogView
and it should still be working as usual.
add: function() { // Check login if (!Parse.User.current()) { this.navigate('login', { trigger: true }); } else { var writeBlogView = new WriteBlogView(); writeBlogView.render(); $container.html(writeBlogView.el); } }, edit: function(id) { // Check login if (!Parse.User.current()) { this.navigate('login', { trigger: true }); } else { var query = new Parse.Query(Blog); query.get(id, { success: function(blog) { var writeBlogView = new WriteBlogView({ model: blog }); writeBlogView.render(); $container.html(writeBlogView.el); }, error: function(blog, error) { console.log(error); } }); } }
Notice that you should also send visitors back to the login page if they are not logged in.
3. Add Access Control List to Blogs
Now that we’ve taken out all the repetitive code, we can now move on to some of the features we can improve on.
Many of you have asked how we can keep the data safe if the API is in the code. Parse.js provides both class-level and item-level Access Control Lists (ACLs) to help manage user access. We’ve talked about class-level ACLs in Part 3: User Login. Today, I will show you how to add an item-level ACL.
As an example, let’s assume we want every blog only to be editable by its original author.
To make that happen, we need to set the ACL
field in the update()
function:
update: function(title, content) { // Only set ACL if the blog doesn't have it if ( !this.get('ACL') ) { // Create an ACL object to grant access to the current user // (also the author of the newly created blog) var blogACL = new Parse.ACL(Parse.User.current()); // Grant read-read only access to the public so everyone can see it blogACL.setPublicReadAccess(true); // Set this ACL object to the ACL field this.setACL(blogACL); } this.set({ ... }); }
4. Root and Static URL
Another issue that many of you may feel is that it’s quite hard to test the blog system you are creating. Every time you test, you have to go back to http://localhost/your-directory/ to trigger the router.
Let’s solve this problem first.
Step 1: Add a Root in BlogRouter.start()
Parse.js makes it quite easy to do that, so let’s just change the BlogRouter.start()
function to set a file root.
start: function(){ Parse.history.start({ // put in your directory below root: '/tutorial_blog/' }); }
Notice that we can take out the this.navigate()
function now.
Step 2: Static URL
Another issue with the URLs we have right now is that they can’t be bookmarked or revisited. Everything you want to do, you need to start from the main URL. For example, if you visit http://localhost/blog/admin, the router is set to accept this URL pattern but the server still returns 404. That’s because when you visit /admin
, your server doesn’t know it should actually go to index.html
to start the router in the first place.
One way to solve this problem is to configure your server so it redirects all the URLs to index.html
. But that’s not really in the scope of this class. We are going to try the other method: add a #/
before all our URLs.
The URL of the admin panel would look like this: http://localhost/blog/#/admin. It’s not really ideal, but it’s an easy way around. When the browser meets /#
, it's not going to treat the rest of the URL as a file path; instead, it will direct the user to index.html, so our Router can pick up the rest.
Now, let’s go ahead and change the href
attribute of all the <a>
tags in the index.html
from something like this:
<a class="app-link" href="edit/{{url}}">Edit</a>
to something like this:
<a class="app-link" href="#/edit/{{url}}">Edit</a>
Similarly, let’s change all the BlogApp.navigate()
in blog.js
from something like this:
BlogApp.navigate('admin', { trigger: true });
to something like this:
BlogApp.navigate('#/admin', { trigger: true });
You can take out some of the events to use the <a>
tag, too.
For example, the “Add a New Blog” button used to use an event:
events: { 'click .add-blog': 'add' }, add: function(){ blogRouter.navigate('#/add', { trigger: true }); }
We can take out those and change it to a link in index.html
:
<a href="#/add" class="add-blog btn btn-lg btn-primary">Add a New Blog</a>
You can also take out this function since the URLs will be working by themselves:
$(document).on('click', '.blog-nav-item', function(e) { e.preventDefault(); var href = $(e.target).attr('href'); blogRouter.navigate(href, { trigger: true }); $(this).addClass('active').siblings().removeClass('active'); });
Let’s also take out the active
class for now, but we will add it back and make it work in later sessions in another way.
<nav class="blog-nav"> <a class="blog-nav-item" href="">Home</a> <a class="blog-nav-item" href="#/admin">Admin</a> </nav>
Alright, go through your blog, test, and make sure all the links are now to http://localhost/#/... except the homepage.
Now you have URLs that you can refresh and revisit. Hope this makes your life a lot easier!
Bonus: Other Fixes and Improvement
If you don’t mind the super-long tutorial and would like to make a few more improvements, here are a few other fixes and improvements you can do.
Step 1: Sort
One thing you might also notice is that the blogs are sorted from the earliest to the latest. Usually we would expect to see the latest blogs first. So let’s change the Blogs
collection to sort them in that order:
Blogs = Parse.Collection.extend({ model: Blog, query: (new Parse.Query(Blog)).descending('createdAt') })
Step 2: Redirect to WelcomeView after update()
Here's another thing we can improve on. Instead of popping out an alert window after updating a blog, let’s just make a redirect to the /admin
page:
this.set({ ... }).save(null, { success: function(blog) { blogRouter.navigate('#/admin', { trigger: true }); }, error: function(blog, error) { ... } });
Step 3: Merge AdminView in WelcomeView
If you get into cleaning, you can also merge AdminView and WelcomeView into one—there’s no real need to have two separate views.
Again, the HTML template first:
<script id="admin-tpl" type="text/x-handlebars-template"> <h2>Welcome, {{username}}!</h2> <a href="#/add" class="add-blog btn btn-lg btn-primary">Add a New Blog</a> <table> <thead> <tr> <th>Title</th> <th>Author</th> <th>Time</th> <th>Action</th> </tr> </thead> <tbody> {{#each blog}} <tr> <td><a class="app-link" href="#/edit/{{objectId}}">{{title}}</a></td> <td>{{authorName}}</td> <td>{{time}}</td> <td> <a class="app-link app-edit" href="#/edit/{{objectId}}">Edit</a> | <a class="app-link" href="#">Delete</a> </td> </tr> {{/each}} </tbody> </table> </script>
Then let’s change BlogRouter.admin()
to pass username
to AdminView
:
admin: function() { var currentUser = Parse.User.current(); // Check login if (!currentUser) BlogApp.navigate('#/login', { trigger: true }); this.blogs.fetch({ success: function(blogs) { var blogsAdminView = new BlogsAdminView({ // Pass in current username to be rendered in #admin-tpl username: currentUser.get('username'), collection: blogs }); blogsAdminView.render(); $('.main-container').html(blogsAdminView.el); }, error: function(blogs, error) { console.log(error); } }); }
And then pass down the username
to be rendered in #admin-tpl
:
BlogsAdminView = Parse.View.extend({ template: Handlebars.compile($('#admin-tpl').html()), render: function() { var collection = { // Pass in username as variable to be used in the template username: this.options.username, blog: this.collection.toJSON() }; this.$el.html(this.template(collection)); } })
Step 4: $container
Finally, we can store $('.main-container')
as a variable to avoid making multiple queries.
var $container = $('.main-container');
And just replace all the $('.main-container')
with $container
.
Conclusion
First of all, congratulations for making it to the end! It’s been a long session, but you’ve cleaned up the whole project. In addition, you also added ACL to blogs, implemented static URLs, and made a ton of other fixes. Now it’s a really solid project.
In the next session, we will speed things up and add three new functions: single blog view, delete a blog, and logout, because now you have a good understanding of Parse.js and can move forward much faster. I would recommend thinking about how to write those functions ahead of time so you can test your knowledge a little bit. Other than that, stay tuned!
Comments