DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.0.1
5.33.2 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Migrating to CanJS 6
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Signup and Login

  • Edit on GitHub

This beginner 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:

See the Pen Signup and Login (Simple) [Finished] by Bitovi (@bitovi) on CodePen.

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 clicking the “Run in your browser” button below:

<script type="module">
import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

fixture("POST /api/session", function(request, response) {
    const userData = localStorage.getItem("user");
    if (userData) {
        const user = JSON.parse(userData);
        const requestUser = request.data.user;
        if (
            user.email === requestUser.email &&
            user.password === requestUser.password
        ) {
            return request.data;
        } else {
            response(401, { message: "Unauthorized" }, {}, "unauthorized");
        }
    }
    response(401, { message: "Unauthorized" }, {}, "unauthorized");
});
fixture("GET /api/session", function(request, response) {
    const session = localStorage.getItem("session");
    if (session) {
        response(JSON.parse(session));
    } else {
        response(404, { message: "No session" }, {}, "unauthorized");
    }
});
fixture("DELETE /api/session", function() {
    localStorage.removeItem("session");
    return {};
});
fixture("POST /api/users", function(request) {
    const session = {
        user: { email: request.data.email }
    };
    localStorage.setItem("user", JSON.stringify(request.data));
    localStorage.setItem("session", JSON.stringify(session));

    return session.user;
});
</script>

<style type="text/less">
  @font-family: 'Raleway', "Helvetica Neue", Arial, sans-serif;
  @font-size: 1em;
  @color-dark: #54599c;
  @color-light: #fff;
  @color-light-gray: #d3d3d3;
  @color-light-blue: #e2f5ff;
  @color-error: #ff000e;
  @color-error-light: #fde5ec;
  @link-color: #2196F3;

  body,
  input,
  button {
    font-family: @font-family;
    font-size: @font-size;
  }

  body {
    background-color: @color-dark;
    padding: 5%;
  }

  form {
    background-color: @color-light;
    padding: 30px 40px 0 40px;
    border-radius: 6px;

  }

  input {
    border: 1px solid @color-light-gray;
    border-radius: 4px;
    width: 93%;
    padding: 3%;
    margin-bottom: 20px;
    &:focus {
      background-color: @color-light-blue;
      outline: 0;
      border-color: #a1c7e8;
    }
  }

  button {
    background-color: #3ec967;
    text-transform: uppercase;
    letter-spacing: 2px;
    border-radius: 20px;
    border: 0;
    color: White;
    padding: 10px;
    width:100%;
  }

  a {
    color: @link-color;
  }

  h2 {
    color: #b027a1;
    text-align: center;
    font-size: 2em;
  }

  aside {
    background-color: #f1f0ff;
    margin: 40px -40px;
    padding: 15px;
    border-radius: 0px 0px 6px 6px;
    text-align: center;
    color: @color-dark;
  }

  .welcome-message {
    color: white;
    text-align: center;
  }

  .error {
    padding: 20px;
    margin-top: 20px;
    text-align: center;
    color: @color-error;
    background-color: @color-error-light;
  }
</style>

This starter code includes:

  • CanJS (import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@5/core.mjs" imports can-ajax, can-fixture, can-stache-element, and can-type)
  • Pre-made styles so the app looks pretty 😍
  • A mock service layer

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

The mock service layer provided by the starter code is implemented with can-fixture. The service layer supplies:

  • POST /api/session for creating sessions (log in)
  • GET /api/session for checking 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:

Request:

GET /api/session
Response:

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

If someone is logged out:

Request:

GET /api/session
Response:

STATUS: 404
{message: "No session"}

To log someone in:

Request:

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

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

If someone logs in with invalid credentials:

Request:

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

STATUS: 401 unauthorized
{ message: "Unauthorized"}

To log someone out:

Request:

DELETE /api/session
Response:

STATUS: 200
{}

To create a user:

Request:

POST /api/users

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

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

Setup

The problem

When someone adds <signup-login></signup-login> to their HTML, we want the following HTML to show up:

<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>

What you need to know

To set up a basic CanJS application, you define a custom element in JavaScript and use the custom element in your page’s HTML.

To define a custom element, create a class that extends can-stache-element. Then register your element via customElements.define by calling it with the name of your custom element and the class.

For example, we will use <signup-login> as our custom tag:

class SignupLogin extends StacheElement {}
customElements.define("signup-login", SignupLogin);

But this doesn’t do anything. Components created with can-stache-element add their own HTML through their view property like this:

class SignupLogin extends StacheElement {
  static view = `
    <h2>Sign Up or Log In</h2>
  `   
}

The solution

Update the JavaScript tab to:

import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

fixture("POST /api/session", function(request, response) {
    const userData = localStorage.getItem("user");
    if (userData) {
        const user = JSON.parse(userData);
        const requestUser = request.data.user;
        if (
            user.email === requestUser.email &&
            user.password === requestUser.password
        ) {
            return request.data;
        } else {
            response(401, { message: "Unauthorized" }, {}, "unauthorized");
        }
    }
    response(401, { message: "Unauthorized" }, {}, "unauthorized");
});
fixture("GET /api/session", function(request, response) {
    const session = localStorage.getItem("session");
    if (session) {
        response(JSON.parse(session));
    } else {
        response(404, { message: "No session" }, {}, "unauthorized");
    }
});
fixture("DELETE /api/session", function() {
    localStorage.removeItem("session");
    return {};
});
fixture("POST /api/users", function(request) {
    const session = {
        user: { email: request.data.email }
    };
    localStorage.setItem("user", JSON.stringify(request.data));
    localStorage.setItem("session", JSON.stringify(session));

    return session.user;
});

class SignupLogin extends StacheElement {
    static 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>
    `;
}
customElements.define("signup-login", SignupLogin);

Update the HTML tab to:

<signup-login></signup-login>

Check if the user is logged in

The problem

Let’s make a request to GET /api/session to know if there is an active user 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:

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

What you need to know

  • The props property on a StacheElement class specifies well-defined properties for each element instance via can-observable-object:

    class SignupLogin extends StacheElement {
      static view = `{{ this.myProperty }}`;  
      static props = {
        myProperty: String
      }
    }
    
  • The default property can return the initial value of a property:

    class SignupLogin extends StacheElement {
      static view = `{{ this.myProperty }} <!-- renders “This string” -->`;
      static props = {
        myProperty: {
          get default() {
            return "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.

    {{# if(this.myProperty) }}
      Value is truth-y
    {{ else }}
      Value is false-y
    {{/ if }}
    
  • Promises are observable in can-stache. For the promise myPromise:

    • myPromise.value is the resolved value of the promise
    • myPromise.isPending is true if the promise has not been resolved or rejected
    • 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 JavaScript tab to:

import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

fixture("POST /api/session", function(request, response) {
    const userData = localStorage.getItem("user");
    if (userData) {
        const user = JSON.parse(userData);
        const requestUser = request.data.user;
        if (
            user.email === requestUser.email &&
            user.password === requestUser.password
        ) {
            return request.data;
        } else {
            response(401, { message: "Unauthorized" }, {}, "unauthorized");
        }
    }
    response(401, { message: "Unauthorized" }, {}, "unauthorized");
});
fixture("GET /api/session", function(request, response) {
    const session = localStorage.getItem("session");
    if (session) {
        response(JSON.parse(session));
    } else {
        response(404, { message: "No session" }, {}, "unauthorized");
    }
});
fixture("DELETE /api/session", function() {
    localStorage.removeItem("session");
    return {};
});
fixture("POST /api/users", function(request) {
    const session = {
        user: { email: request.data.email }
    };
    localStorage.setItem("user", JSON.stringify(request.data));
    localStorage.setItem("session", JSON.stringify(session));

    return session.user;
});

class SignupLogin extends StacheElement {
    static view = `
        {{# if(this.sessionPromise.value) }}

            <p class="welcome-message">
                Welcome {{ this.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 }}
    `;

    static props = {
        sessionPromise: {
            get default() {
                return ajax({
                    url: "/api/session"
                });
            }
        }
    };
}
customElements.define("signup-login", SignupLogin);

Signup form

The problem

Let’s 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

  • Component properties defined with can-observable-object allow you specify a type like so:

    static props = {
      name: String,
      password: Number
    }
    
  • The key:to binding can set an input’s value to an element property like:

    <input value:to="this.name" />
    
  • Use on: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="this.doSomething(scope.event)"> ... </div>
    

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

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

  • can-ajax can make POST requests to a URL like:

ajax({
  url: "http://query.yahooapis.com/v1/public/yql",
  type: "post",
  data: {
    format: "json",
    q: 'select * from geo.places where text="sunnyvale, ca"'
  }
}) //-> Promise
  • Use the then() method on a Promise to map the source Promise to another Promise value.

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

The solution

Update the JavaScript tab to:

import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

fixture("POST /api/session", function(request, response) {
    const userData = localStorage.getItem("user");
    if (userData) {
        const user = JSON.parse(userData);
        const requestUser = request.data.user;
        if (
            user.email === requestUser.email &&
            user.password === requestUser.password
        ) {
            return request.data;
        } else {
            response(401, { message: "Unauthorized" }, {}, "unauthorized");
        }
    }
    response(401, { message: "Unauthorized" }, {}, "unauthorized");
});
fixture("GET /api/session", function(request, response) {
    const session = localStorage.getItem("session");
    if (session) {
        response(JSON.parse(session));
    } else {
        response(404, { message: "No session" }, {}, "unauthorized");
    }
});
fixture("DELETE /api/session", function() {
    localStorage.removeItem("session");
    return {};
});
fixture("POST /api/users", function(request) {
    const session = {
        user: { email: request.data.email }
    };
    localStorage.setItem("user", JSON.stringify(request.data));
    localStorage.setItem("session", JSON.stringify(session));

    return session.user;
});

class SignupLogin extends StacheElement {
    static view = `
        {{# if(this.sessionPromise.value) }}

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

        {{ else }}

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

                <input placeholder="email" value:to="this.email" />

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

                <button>Sign Up</button>

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

        {{/ if }}
    `;

    static props = {
        email: String,
        password: String,
        sessionPromise: {
            get default() {
                return ajax({
                    url: "/api/session"
                });
            }
        }
    };

    signUp(event) {
        event.preventDefault();
        this.sessionPromise = ajax({
            url: "/api/users",
            type: "post",
            data: {
                email: this.email,
                password: this.password
            }
        }).then(function(user) {
            return { user: user };
        });
    }
}
customElements.define("signup-login", SignupLogin);

Log out button

The problem

Let’s 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.

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

The solution

Update the JavaScript tab to:

import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

fixture("POST /api/session", function(request, response) {
    const userData = localStorage.getItem("user");
    if (userData) {
        const user = JSON.parse(userData);
        const requestUser = request.data.user;
        if (
            user.email === requestUser.email &&
            user.password === requestUser.password
        ) {
            return request.data;
        } else {
            response(401, { message: "Unauthorized" }, {}, "unauthorized");
        }
    }
    response(401, { message: "Unauthorized" }, {}, "unauthorized");
});
fixture("GET /api/session", function(request, response) {
    const session = localStorage.getItem("session");
    if (session) {
        response(JSON.parse(session));
    } else {
        response(404, { message: "No session" }, {}, "unauthorized");
    }
});
fixture("DELETE /api/session", function() {
    localStorage.removeItem("session");
    return {};
});
fixture("POST /api/users", function(request) {
    const session = {
        user: { email: request.data.email }
    };
    localStorage.setItem("user", JSON.stringify(request.data));
    localStorage.setItem("session", JSON.stringify(session));

    return session.user;
});

class SignupLogin extends StacheElement {
    static view = `
        {{# if(this.sessionPromise.value) }}

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

        {{ else }}

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

                <input placeholder="email" value:to="this.email" />

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

                <button>Sign Up</button>

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

        {{/ if }}
    `;

    static props = {
        email: String,
        password: String,
        sessionPromise: {
            get default() {
                return ajax({
                    url: "/api/session"
                });
            }
        }
    };

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

    logOut() {
        this.sessionPromise = ajax({
            url: "/api/session",
            type: "delete"
        }).then(function() {
            return Promise.reject({ message: "Unauthorized" });
        });
    }
}
customElements.define("signup-login", SignupLogin);

Login form

The problem

Let’s add a "Log In" form for the user:

<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>

The user should be able 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 this.sessionPromise accordingly.

What you need to know

  • Use {{# eq(value1, value2) }} to test equality in can-stache.
    {{# eq(this.value1, "value 2") }}
      Values are equal
    {{ else }}
      Values are not equal
    {{/ if }}
    
  • You can set a property value within an event binding like:
    <a on:click="this.aProperty = 'a value'">Link</a>
    

The solution

Update the JavaScript tab to:

import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

fixture("POST /api/session", function(request, response) {
    const userData = localStorage.getItem("user");
    if (userData) {
        const user = JSON.parse(userData);
        const requestUser = request.data.user;
        if (
            user.email === requestUser.email &&
            user.password === requestUser.password
        ) {
            return request.data;
        } else {
            response(401, { message: "Unauthorized" }, {}, "unauthorized");
        }
    }
    response(401, { message: "Unauthorized" }, {}, "unauthorized");
});
fixture("GET /api/session", function(request, response) {
    const session = localStorage.getItem("session");
    if (session) {
        response(JSON.parse(session));
    } else {
        response(404, { message: "No session" }, {}, "unauthorized");
    }
});
fixture("DELETE /api/session", function() {
    localStorage.removeItem("session");
    return {};
});
fixture("POST /api/users", function(request) {
    const session = {
        user: { email: request.data.email }
    };
    localStorage.setItem("user", JSON.stringify(request.data));
    localStorage.setItem("session", JSON.stringify(session));

    return session.user;
});

class SignupLogin extends StacheElement {
    static view = `
        {{# if(this.sessionPromise.value) }}

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

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

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

                    <input placeholder="email" value:to="this.email" />

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

                    <button>Sign Up</button>

                    <aside>
                        Have an account?
                        <a href="javascript://" on:click="this.page = 'login'">Log in</a>
                    </aside>
                </form>

            {{ else }}

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

                    <input placeholder="email" value:to="this.email" />

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

                    <button>Log In</button>

                    <aside>
                        Don’t have an account?
                        <a href="javascript://" on:click="this.page = 'signup'">Sign up</a>
                    </aside>
                </form>

            {{/ eq }}

        {{/ if }}
    `;

    static props = {
        email: String,
        password: String,
        page: { type: String, default: "login" },
        sessionPromise: {
            get default() {
                return ajax({
                    url: "/api/session"
                });
            }
        }
    };

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

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

    logIn(event) {
        event.preventDefault();
        this.sessionPromise = ajax({
            url: "/api/session",
            type: "post",
            data: {
                user: {
                    email: this.email,
                    password: this.password
                }
            }
        });
    }
}
customElements.define("signup-login", SignupLogin);

Login errors

The problem

If the user tried to login, but the server responded with an error message, let’s display that error message.

<div class="error">An error message</div>

We’ll do this by catching the create-session request. If the request fails, 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:

    const source = Promise.reject({message: "foo"})
    source.catch(function(reason) {
        reason //-> {message: "foo"}
    });
    
  • Use the Any type to define a property of indeterminate type:

    class AppModel extends ObservableObject {
      static props = {
        myProperty: type.Any
      };    
    };
    const appModel = new AppModel({});
    appModel.myProperty = ANYTHING;
    

The solution

Update the JavaScript tab to:

import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

fixture("POST /api/session", function(request, response) {
    const userData = localStorage.getItem("user");
    if (userData) {
        const user = JSON.parse(userData);
        const requestUser = request.data.user;
        if (
            user.email === requestUser.email &&
            user.password === requestUser.password
        ) {
            return request.data;
        } else {
            response(401, { message: "Unauthorized" }, {}, "unauthorized");
        }
    }
    response(401, { message: "Unauthorized" }, {}, "unauthorized");
});
fixture("GET /api/session", function(request, response) {
    const session = localStorage.getItem("session");
    if (session) {
        response(JSON.parse(session));
    } else {
        response(404, { message: "No session" }, {}, "unauthorized");
    }
});
fixture("DELETE /api/session", function() {
    localStorage.removeItem("session");
    return {};
});
fixture("POST /api/users", function(request) {
    const session = {
        user: { email: request.data.email }
    };
    localStorage.setItem("user", JSON.stringify(request.data));
    localStorage.setItem("session", JSON.stringify(session));

    return session.user;
});

class SignupLogin extends StacheElement {
    static view = `
      {{# if(this.sessionPromise.value) }}

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

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

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

                  <input placeholder="email" value:to="this.email" />

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

                  <button>Sign Up</button>

                  <aside>
                      Have an account?
                      <a href="javascript://" on:click="this.page = 'login'">Log in</a>
                  </aside>
              </form>

          {{ else }}

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

                  <input placeholder="email" value:to="this.email" />

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

                  <button>Log In</button>

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

                  <aside>
                      Don’t have an account?
                      <a href="javascript://" on:click="this.page = 'signup'">Sign up</a>
                  </aside>
              </form>

          {{/ eq }}
      {{/ if }}
    `;

    static props = {
        email: String,
        password: String,
        page: { type: String, default: "login" },
        logInError: type.Any,
        sessionPromise: {
            get default() {
                return ajax({
                    url: "/api/session"
                });
            }
        }
    };

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

    logIn(event) {
        event.preventDefault();
        this.sessionPromise = ajax({
            url: "/api/session",
            type: "post",
            data: {
                user: {
                    email: this.email,
                    password: this.password
                }
            }
        });

        this.logInError = null;
        this.sessionPromise.catch(
            function(error) {
                this.logInError = error;
            }.bind(this)
        );
    }

    logOut() {
        this.sessionPromise = ajax({
            url: "/api/session",
            type: "delete"
        }).then(function() {
            return Promise.reject({ message: "Unauthorized" });
        });
    }
}
customElements.define("signup-login", SignupLogin);

Result

When finished, you should have something like the following code:

<signup-login></signup-login>

<script type="module">
import { ajax, fixture, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

fixture("POST /api/session", function(request, response) {
    const userData = localStorage.getItem("user");
    if (userData) {
        const user = JSON.parse(userData);
        const requestUser = request.data.user;
        if (
            user.email === requestUser.email &&
            user.password === requestUser.password
        ) {
            return request.data;
        } else {
            response(401, { message: "Unauthorized" }, {}, "unauthorized");
        }
    }
    response(401, { message: "Unauthorized" }, {}, "unauthorized");
});
fixture("GET /api/session", function(request, response) {
    const session = localStorage.getItem("session");
    if (session) {
        response(JSON.parse(session));
    } else {
        response(404, { message: "No session" }, {}, "unauthorized");
    }
});
fixture("DELETE /api/session", function() {
    localStorage.removeItem("session");
    return {};
});
fixture("POST /api/users", function(request) {
    const session = {
        user: { email: request.data.email }
    };
    localStorage.setItem("user", JSON.stringify(request.data));
    localStorage.setItem("session", JSON.stringify(session));

    return session.user;
});

class SignupLogin extends StacheElement {
    static view = `
      {{# if(this.sessionPromise.value) }}

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

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

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

                  <input placeholder="email" value:to="this.email" />

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

                  <button>Sign Up</button>

                  <aside>
                      Have an account?
                      <a href="javascript://" on:click="this.page = 'login'">Log in</a>
                  </aside>
              </form>

          {{ else }}

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

                  <input placeholder="email" value:to="this.email" />

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

                  <button>Log In</button>

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

                  <aside>
                      Don’t have an account?
                      <a href="javascript://" on:click="this.page = 'signup'">Sign up</a>
                  </aside>
              </form>

          {{/ eq }}
      {{/ if }}
    `;

    static props = {
        email: String,
        password: String,
        page: { type: String, default: "login" },
        logInError: type.Any,
        sessionPromise: {
            get default() {
                return ajax({
                    url: "/api/session"
                });
            }
        }
    };

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

    logIn(event) {
        event.preventDefault();
        this.sessionPromise = ajax({
            url: "/api/session",
            type: "post",
            data: {
                user: {
                    email: this.email,
                    password: this.password
                }
            }
        });

        this.logInError = null;
        this.sessionPromise.catch(
            function(error) {
                this.logInError = error;
            }.bind(this)
        );
    }

    logOut() {
        this.sessionPromise = ajax({
            url: "/api/session",
            type: "delete"
        }).then(function() {
            return Promise.reject({ message: "Unauthorized" });
        });
    }
}
customElements.define("signup-login", SignupLogin);
</script>

<style type="text/less">
  @font-family: 'Raleway', "Helvetica Neue", Arial, sans-serif;
  @font-size: 1em;
  @color-dark: #54599c;
  @color-light: #fff;
  @color-light-gray: #d3d3d3;
  @color-light-blue: #e2f5ff;
  @color-error: #ff000e;
  @color-error-light: #fde5ec;
  @link-color: #2196F3;

  body,
  input,
  button {
    font-family: @font-family;
    font-size: @font-size;
  }

  body {
    background-color: @color-dark;
    padding: 5%;
  }

  form {
    background-color: @color-light;
    padding: 30px 40px 0 40px;
    border-radius: 6px;

  }

  input {
    border: 1px solid @color-light-gray;
    border-radius: 4px;
    width: 93%;
    padding: 3%;
    margin-bottom: 20px;
    &:focus {
      background-color: @color-light-blue;
      outline: 0;
      border-color: #a1c7e8;
    }
  }

  button {
    background-color: #3ec967;
    text-transform: uppercase;
    letter-spacing: 2px;
    border-radius: 20px;
    border: 0;
    color: White;
    padding: 10px;
    width:100%;
  }

  a {
    color: @link-color;
  }

  h2 {
    color: #b027a1;
    text-align: center;
    font-size: 2em;
  }

  aside {
    background-color: #f1f0ff;
    margin: 40px -40px;
    padding: 15px;
    border-radius: 0px 0px 6px 6px;
    text-align: center;
    color: @color-dark;
  }

  .welcome-message {
    color: white;
    text-align: center;
  }

  .error {
    padding: 20px;
    margin-top: 20px;
    text-align: center;
    color: @color-error;
    background-color: @color-error-light;
  }
</style>

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

On this page

Get help

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