DoneJS StealJS jQuery++ FuncUnit DocumentJS
3.14.1
5.0.0 4.3.0 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
    • getting started
      • Setting Up CanJS
      • Reading the Docs (API Guide)
    • recipes
      • Credit Card Guide (Advanced)
      • Credit Card Guide (Simple)
      • CTA Bus Map (Medium)
      • File Navigator Guide (Advanced)
      • File Navigator Guide (Simple)
      • Playlist Editor (Advanced)
      • Signup and Login (Simple)
      • TodoMVC with StealJS
    • upgrade
      • Migrating to CanJS 3
      • Using Codemods
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Signup and Login (Simple)

  • Edit on GitHub

This guide walks through building simple signup, login forms and a logout button.

In this guide, you will learn how to:

  • Set up a basic CanJS application.
  • Collect form data and post it to a service endpoint when the form is submitted.

The final widget looks like:

JS Bin on jsbin.com

To use the widget:

  1. Click the Sign up link.
  2. Enter an email and password and Click SIGN UP. You will be logged in.
  3. Click the log out link. You will be presented with the Sign Up form.
  4. Click the Log in link. Enter the same email and password you used to sign up. Click the LOG IN button. You will be logged in.

START THIS TUTORIAL BY CLONING THE FOLLOWING JS BIN:

JS Bin on jsbin.com

This JS Bin has initial prototype HTML and CSS which is useful for getting the application to look right.

The following sections are broken down into:

  • The problem — A description of what the section is trying to accomplish.
  • What you need to know — Information about CanJS that is useful for solving the problem.
  • The solution — The solution to the problem.

Understanding the service API

This JSBin comes with a mock service layer provided by can.fixture. It supplies:

  • POST /api/session for creating sessions (log in).
  • GET /api/session for returning if there is a session.
  • DELETE /api/session for deleting a session (log out).
  • POST /api/users for creating users.

To tell if the current client is logged in:

=>
GET /api/session

<=
STATUS: 200
{user: {email: "someone@email.com"}}

If someone is logged out:

=>
GET /api/session

<=
STATUS: 404
{message: "No session"}

To log someone in:

=>
POST /api/session
{user: {email: "someone@email.com", password: "123"}}

<=
STATUS: 200
{user: {email: "someone@email.com"}}

If someone logs in with invalid credentials:

=>
POST /api/session
{user: {email: "WRONG", password: "WRONG"}}

<=
STATUS: 401 unauthorized
{ message: "Unauthorized"}

To log someone out:

=>
DELETE /api/session

<=
STATUS: 200
{}

To create a user:

=>
POST /api/users

{email: "someone@email.com", password: "123"}

<=
STATUS: 200
{email: "someone@email.com"}

Setup

The problem

Let’s create a app-view template with all the HTML content and render it with a ViewModel called AppViewModel.

What you need to know

  • CanJS uses can-stache to render data in a template and keep it live. Templates can be authored in <script> tags like:

    <script type="text/stache" id="app-view">
      TEMPLATE CONTENT
    </script>
    

    A can-stache template uses {{key}} magic tags to insert data into the HTML output like:

    <script type="text/stache" id="app-view">
      {{something.name}}
    </script>
    
  • Load a template from a <script> tag with can.stache.from like:

    var view = can.stache.from(SCRIPT_ID);
    
  • Render the template with data into a documentFragment like:

    var frag = view({
      something: {name: "Derek Brunson"}
    });
    
  • Insert a fragment into the page with:

    document.body.appendChild(frag);
    
  • DefineMap.extend allows you to define a property with a default value like:

    AppViewModel = can.DefineMap.extend("AppViewModel",{
      isLoggedIn: {value: false}
    })
    

    This lets you create instances of that type, get and set those properties and listen to changes like:

    var viewModel = new AppViewModel({});
    
    viewModel.isLoggedIn //-> false
    
    viewModel.on("isLoggedIn", function(ev, newValue){
      console.log("isLoggedIn changed to ", newValue);
    });
    
    viewModel.isLoggedIn = true //-> logs "isLoggedIn changed to true"
    

The solution

Wrap the HTML content in the body with the following script tags:

<script type='text/stache' id='app-view'>
  <p class="welcome-message">
    Welcome Someone.
    <a href="javascript://">log out</a>
  </p>

  <form>
    <h2>Sign Up</h2>

    <input placeholder='email'/>

    <input type='password'
         placeholder='password'/>

    <button>Sign Up</button>

    <aside>
      Have an account?
      <a href="javascript://">Log in</a>
    </aside>
  </form>

  <form>
    <h2>Log In</h2>

    <input placeholder='email'/>

    <input type='password'
       placeholder='password'/>

    <button>Log In</button>

    <div class='error'>error message</div>

    <aside>
      Don't have an account?
      <a href="javascript://">Sign up</a>
    </aside>
  </form>
</script>

Update the JavaScript tab to:

var AppViewModel = can.DefineMap.extend({

});

var viewModel = new AppViewModel({});

var view = can.stache.from("app-view");
var frag = view(viewModel);

document.body.appendChild(frag);

Check if the user is logged in

The problem

Lets make a request to GET /api/session to know if there is a session. If there is a session, we will print out the user's email address. If there is not a session, we will show the Sign Up form.

We'll keep the session data within a Promise on the sessionPromise property. The following simulates a logged in user:

viewModel.sessionPromise = Promise.resolve({user: {email: "someone@email.com"}})

What you need to know

  • The value property definition can return the initial value of a property like:
    var AppViewModel = can.DefineMap.extend({
      myProperty: {
        value: function(){
          return "This string"
        }
      }  
    });
    new AppViewModel().myProperty //-> "This string"
    
  • can.ajax can make requests to a url like:
    ajax({
      url: "http://query.yahooapis.com/v1/public/yql",
      data: {
        format: "json",
        q: 'select * from geo.places where text="sunnyvale, ca"'
      }
    }) //-> Promise
    
  • Use {{#if(value)}} to do if/else branching in can-stache.
  • Promises are observable in can-stache. For a promise myPromise:
    • myPromise.value is the resolved value of the promise
    • myPromise.isPending is true if the promise has not resolved
    • myPromise.isResolved is true if the promise has resolved
    • myPromise.isRejected is true if the promise was rejected
    • myPromise.reason is the rejected value of the promise

The solution

Update the template in the HTML tab to:

<script type='text/stache' id='app-view'>
  {{#if(sessionPromise.value)}}

    <p class="welcome-message">
      Welcome {{sessionPromise.value.user.email}}.
      <a href="javascript://">log out</a>
    </p>

  {{else}}

    <form>
      <h2>Sign Up</h2>

      <input placeholder='email'/>

      <input type='password'
           placeholder='password'/>

      <button>Sign Up</button>

      <aside>
        Have an account?
        <a href="javascript://">Log in</a>
      </aside>
    </form>

  {{/if}}
</script>

Update the JavaScript tab to:

var AppViewModel = can.DefineMap.extend({
  sessionPromise: {
    value: function(){
      return can.ajax({
        url: "/api/session"
      });
    }
  }
});

var viewModel = new AppViewModel({});

var view = can.stache.from("app-view");
var frag = view(viewModel);

document.body.appendChild(frag);

Signup form

The problem

Lets allow the user to enter an email and password and click Sign Up. When this happens we'll POST this data to /api/users. Once the user is created, we'll want to update the sessionPromise property to have a promise with a session-like object.

A promise with a session-like object looks like:

{user: {email: "someone@email.com"}}

What you need to know

  • DefineMap.extend allows you to define a property by defining its type like so:

    AppViewModel = can.DefineMap.extend("AppViewModel",{
      name: "string",
      password: "number"
    });
    
  • The toParent:to can set an input’s value to a ViewModel property like:

    <input value:to="name"/>
    
  • Use ($EVENT) to listen to an event on an element and call a method in can-stache. For example, the following calls doSomething() when the <div> is clicked:

    <div on:click="doSomething(%event)"> ... </div>
    

    Notice that it also passed the event object with %event.

  • To prevent a form from submitting, call event.preventDefault().

  • Use .then on a promise to map the source promise to another promise value.

    var source = Promise.resolve({email: "justin@bitovi.com"})
    
    var result = source.then(function(userData){
      return {user: userData}
    });
    

The solution

Update the template in the HTML tab to:

<script type='text/stache' id='app-view'>
  {{#if(sessionPromise.value)}}

    <p class="welcome-message">
      Welcome {{sessionPromise.value.user.email}}.
      <a href="javascript://">log out</a>
    </p>

  {{else}}

    <form on:submit="signUp(%event)">
      <h2>Sign Up</h2>

      <input placeholder='email' value:to="email"/>

      <input type='password'
           placeholder='password' value:to="password"/>

      <button>Sign Up</button>

      <aside>
        Have an account?
        <a href="javascript://">Log in</a>
      </aside>
    </form>

  {{/if}}
</script>

Update the JavaScript tab to:

var AppViewModel = can.DefineMap.extend({
  sessionPromise: {
    value: function(){
      return can.ajax({
        url: "/api/session"
      });
    }
  },

  email: "string",
  password: "string",
  signUp: function(event){
    event.preventDefault();
    this.sessionPromise = can.ajax({
      url: "/api/users",
      type: "post",
      data: {
        email: this.email,
        password: this.password
      }
    }).then(function(user){
      return {user: user};
    });
  }
});

var viewModel = new AppViewModel({});

var view = can.stache.from("app-view");
var frag = view(viewModel);

document.body.appendChild(frag);

Log out button

The problem

Lets update the app to log the user out when the log out button is clicked. We can do this by making a DELETE request to /api/session and updating the sessionPromise property to have a rejected value.

What you need to know

  • Use .then and Promise.reject to map a source promise to a rejected promise.

    var source = Promise.resolve({})
    
    var result = source.then(function(userData){
      return Promise.reject({message: "Unauthorized"});
    });
    
    result.catch(function(reason){
        reason.message //-> "Unauthorized";
    });
    

The solution

Update the template in the HTML tab to:

<script type='text/stache' id='app-view'>
  {{#if(sessionPromise.value)}}

    <p class="welcome-message">
      Welcome {{sessionPromise.value.user.email}}.
      <a href="javascript://" on:click="logOut()">log out</a>
    </p>

  {{else}}

    <form on:submit="signUp(%event)">
      <h2>Sign Up</h2>

      <input placeholder='email' value:to="email"/>

      <input type='password'
           placeholder='password' value:to="password"/>

      <button>Sign Up</button>

      <aside>
        Have an account?
        <a href="javascript://">Log in</a>
      </aside>
    </form>

  {{/if}}
</script>

Update the JavaScript tab to:

var AppViewModel = can.DefineMap.extend({
  sessionPromise: {
    value: function(){
      return can.ajax({
        url: "/api/session"
      });
    }
  },

  email: "string",
  password: "string",
  signUp: function(event){
    event.preventDefault();
    this.sessionPromise = can.ajax({
      url: "/api/users",
      type: "post",
      data: {
        email: this.email,
        password: this.password
      }
    }).then(function(user){
      return {user: user};
    });
  },

  logOut: function(){
    this.sessionPromise = can.ajax({
      url: "/api/session",
      type: "delete"
    }).then(function(){
      return Promise.reject({message: "Unauthorized"});
    });
  }
});

var viewModel = new AppViewModel({});

var view = can.stache.from("app-view");
var frag = view(viewModel);

document.body.appendChild(frag);

Login form

The problem

Lets allow the user to go back and forth between the Sign Up page and the Log In page. We'll do this by changing a page property to "signup" or "login".

We'll also implement the Log In form's functionality. When a session is created, we'll want to POST session data to /api/session and update sessionPromise accordingly.

What you need to know

  • Use {{#eq(value1, value2)}} to test equality in can-stache.

The solution

Update the template in the HTML tab to:

<script type='text/stache' id='app-view'>
  {{#if(sessionPromise.value)}}

    <p class="welcome-message">
      Welcome {{sessionPromise.value.user.email}}.
      <a href="javascript://" on:click="logOut()">log out</a>
    </p>

  {{else}}
    {{#eq(page, "signup")}}

      <form on:submit="signUp(%event)">
        <h2>Sign Up</h2>

        <input placeholder='email' value:to="email"/>

        <input type='password'
             placeholder='password' value:to="password"/>

        <button>Sign Up</button>

        <aside>
          Have an account?
          <a href="javascript://" on:click="gotoLogIn()">Log in</a>
        </aside>
      </form>

    {{else}}

      <form on:submit="logIn(%event)">
        <h2>Log In</h2>

        <input placeholder='email' value:to="email"/>

        <input type='password'
           placeholder='password' value:to="password"/>

        <button>Log In</button>

        <aside>
          Don't have an account?
          <a href="javascript://" on:click="gotoSignUp()">Sign up</a>
        </aside>
      </form>

    {{/eq}}

  {{/if}}
</script>

Update the JavaScript tab to:

var AppViewModel = can.DefineMap.extend({
  sessionPromise: {
    value: function(){
      return can.ajax({
        url: "/api/session"
      });
    }
  },

  email: "string",
  password: "string",
  signUp: function(event){
    event.preventDefault();
    this.sessionPromise = can.ajax({
      url: "/api/users",
      type: "post",
      data: {
        email: this.email,
        password: this.password
      }
    }).then(function(user){
      return {user: user};
    });
  },

  logOut: function(){
    this.sessionPromise = can.ajax({
      url: "/api/session",
      type: "delete"
    }).then(function(){
      return Promise.reject({message: "Unauthorized"});
    });
  },

  page: {value: "login"},
  gotoSignUp: function(){
    this.page = "signup";
  },
  gotoLogIn: function(){
    this.page = "login";
  },
  logIn: function(event){
    event.preventDefault();
    this.sessionPromise = can.ajax({
      url: "/api/session",
      type: "post",
      data: {
        user: {
          email: this.email,
          password: this.password
        }
      }
    });
  }
});

var viewModel = new AppViewModel({});

var view = can.stache.from("app-view");
var frag = view(viewModel);

document.body.appendChild(frag);

Login errors

The problem

If the user tried to login, but the server responded with an error message, let's display that error message. We'll do this by catching the create-session request. If the request failed we will set a logInError property with the server's response data.

What you need to know

  • Use .catch to handle when a promise is rejected:
    var source = Promise.reject({responseText: '{"message": "foo"}'})
    source.catch(function(reason){
        reason.responseText //->  '{"message": "foo"}'
    })
    
  • Use JSON.parse to convert text to JavaScript objects:
    JSON.parse('{"message": "foo"}') //-> {message: "foo"}
    
  • Use the "any" type to define a property of indeterminate type:
    var AppViewModel = can.DefineMap.extend({
      myProperty: "any"  
    });
    var viewModel = new AppViewModel({});
    viewModel.myProperty = ANYTHING;
    

The solution

Update the template in the HTML tab to:

<script type='text/stache' id='app-view'>
  {{#if(sessionPromise.value)}}

    <p class="welcome-message">
      Welcome {{sessionPromise.value.user.email}}.
      <a href="javascript://" on:click="logOut()">log out</a>
    </p>

  {{else}}
    {{#eq(page, "signup")}}

      <form on:submit="signUp(%event)">
        <h2>Sign Up</h2>

        <input placeholder='email' value:to="email"/>

        <input type='password'
             placeholder='password' value:to="password"/>

        <button>Sign Up</button>

        <aside>
          Have an account?
          <a href="javascript://" on:click="gotoLogIn()">Log in</a>
        </aside>
      </form>

    {{else}}

      <form on:submit="logIn(%event)">
        <h2>Log In</h2>

        <input placeholder='email' value:to="email"/>

        <input type='password'
           placeholder='password' value:to="password"/>

        <button>Log In</button>

        {{#if(logInError)}}
          <div class='error'>{{logInError.message}}</div>
        {{/if}}

        <aside>
          Don't have an account?
          <a href="javascript://" on:click="gotoSignUp()">Sign up</a>
        </aside>
      </form>

    {{/eq}}

  {{/if}}
</script>

Update the JavaScript tab to:

var AppViewModel = can.DefineMap.extend({
  sessionPromise: {
    value: function(){
      return can.ajax({
        url: "/api/session"
      });
    }
  },

  email: "string",
  password: "string",
  signUp: function(event){
    event.preventDefault();
    this.sessionPromise = can.ajax({
      url: "/api/users",
      type: "post",
      data: {
        email: this.email,
        password: this.password
      }
    }).then(function(user){
      return {user: user};
    });
  },

  logOut: function(){
    this.sessionPromise = can.ajax({
      url: "/api/session",
      type: "delete"
    }).then(function(){
      return Promise.reject({message: "Unauthorized"});
    });
  },

  page: {value: "login"},
  gotoSignUp: function(){
    this.page = "signup";
  },
  gotoLogIn: function(){
    this.page = "login";
  },
  logIn: function(event){
    event.preventDefault();
    this.sessionPromise = can.ajax({
      url: "/api/session",
      type: "post",
      data: {
        user: {
          email: this.email,
          password: this.password
        }
      }
    });

    this.logInError = null;
    this.sessionPromise.catch(function(xhr){
      this.logInError = JSON.parse(xhr.responseText);
    }.bind(this));
  },
  logInError: "any"
});

var viewModel = new AppViewModel({});

var view = can.stache.from("app-view");
var frag = view(viewModel);

document.body.appendChild(frag);

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 3.14.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news