This is the second part of our tutorial series about creating the RealWorld App with OWL (Odoo Web Library). In this part, we will implement the authentication layer with OWL Store, Actions, and getters. The OWL Store will help us and handle the global state of the application when the user is logged in or not, and what menu and actions should be available.

The app adheres to the spec of the RealWorld example apps repository on GitHub, and our current implementation in OWL is available here:

Coding-Dodo/owl-realworld-app
OWL RealWorld App Implementation. Contribute to Coding-Dodo/owl-realworld-app development by creating an account on GitHub.

But, to continue directly where we left off last time, you should clone this branch:

git clone -b feature/basic-pages-structure-routing https://github.com/Coding-Dodo/owl-realworld-app.git

Or continue with your own project,

The problem of keeping the coherent state of an Application.

As our application grows and grows, we will have more and more components that need to communicate and share data between themselves.

For example, in our RealWorld App:

  • The Navbar has to know if the user is logged in to show or hide the Login/Register/Settings links. It also shows the current Username if the user is connected.
  • The Homepage has to know if the user is logged in to make the personalized "Feed" of the article available or not.
  • Each article inside a list has to display the "Like" (heart button) if the user is logged in.

Passing props from Parent to Child works in a lot of cases but it can make the application complicated very fast and may sometimes not be possible at all.

         App
        /   \
 RouterComp  Navbar
      / \
Homepage Settings
    /
ArticlesList
   /
Article

In this example, handling the "user is logged in" info inside the App and passing it as props down the children's chain will be tedious. Article Component also has to know the user state and on the contrary, ArticlesList maybe doesn't need to know!

So it would force us to pass the user logged-in info as props to ArticlesList (that doesn't need it) to pass it again to Article (that needs it).

TLDR; Some data may need to be stored in a global state of the app and accessible from any components.

Creating and Using the OWL Store

OWL Store presentation

OWL gives us the possibility to use a centralized store, a class that owns data that will represent the state of the application.

This store is updated via actions, and OWL Components can connect to the store via a special hook to read that data.

The Components can also subscribe to the actions and dispatch them so they update the store. Without actions the store cannot be updated directly (assigning new values from within a Component), it is the job of the actions.

Store instantiation takes an Object as the argument:

const config = {
  state, // Initial State of the App
  actions, // Actions are functions that update the state
  getters, // Needed to transform raw data from state
  env, // The current env
};
const store = new Store(config);

Creating and connecting the store to our (root) App Component.

Let's instantiate a Store in our main.js that we will attach to our App. Our store will only have one Object called user.

We will have to actions functions for now:

  • login action that will store the resulting user (from an API POST call done elsewhere) inside the store.
  • logout cleanup our store and

We will define our User Object in the store to be similar to the User return by the API of "RealWorld app":

"user": {
    "email": "[email protected]",
    "token": "jwt.token.here",
    "username": "jake",
    "bio": "I work at statefarm",
    "image": null
}

We could directly make API calls inside our Store actions but it is important to not bind our Store logic to the underlying API. So our login and logout store actions will only store a user in the global state of the application and will not be responsible to authenticate against an external API.

We will also create a getter for the state called userLoggedIn that will return a boolean by checking if the user Object is present and filled with data. According to the specs seen earlier if the user object has the "token" property then we will say that it is correct and logged in. The other getter is getUser that will just return the current store user.

const actions = {
  logout({ state }) {
    state.user = {};
  },
  login({ state }, user) {
    state.user = user;
  },
};
const initialState = {
  user: {},
};
const getters = {
  userLoggedIn({ state }) {
    if (state.user && state.user.token) {
      return true;
    }
    return false;
  },
  getUser({ state }) {
    return state.user;
  },
};
./src/main.js

Finally, the initial state of the Application is an empty user Object.

Then, still inside ./src/main.js, we will create a function that will take all of these, create the Store and attach it to the env of the App.

// on top of the file main.js update the imports 
// to import the Store 
import { utils, router, mount, QWeb, Store } from "@odoo/owl";
// ...
// ...
async function makeEnvironment(store) {
  const env = { qweb: new QWeb(), store: store };
  env.router = new router.Router(env, ROUTES, { mode: "hash" });
  await env.router.start();
  return env;
}

function makeStore() {
  const store = new Store({ initialState, actions, getters });
  return store;
}

async function setup() {
  let store = makeStore();
  App.env = await makeEnvironment(store);
  mount(App, { target: document.body });
}

Notice that we also changed the makeEnvironment function to take the store as a parameter and put it inside the env of our App.

Now that our Store is set up we will use its state and actions inside our components.

Now that we have our Store and our getters functions we will update our  Navbar Component to make use of that, and render its links conditionally.

// Adding hooks import from owl library
import { Component, tags, router, hooks } from "@odoo/owl";
// importing the useGetters hook
const { useGetters } = hooks;
const Link = router.Link;
import { NavbarLink } from "./NavbarLink";

const NAVBAR_TEMPLATE = tags.xml/*xml*/ `
<nav class="navbar navbar-light">
    <div class="container">
        <Link to="'HOME'" class="navbar-brand">conduit</Link>
        <ul class="nav navbar-nav pull-xs-right">
            <li class="nav-item">
                <NavbarLink to="'HOME'" class="nav-link">Home</NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'EDITOR'" class="nav-link nav-link-editor" t-if="getters.userLoggedIn()">
                    <i class="ion-compose"></i> New Post
                </NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'SETTINGS'" class="nav-link" t-if="getters.userLoggedIn()">
                    <i class="ion-gear-a"></i> Settings
                </NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'LOG_IN'" class="nav-link" t-if="!getters.userLoggedIn()">
                    Sign in
                </NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'REGISTER'" class="nav-link" t-if="!getters.userLoggedIn()">
                    Sign up
                </NavbarLink>
            </li>
            <li class="nav-item" t-if="getters.userLoggedIn()">
                <NavbarLink to="'PROFILE'" class="nav-link">
                    <t t-esc="getters.getUser().username"/>
                </NavbarLink>
            </li>
        </ul>
    </div>
</nav>
`;
export class Navbar extends Component {
  static template = NAVBAR_TEMPLATE;
  static components = { Link, NavbarLink };
  // registering the getters
  getters = useGetters();
}
./src/components/Navbar.js

Since we registered the getters = useGetters we can use the getters functions directly inside our template in conditionals and other expressions

<li class="nav-item" t-if="getters.userLoggedIn()">
    <NavbarLink to="'PROFILE'" class="nav-link">
        <t t-esc="getters.getUser().username"/>
    </NavbarLink>
</li>

For this example, the last link shows the username and navigate to the profile page if the user is logged in. So we surround the link with a conditional and then use the getUser() getter to have access to the Store user and display its username.

Dispatching Store action to Login and Logout.

Loggin in from the LogIn page

From the "LogIn page", we will call the Store action we created earlier login. For now, we will not implement Form logic yet and just dispatch the login action when the user clicks on the "login" button to test if our application state management works.

// import hooks from owl
import { Component, tags, router, hooks } from "@odoo/owl";
// import useDispatch hook to access actions
const { useDispatch } = hooks;
const Link = router.Link;
const { xml } = tags;

const LOG_IN_TEMPLATE = xml/* xml */ `
<div class="auth-page">
  <div class="container page">
    <div class="row">

      <div class="col-md-6 offset-md-3 col-xs-12">
        <h1 class="text-xs-center">Sign in</h1>
        <p class="text-xs-center">
            <Link to="'REGISTER'">Need an account?</Link>
        </p>

        <ul class="error-messages">
          <li>Invalid credentials</li>
        </ul>

        <form>
          <fieldset class="form-group">
            <input class="form-control form-control-lg" type="text" placeholder="Email"/>
          </fieldset>
          <fieldset class="form-group">
            <input class="form-control form-control-lg" type="password" placeholder="Password"/>
          </fieldset>
          <button class="btn btn-lg btn-primary pull-xs-right" t-on-click="login">
            Sign In
          </button>
        </form>
      </div>

    </div>
  </div>
</div>
`;
export class LogIn extends Component {
  static components = { Link };
  static template = LOG_IN_TEMPLATE;
  dispatch = useDispatch();

  login(ev) {
    ev.preventDefault();
    this.dispatch("login", {
      email: "[email protected]",
      token: "jwt.token.here",
      username: "CodingDodo",
      bio: "I am a Coding Dodo",
      image: null,
    });
    this.env.router.navigate({ to: "HOME" });
  }
}
./src/pages/LogIn.js

After importing the correct dependencies and hook we declare that our LogIn Component should have a property dispatch = useDispatch.

On the login button, we bind the click event to an internal function of the component called login:

<button class="btn btn-lg btn-primary pull-xs-right" t-on-click="login">
    Sign In
</button>

This function is implemented inside the component and just dispatch the Store actions login with a dummy user object for now:

  login(ev) {
    ev.preventDefault();
    this.dispatch("login", {
      email: "[email protected]",
      token: "jwt.token.here",
      username: "CodingDodo",
      bio: "I am a Coding Dodo",
      image: null,
    });
    // redirect to the homepage:
    this.env.router.navigate({ to: "HOME" });
  }

We have access to ev that represents the event of clicking and we use preventDefault so it doesn't trigger the original browser page refresh. After we have dispatched the action we redirect to the home page programmatically via the router function navigate accessible through the this.env.router (accessible in all Components env).

Logging out from the Settings page

From the Settings page, there is a button to Log out, and we will do the same as on the Login page: dispatch a Store action with the click of the button:

import { Component, tags, hooks } from "@odoo/owl";
const { useDispatch } = hooks;
const { xml } = tags;

const SETTINGS_TEMPLATE = xml/* xml */ `
<div class="settings-page">
  <div class="container page">
    <div class="row">

      <div class="col-md-6 offset-md-3 col-xs-12">
        <h1 class="text-xs-center">Your Settings</h1>
        <form>
          <fieldset>
              <fieldset class="form-group">
                <input class="form-control" type="text" placeholder="URL of profile picture"/>
              </fieldset>
              <fieldset class="form-group">
                <input class="form-control form-control-lg" type="text" placeholder="Your Name"/>
              </fieldset>
              <fieldset class="form-group">
                <textarea class="form-control form-control-lg" rows="8" placeholder="Short bio about you"></textarea>
              </fieldset>
              <fieldset class="form-group">
                <input class="form-control form-control-lg" type="text" placeholder="Email"/>
              </fieldset>
              <fieldset class="form-group">
                <input class="form-control form-control-lg" type="password" placeholder="Password"/>
              </fieldset>
              <button class="btn btn-lg btn-primary pull-xs-right">
                Update Settings
              </button>
          </fieldset>
        </form>
        <hr/>
        <button class="btn btn-outline-danger" t-on-click="logout">Or click here to logout.</button>
      </div>

    </div>
  </div>
</div>
`;

export class Settings extends Component {
  static template = SETTINGS_TEMPLATE;
  dispatch = useDispatch();

  logout(ev) {
    ev.preventDefault();
    this.dispatch("logout");
    this.env.router.navigate({ to: "HOME" });
  }
}
./src/pages/Settings.js

Now we can test our Application http://localhost:8080/#/ and see that the Application test is handled correctly. Clicking on the "login" button changes the Navbar and with "logged in" menus actions, and show username.

Going into the Settings page and clicking Logout also change the state of the application correctly.

But there is a problem. Every time we refresh the page we lose the state of our application.

Using LocalStorage to keep the state of the application after refresh.

To keep the state of the application we will use localStorage to keep our user as data inside the browser. localStorage is natively present in JavaScript and will do fine for keeping our application state between refresh.

Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Coding Dodo - Odoo, Python & JavaScript Tutorials.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.