Ember.js 2.0 Example App With Firebase And Login Authentication

Ember.js 2.0 Example App With Firebase And Login Authentication

I've written a lot of tutorials with Ember.js and Firebase. I've written about deploying your Ember application, using Simple Login, and creating a blog. It was a lot of material.

Ember has a six week release cycle so things move fast. With that said some of my previous tutorials are a little out of date. The purpose of this post is to take all the concepts from the previous tutorials and roll them into this one.

What will this tutorial cover?

This application is an example of a blog. You'll be able to login with Twitter and add or delete posts. In additions you can leave a comment on anyone else's post.

If you'd like to follow along the code is on Github. A fully working demo can be found here.

This tutorial will cover the following concepts:

  • EmberFire
  • torii/authentication
  • helpers
  • pods
  • utilities
  • routes
  • models
  • relationships
  • Promises
  • components
  • templates

This tutorial is meant for beginners to intermediate developers interested in Ember.js. I'll assume you have some knowledge of JavaScript and some basics of Ember.


Versions as of this writing

As of this writing I'm using these versions of libraries:

  • Ember CLI v1.13.13
  • Node v5.3
  • npm v2.14.10
  • Ember 2.2.0
  • Ember Data 2.2.0

The latest beta version of Ember CLI was having issues with some of the addons so I'm using Ember CLI 1.13.13 instead of the beta.

To get the latest version of Ember and Ember Data with Ember CLI v1.13.13 delete the bower_components and node_modules folders. Then update the bower.json and the package.json files with the latest versions of Ember and Ember data. Then run npm install and bower install again.


Creating the folder structure and configuring the app

We'll assume you have Ember CLI installed. If not check out the getting started guide here.

Let's begin by creating a new application. We'll just call it blog.

$ ember new blog

This will create all the necessary files for our project.

Let's edit the .ember-cli file so we can generate files using the pods structure by default.

// .ember-cli
{
  "disableAnalytics": false,
  "usePods": true
}

This will make sure all files we generate are in the pod structure. If you want more information on pods check out my pod tutorial.

Next we'll edit the environment.js file so all pod files are put into a folder called features in the app folder.

// config/environment.js
...
  var ENV = {
    modulePrefix: 'blog',
    podModulePrefix: 'blog/features',

All pods will now be installed in app/features.

Let's generate the rest of our files now.

$ ember g adapter application
$ ember g route application
$ ember g model comment
$ ember g component add-comments
$ ember g component edit-post
$ ember g component new-post
$ ember g template index
$ ember g route new
$ ember g model post
$ ember g route posts
$ ember g model user
$ ember install emberfire
$ ember install torii
$ ember install ember-moment
$ ember install ember-cli-showdown
$ ember install ember-bootstrap

You may have noticed that we have a features/post folder and a features/posts folder. This is true with user and comment as well. Unfortunately by convention model's are usually named with a singular name and the routes are plural. This doesn't quite fit in with the pod structure. There is talk here on what to do about it for the future while the pod structure is finalized. For now we'll live with it.

Update the config file for Firebase

After installing the Firebase addon we'll need to configure it.

// config/environment.js
...
firebase: 'https://YOUR-FIREBASE-NAME.firebaseio.com/',
torii: {
   sessionServiceName: 'session'
},

...

Make sure to change the URL to the correct one from Firebase. If you don't have one already signup for an account here. It'll walk you through creating your first Firebase app.

When using Emberfire with torii we'll have access to a service called session. This session service will be used throughout the application. After being authenticated we'll be able to pull the username {{session.currentUser.username}}, uid {{session.uid}} and profileImageURL {{session.currentUser.profileImageURL}} from it. You can find more about the twitter session object here.

After installing Emberfire it creates an application adapter for us! How convenient, although it doesn't generate it in the correct location for the pod structure. We'll need to move it to the right place.

$ mv app/adapters/application.js app/features/application/adapter.js

Configuring Torii

Torii is an authentication library for Ember. It's the preferred way to work with authentication with Emberfire.

Create a new folder in app called torii-adapters

// app/torii-adapters/application.js
import Ember from 'ember';
import ToriiFirebaseAdapter from 'emberfire/torii-adapters/firebase';
export default ToriiFirebaseAdapter.extend({
  firebase: Ember.inject.service()
});

This will configure torii so we can use authetication in our app. You can take a sneak peak of how it's implementing everything by looking at the node_modules/emberfire/addon/torii-adapters/firebase.json file. This is where the open, fetch and close methods are defined.

The next step is to log into the Firebase dashboard and setup the User Login and Authentication tab for Twitter. I'm not going to get into details because you can find the directions here. Essentially you'll need to create a Twitter application and input the Twitter API key and API secret key into the dashboard.

After this is all setup we'll be able to use the session service throughout our application to login, logout and check authentication.

Delete non-pod structure folders

Since we are using pods we can clean up the app folder structure. Delete all the folders except for styles, features and torii-adapters. The directory structure should look like this.

  • features/
  • styles/
  • torii-adapters/
  • app.js
  • index.html
  • router.js

That's it! We'll create a few more folders for the utils and helpers later. For now this will work.


Using ES6 in our App

In my past tutorials I have used mostly used ES5 with some ES6. That's fine and it will work. With that said it's recommended to use ES6 in your Ember applications. It just makes things easier.

If you'r not familiar ES6 is the new ECMAScript standard. I've written about it here.. It adds a bunch of new features into JavaScript.

In this tutorial I'll be sticking with ES6. The three most important ES6 features I'll be using in this tutorial are:

Destructuring

Throughout this tutorial you'll see destructuring statements at the top of many class files. It may look like this.

const {set, get} = Ember

This is an easier way to create the variable set and get for Ember.set/Ember.get. Instead of having to write out Ember.get or Ember.set over and over again I can use the shorthand set or get instead.

You maybe asking why I'm not using this.set or this.get? Well their equal, and there are some advantages in using Ember.get instead of this.get. It's also seems to be the preferred method to use.

Arrow Functions

In this app I'll stick to using arrow functions. They have a shorter syntax and it lexically binds the this value. They look like this.

    //old way
    a.map(function(s){return s.length});
    //new way
    a.map(s => s.length); //or
    a.map((s) => s.length);
     

Method Definitions

Using the new ES6 method definitions saves a little bit of time. This is the older method function syntax.

...
actions:{
     submitAction: function(){
           //code here
      }
}

Now we can remove the function part all together.

...
actions: {
       submitAction(){
       }
}
...

It's just a little cleaner.

There are a few other things I'm using, but you'll notice these the most.


Configure the router

For this application we want the title of each post to be the name of the route. For example if the title of the blog post is Ember 2.0 Example App then the URL should be /ember_20_example_app. We'll use a utility to help make this possible. For now we'll create a dynamic segment called :titleURL.

// app/router.js
import Ember from 'ember';
import config from './config/environment';

const Router = Ember.Router.extend({
  location: config.locationType
});

Router.map(function() {
   this.route('posts', {path: '/:titleURL'});
   this.route('new');
});

export default Router;

The application route is already defined by default. This route is where we'll display each blog post. The posts route will display each individual blog post. The new route is where we will add new blog posts.


Defining our models

We'll be using three different models: comment, post and user. The relationships will follow these rules.

  • Each user will have zero or more comments
  • Each user will have zero or more posts
  • Each post will have zero or more comments

This is a very simple example as demonstrated by my UML diagram above.

Post Model

Let's begin by taking a look at the posts model.

// app/features/post/model.js
import DS from 'ember-data';

export default DS.Model.extend({
    title: DS.attr('string'),
    body: DS.attr('string'),
    titleURL: DS.attr('string'),
    comments: DS.hasMany('comment' ),
    user: DS.belongsTo('user'),
    date: DS.attr('date')

});

Each post as a title, body, titleURL and date. Each comment also has many comments and belongs to user.

Comment Model

// app/features/comment/model.js
import DS from 'ember-data';

export default DS.Model.extend({
    body: DS.attr('string'),
    user: DS.belongsTo('user'),
    post: DS.belongsTo('post')

});

The comment model has a body and belongs to user and post. That way you can track the user and post with each comment.

User Model

// app/features/user/model.js
import DS from 'ember-data';

export default DS.Model.extend({
  uid: DS.attr('string'),
  username: DS.attr('string'),
  avatar: DS.attr('string'),
  posts: DS.hasMany('post'),
  comments: DS.hasMany('comment')
});

The user model has a uid, which is where we will store the unique identifier that is passed from Firebase after authenticating the user. We also store the username, the avatar which is the Twitter picture, and it has many posts and comments.


Utils and Helpers

To make our job easier we can use utils and helpers. Utils are small snippets of code that we can import anywhere. It'll help us with some common tasks.

Helpers are used with templates. You might be familiar with input helpers. Ember gives us the flexibility in creating our own.

Creating an excerpt Helper

When displaying each post on the front page we'll want to just show the first 15 characters. That way the user will get an idea what the post is about. To do that we'll create a really simple helper called excerpt.

// app/features/excerpt/helper.js
import Ember from 'ember';

export function excerpt(params) {
  return params[0].substring(0,15)+'...';
}

export default Ember.Helper.helper(excerpt);

When working with helpers they will receive a params array. The excerpt function returns a substring of the first 15 characters and appends '...' at the end.

Checking user util

The first of two utilities we'll be creating is called get-or-create-user.

// app/features/get-or-create-user/util.js
import Ember from 'ember';

const {RSVP: {Promise}} = Ember;

export default function getOrCreateUser(uid,username,avatar,store) {

    return new Promise((resolve)=>{
        store.query('user', {orderBy: 'uid', equalTo: uid }).then( (records) =>{
            if(records.get('length') === 0){
                resolve(store.createRecord('user',{
                    uid: uid,
                    username:username,
                    avatar:avatar
                }));// end resolve
            }// end if
            else{
                resolve(records.get('firstObject'));
            }// end else
        });// end store
    }); // end promise
}

In this example we create a Ember.RSVP.Promise that resolves with either the record of the new user or the existing user record.

The purpose of this is to find if the uid passed to the function matches an existing user. If it does it returns that user, if not it creates a new one.

To accomplish this we use the Emberfire query method. The documentation is here. We have to pass in the store because the store is not injected into utils by default.

Cleaning the URL

To make this tutorial more interesting I changed the way we are storing the URL to each post. Each post's title will act as the URL. Therefore we need to clean the URL so it doesn't have any weird characters in it.

// app/features/clean/util.js
export default function clean(title) {
    title = title.replace(/ /g, '_');
    return title.replace(/[^a-zA-Z0-9-_]/g, '');
}

This function changes all spaces to underscores and removes all characters except a-z, A-Z and 0-9, using some regular expression.


Components

In our tutorial we want our components help with the logic for our posts. The components will allow the user to edit, delete or create new posts. It will also handle the comments.

When creating components it's a good idea to keep a few best practices in mind. One of the most important ones is data down and action up. We want to pass our models and session information down, but we don't want to mutate or change them in the component. Rather we'll send them back up to the route to be updated.

We'll be using sendAction in this example instead of closure actions. Check out Jeff's tutorial on closure actions for more information. The reason being is that you can't pass a closure action from a route. At least not until routable components lands in Ember.js. Since I have all my actions in my routes and I don't have anything in my controllers I'll use sendAction for now.

The reason being is that you can't pass a closure action from a route.

Comments component and template

The comment component will display and add comments.

// app/features/components/add-comments/template.hbs
<h4>Comments:</h4>

{{#unless post.comments}}
    <h4> No Comments</h4>
{{/unless}}
    {{#each post.comments as |comment|}}
<div class='comments'>
    <img src='{{comment.user.avatar}}'/>
        {{comment.body}}<br>
    -<a href='http://twitter.com/{{comment.user.username}}' target='_blank'>{{comment.user.username}}</a><br>
</div>
    {{/each}}

{{#if session.isAuthenticated}}
<div class='box'>
    <img src='{{session.currentUser.profileImageURL}}' height='70px' width='70px'/>
    <textarea rows='2' placeholder='Add New Comment' value= {{body}} onchange={{action (mut body) value='target.value'}} />
    <button {{action 'submitComment' session body}}>Submit</button>
</div>
{{/if}}

The session and comment is passed into the component. We can use it to check if the user is logged in and display comments. If they are authenticated they'll be able to leave a comment. The unless and if helpers to make this possible.

...
<textarea rows='2' placeholder='Add New Comment' value= {{body}} onchange={{action (mut body) value='target.value'}} />
...

Ember 2.0 has been promoting one-way-binding. What that means in our example is that the body property is copied to the template. Any changes to it won't effect the parent component's property. This actually makes a lot of sense.

One way to update the body property in the template is to attach an action to the onchange event. When any change occurs the target.value is copied to the body property. The mut helper makes the body property mutable. Once this is done the body property can then be submitted as an argument to the submitComment action in the component.

In some instances using one way bindings with native <input> elements might cause cursor problems. Check out Dockyards ember-one-way-controls addon. It fixes this and adds a lot more features.

// app/features/components/add-comments/component.js
import Ember from 'ember';

export default Ember.Component.extend({
    actions:{
        submitComment(author, body){
            let post = this.get('post');
            this.sendAction('store',author,body,post);
            this.setProperties({
                body: ''
            });
        }
    }
});

The submit action receives the author and body It then sends the action to the route and clears the property.

Edit post component and template

The edit-post component does a lot of things. It's used to display each post and allow logged in users to edit their posts.

// app/features/components/edit-post/template.hbs
{{#link-to  'index'}}
&#8592; back
{{/link-to}}
{{#each model as |post|}}
<div class='row'>
    {{#if isEditing}}
        <div class='col-md-4 border'>
            <form {{action 'save' post on='submit'}}>
                <dl>
                    <dt>Title:<br>  <textarea cols=40 rows='1' value= {{post.title}} oninput={{action (mut post.title) value='target.value'}} /></dt>
                    <dt>Body:<br><textarea cols=40 rows='6'  value= {{post.body}} oninput={{action (mut post.body) value='target.value'}} /></dt>
                </dl>
                <div class = 'row'>
                    <button type='submit' class = 'btn btn-primary'>Done</button>
                </div>
            </form>
        </div>
    {{/if}}
    <div  class='col-md-4 border'>
                <h1><img src='{{post.user.avatar}}'/>{{post.title}}</h1>
                <h3>{{markdown-to-html markdown=post.body}}</h3>
                <h4>-<a href='http://twitter.com/{{post.user.username}}' target='_blank'>{{post.user.username}}</a> </h4>
                {{#if post.date}}
                {{moment-from-now post.date}}
                {{/if}}

            {{#if session.isAuthenticated}}
            {{#if isAllowed}}
                <form >
                <button type='submit' class='btn btn-primary'{{action 'edit' }}>Edit</button>
                <button type='delete' class= 'btn btn-primary' {{action 'delete' post}}>Delete</button>
                </form>
            {{/if}}
            {{/if}}
    </div>
</div>
{{add-comments post=post session=session store='createComment' }}
{{/each}}

The template is broken into two parts. The first part only displays if isEditing is true. The next section shows the post. The markdown addon is used here to convert the text in post.body to markdown. This gives us a preview of our edits while we're typing. The moment addon is also used here to display the date. Once again we'll use the <textarea> element using one-way-binding to capture the body and title.

It should look like this.

The text on the left is displayed in markdown on the right.

Here is the component.

// app/features/components/edit-post/component.js
import Ember from 'ember';
const {get, set} = Ember;

export default Ember.Component.extend({
    isEditing: false,
       classNames: 'edit',
       isAllowed: Ember.computed('model.firstObject.user.username','session.currentUser.username', function(){
        return get(this,'model.firstObject.user.username') === get(this,'session.currentUser.username');
       }),
       actions:{
           save(post){
               let sessionName = get(this,'session.currentUser.username');
               if(sessionName === post.get('user.username')){
                   set(this, 'isEditing', false);
                   this.sendAction('save',post);

               }
               else{
                   alert('Sorry not authorized');
               }

           },
        edit(){
            set(this, 'isEditing', true);
        },
        delete(post){
                this.sendAction('delete',post);
                set(this,'isEditing',false);
        },
        createComment(author, body, post){
            this.sendAction('createComment',author, body, post);
        }
    }
});


Let's take a look a little closer.

       isAllowed: Ember.computed('model.firstObject.user.username','session.currentUser.username', function(){
        return get(this,'model.firstObject.user.username') === get(this,'session.currentUser.username');
       }),

This computed property takes the model and session objects passed in and checks to see if the username matches**. The purpose is so users can't edit other people's posts only their own. The isAllowed computed property will return true if everything matches and false if it doesn't.
**This will need to be enforced on the server side to be effective

The rest of the actions use sendAction to send everything to the route. The isEditing property keeps track if the article is being edited.

New post component and template

The new post, as the name suggests, is used to create new posts.

// app/features/components/new-post/template.hbs
<br><br>
<div class='col-md-4 border' >
        <h1>New Post</h1>
        <form {{action 'save' title body on='submit'}}>
        <dl>
            <dt>Title:<br> <textarea cols=40 rows='1' placeholder='Title' value= {{title}} oninput={{action (mut title) value='target.value'}} /></dt>
            <dt>Body:<br> <textarea cols=40 rows='6' placeholder='Body'  value= {{body}} oninput={{action (mut body) value='target.value' }} /></dt>
        </dl>
        <button type='submit' class='btn btn-primary'>Add</button>
        </form>
</div>
<div id='preview' class='col-md-4 border'  >
    <h1>Preview</h1>
    <h3>{{title}}</h3>
    <h4>{{markdown-to-html markdown=body}}</h4>
</div>

The template looks similar to the edit-post component template. I thought of trying to combine the two, however It ended up being too much work.

One side shows the text boxes and the other side shows the edits. Once again we'll use one-way-input binding and the markdown addon.

// app/features/components/new-post/component.js
import Ember from 'ember';
const {set} = Ember;
export default Ember.Component.extend({
    classNames: 'new',
    actions:{
        save(title, body){
            this.sendAction('save',title, body);
            set(this,'title','');
            set(this,'body','');

        }
    }
});

This takes both the title and body and grabs the passed in save value and sends the action.

If all goes well it should look like this.

That's it for our components.


Application

When the app loads the application route will load. The application and index template will also load. In our app we want all the posts listed and clickable. Each post will link to the posts route that will display the edit-post component.

Application Templates

The application template is using bootstrap to display a simple navigation menu at the top.

// app/features/application/template.hbs
<nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container-fluid">
                <div class="navbar-header" href="#">
                        {{#link-to 'index' class='navbar-brand'}}Program With Erik Blog Example{{/link-to}}
                </div>
                <ul class="nav navbar-nav">
                    {{#if session.isAuthenticated}}
                        <li>{{#link-to 'new'}}Add New Post{{/link-to}}</li>
                    {{/if}}
                </ul>
                <ul class="nav navbar-nav navbar-right">
                    {{#unless session.isAuthenticated}}
                        <li><a href="#" {{action 'login' }}>Login</a></li>
                    {{else}}
                        <li><a href="#" {{action 'logout' }}>Logout</a></li>
                        <li>
                            <a href='http://twitter.com/{{session.currentUser.username}}' target='_blank'>
                                <img src='{{session.currentUser.profileImageURL}}' height='30px' width='30px'/>
                            </a>
                        </li>
                    {{/unless}}
                </ul>
        </div>
</nav>
{{outlet}}

In the code above the session service is used to display menu options. The login link logs the user in while the logout link logs the user out. Just for fun I added the current logged in users profile as a picture at the top that links to their Twitter profile.

// app/features/index/template.hbs
<div class = 'row main'>
    <div class='col-md-4'>
        <h1>Posts</h1>
            {{#each model as |post|}}
            <br><h2 class='post-title'>{{#link-to 'posts' post.titleURL}}{{post.title}}{{/link-to}}</h2>
            <section class='post excerpt'>
                <p>{{excerpt post.body}} -{{post.user.username}}</p>
            </section>
            {{#link-to 'posts' post.titleURL}}continue reading &#8594;{{/link-to}}
            {{/each}}
    </div>

</div>

The index template will render inside the application's outlet. This is where each post will be displayed. The each helper iterates over each post. This is also where we use the excerpt helper we created earlier.

Notice how each post is linked to the titleURL, not the id? This is so we can have better looking URLs. The titleURL will get passed to the posts route so it can lookup the correct post and return the model.

Application Route

// app/features/application/route.js
import Ember from 'ember';
const {get} = Ember;

export default Ember.Route.extend({
    beforeModel(){
        return get(this,'session').fetch().catch(function(){});
    },
    model(){
        return this.store.findAll('post');
    },
    actions:{
        login(){
            get(this,'session').open('firebase', { provider: 'twitter'}).then(function(data) {
                            console.log(data);
                  });
        },
        logout(){
            get(this,'session').close();
        }
    }
});

The route is responsible for returning the model and handling actions. In this example the model returns all the posts in the database.

The login and logout actions use the session service. To login we call open and give it the name of the provider. In this case Twitter. To logout all we need to do is call close.

In the future I'd like to update the application so the user is saved after a successful login. In addition a failed login should prompt the user. This isn't implemented yet either although it would be easy to add. As of now the user will be created later when they create a comment or a new post and a failed login isn't handled. This is fine for this tutorial.


Displaying and Editing Posts

The editing and displaying of individual posts is left up to the posts route. It uses the edit-post component to display the template and handle some basic logic. The posts route is responsible for communication with Firebase.

Posts Template

// app/features/posts/template.hbs
{{edit-post model=model session=session save='save' delete='delete' createComment='createComment'  }}

This posts template displays the edit-post component. The component by design is isolated so we must pass in the session and model into it. This is also where we declare the name of the save, delete and createComment methods. These should match the names in the route.

Posts Route

// app/features/posts/route.js
import Ember from 'ember';
import cleanURI from '../clean/util';
import getOrCreateUser from '../get-or-create-user/util';
const {get} = Ember;

export default Ember.Route.extend({
    model(param) {
        return this.store.query('post', {orderBy: 'titleURL',equalTo: param.titleURL });
    },
       actions:{
           delete(post){
               post.deleteRecord();
               post.save();
               this.transitionTo('index');
           },
       save(post){
           let titleURL = cleanURI(post.get('title'));
           post.set('titleURL',titleURL);
           post.save();
           this.transitionTo('index');
       },
       createComment(author, body,post){
           let user = null;
           let comment = this.store.createRecord('comment', {
               body: body
           });
           let uid = author.get('uid');
           user = getOrCreateUser(uid,
                   get(this,'session.currentUser.username'),
                   get(this,'session.currentUser.profileImageURL'),
                   this.store);

           user.then((userData)=>{
               userData.get('comments').addObject(comment);
               post.get('comments').addObject(comment);
               console.log('test');
               return comment.save().then(()=>{
                                        console.log('comment saved succesfully');
                                        return post.save();
                                    })
                                    .catch((error)=>{
                                        console.log(`comment:  ${error}`);
                                        comment.rollbackAttributes();
                                    })
                                    .then(()=>{
                                        console.log('post saved successfuly');
                                        return userData.save();
                                    })
                                    .catch((error)=>{
                                        console.log(`post:  ${error}`);
                                        post.rollbackAttributes();
                                    })
                                    .then(()=>{
                                        console.log('user saved successfuly');
                                    })
                                    .catch((error)=>{
                                        console.log(`user:  ${error}`);
                                        user.rollbackAttributes();
                                    });


           });

       }
   }
});


The posts route is probably the most complicated code in the app. The save and delete actions are self explanatory. Let's take a look at the createComment action and how it deals with creating a one-to-many relationship using Firebase.

...
       createComment(author, body,post){
           let user = null;
           let comment = this.store.createRecord('comment', {
               body: body
           });
           let uid = author.get('uid');
           user = getOrCreateUser(uid,
                   get(this,'session.currentUser.username'),
                   get(this,'session.currentUser.profileImageURL'),
                   this.store);

...

The first part here uses the getOrCreateUser utility we worked on earlier. It receives back an existing user model or a new one. This depends if the uid is already in the database or not. It also creates a new record for the comment.

...
         user.then((userData)=>{
               userData.get('comments').addObject(comment);
               post.get('comments').addObject(comment);
               return comment.save().then(()=>{
                                        console.log('comment saved succesfully');
                                        return post.save();
                                    })
                                    .catch((error)=>{
                                        console.log(`comment:  ${error}`);
                                        comment.rollbackAttributes();
                                    })
                                    .then(()=>{
                                        console.log('post saved successfuly');
                                        return userData.save();
                                    })
                                    .catch((error)=>{
                                        console.log(`post:  ${error}`);
                                        post.rollbackAttributes();
                                    })
                                    .then(()=>{
                                        console.log('user saved successfuly');
                                    })
                                    .catch((error)=>{
                                        console.log(`user:  ${error}`);
                                        user.rollbackAttributes();
                                    });


           });

...

The second part here uses promises. If the user promise resolves we first add the comment object to both the user and post. This is nessary when we are dealing with hasMany and belongsTo relationships. The next part uses chained promises to save information to the Firebase data store.

First we save the comment, then the post and finally the user data. It must be in this order. If any of them fails we rollback the attribute so it doesn't get saved in the data store.

We could do more error catching here, but for a basic example this is good enough.


Creating New Posts

The final route is called new and is used to create new posts.

New Templates

// app/features/new/template.hbs
{{new-post save='save'}}

This templates job is to use the new-post component.

New Route

// app/features/new/route.js
import Ember from 'ember';
import cleanURI from '../clean/util';
import getOrCreateUser from '../get-or-create-user/util';

const {get, set } = Ember;

export default Ember.Route.extend({
    actions: {
        save(title,body){
            let user = null;
            let titleURL= cleanURI(title);
            let uid = get(this,'session.uid');
            let date = new Date();
            let post = this.store.createRecord('post',{
                title: title,
                body: body,
                author: 'test',
                titleURL:titleURL,
                date: date
            });

            user = getOrCreateUser(uid,get(this,'session.currentUser.username'),
                                   get(this,'session.currentUser.profileImageURL'),
                                   this.store);
            user.then((userData)=>{
                userData.get('posts').addObject(post);
                post.save().then(()=> {
                    return userData.save();
                });

            });

            set(this, 'title','');
            set(this, 'body','');
            this.transitionTo('index');
        }
    }
});

In the route we create a new post and then add it as an object to the user. The post gets saved first then the user. This is another way of using promises and saving data.


Setting up our CSS

I'm no CSS expert so I just used some basic CSS to style a few of the components and data in it.

// app/styles/app.css
.main {
    padding: 40px;
    margin: 60px;
}

.edit{
    padding: 5px;
    margin: 60px 5px;
}

.new{
    padding: 5px;
    margin: 60px 5px;
}



.border{
    border-radius: 10px;
    border: 2px solid black;
    width: 560px;
    height: 100%;
    padding: 20px;
    margin: 20px;
}

#preview{
    height:370px;
}

.comments{
    border-radius: 5px;
    border: 2px solid black;
    width: 560px;
    margin: 20px;
    padding: 20px;

}

textarea{

    width: 460px;
}

Firebase Security

I'm still working on some rules for Firebase. Here is what I have so far.

{
    "rules": {
      ".read": true,
      "posts":{
        "$post": {
          ".write": "auth !== null"

        }
      
      },
      "comments":{
        "$comment": {
          "body":{".validate": "newData.val().length > 2"},
          ".validate": "newData.hasChildren(['body','user'])"
        },
         ".write": "auth !== null"
    
         
     
      },
      "users":{
        "$user":{
               ".write": "auth !== null "
     
        }
        
      }
   
    }
}

As of now all the security rules do is make sure that the user is authenticated before their able to write. I also validate that the comment is more then 2 characters long.

In the future I'd like to add a rule that users can't delete or edit posts that are not their own. I'll be updating these security rules in the near future.


Firebase Deploy

The final step is to deploy to Firebase.

$ npm install -g firebase-tools
$ firebase login
$ firebase init //choose dist as the public directory
$ ember build -e prod
$ firebase deploy

That's it! Be aware you should probably add a rewrite rule to your firebase.json file. Otherwise you might have some issues with URLs. Mine looks like this.


  "firebase": "testemberfire",
  "public": "dist",
    "rewrites": [{
        "source": "**",
        "destination": "/index.html"
      }],
  "ignore": [
    "firebase.json",
    "**/.*",
    "**/node_modules/**"
  ]
}


Future Improvements

As with any project there is a lot of improvements I can do for the future. Here are a few of the top of my head.

  • More error checking in the app
  • Add test cases
  • Add a way to delete comments
  • Setup pagination on the front page
  • Update security rules
  • Redo CSS

I'll be looking into this as time goes.

If you got this far please share this post with others! Click on the tweet button below!

If you see any mistakes, or improvements please post below. I'll be making updates to this tutorial as times goes on!

Thanks!

Image Credit To Lisa Speakman