Signup and Login (Simple)
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:
To use the widget:
- Click the Sign up link.
- Enter an email and password and Click SIGN UP. You will be logged in.
- Click the log out link. You will be presented with the Sign Up form.
- 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:
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 promisemyPromise
:myPromise.value
is the resolved value of the promisemyPromise.isPending
is true if the promise has not resolvedmyPromise.isResolved
is true if the promise has resolvedmyPromise.isRejected
is true if the promise was rejectedmyPromise.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 callsdoSomething()
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
andPromise.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 catch
ing 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);