Table of Contents

In this series, we will create the famous "RealWorld App" from scratch. With OWL Framework (Odoo Web Library) πŸ¦‰ as the FrontEnd of choice.

If you are interested in using:

What is the RealWorld Example App?

The RealWorld App is a Medium.com clone called Conduit built with several technologies on the FrontEnd and BackEnd.

The final result of this 4 parts tutorial series can be seen here it is hosted on Netlify.

The RealWorld App repository is a set of specs describing this "Conduit" app, how to create it on the front-end, and on the back-end:

RealWorld Example App Logo
gothinkster/realworld
β€œThe mother of all demo apps” β€” Exemplary fullstack Medium.com clone powered by React, Angular, Node, Django, and many more πŸ… - gothinkster/realworld

In our Tutorial, we will implement the front-end part. Following the FRONTEND Instructions specs defined here, we will use the brand new OWL (Odoo Web Library) as the technology choice. This is a SPA with calls to an external API, so it will be a good starting point to see a lot of what the Framework has to offer in terms of state management, routing, and reactivity.

Styles and HTML templates are available in the repository and the routing structure of the client-side is described like that:

  • Home page (URL: /#/ )
    • List of tags
    • List of articles pulled from either Feed, Global, or by Tag
    • Pagination for list of articles
  • Sign in/Sign up pages (URL: /#/login, /#/register )
    • Uses JWT (store the token in localStorage)
    • Authentication can be easily switched to session/cookie-based
  • Settings page (URL: /#/settings )
  • Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here )
  • Article page (URL: /#/article/article-slug-here )
    • Delete article button (only shown to article's author)
    • Render markdown from server client-side
    • The comments section at bottom of the page
    • Delete comment button (only shown to comment's author)
  • Profile page (URL: /#/profile/:username, /#/profile/:username/favorites )
    • Show basic user info
    • List of articles populated from author's created articles or author's favorited articles

Introducing OWL Framework(Odoo Web Library)

OWL is a new open-source Framework created internally at Odoo with the goal to be used as a replacement to the current old client-side technology used by Odoo. According to the repository description:

The Odoo Web Library (OWL) is a relatively small UI framework intended to be the basis for the Odoo Web Client in future versions (>15). Owl is a modern framework, written in Typescript, taking the best ideas from React and Vue in a simple and consistent way.

The Framework offers a declarative component system, reactivity with hooks (See React inspiration), a Store (mix between Vue and React implementation), and a front-end router.

The documentation is not exhaustive for now, but we will try to make sense of everything via use-cases.

Components

Components are JavaScript classes with properties, functions and the ability to render themselves (Insert or Update themselves into the HTML Dom). Each Component has a template that represents its final HTML structure, with composition, we can call other components with their tag name inside our Component.

class MagicButton extends Component {
  static template = xml`
    <button t-on-click="changeText">
      Click Me! [<t t-esc="state.value"/>]
    </button>`;

  state = { value: 0 };

  changeText() {
    this.state.value = "This is Magic";
    this.render();
  }
}

The templating system is in XML QWeb, which should be familiar if you are an Odoo Developer. t-on-click allow us to listen to the click event on the button and trigger a function defined inside the Component called changeText.

Properties of the Component live inside the state property, it is an object that has all the keys/value we need. This state is isolated and only lives inside that Component, it is not shared with other Components (even if they are copies of that one).

Inside that changeText function we change the state.value to update the text, then we call render to force the update of the Component display: the Button shown in the Browser now has the text "Click Me! This is Magic".

Hooks and reactivity

It is not very convenient to use render function all the time and to handle reactivity better, OWL uses a system its system of hooks, specifically the useState hook.

const { useState } = owl.hooks;

class MagicButton extends Component {
  static template = xml`
    <button t-on-click="changeText">
      Click Me! [<t t-esc="state.value"/>]
    </button>`;

  state = useState({ value: 0 });

  changeText() {
    this.state.value = "This is Magic";
  }
}

As we can see, we don't have to call the render function anymore. Using the useState hook actually tells the OWL Observer to watch for change inside the state via the native Proxy Object.

Passing data from Parent to Child via props

We saw that a Component can have multiple Components inside itself. With this Parent/Child hierarchy, data can be passed via props. For example, if we wanted the initial text "Click me" of our MagicButton to be dynamic and chosen from the Parent we can modify it like that

const { useState } = owl.hooks;

class MagicButton extends Component {
  static template = xml`
    <button t-on-click="changeText">
      <t t-esc="props.initialText"/> [<t t-esc="state.value"/>]
    </button>`;

  state = useState({ value: 0 });

  changeText() {
    this.state.value = "This is Magic";
  }
}

// And then inside a parent Component
class Parent extends Component {
  static template = xml`
<div>
    <MagicButton initialText="Dont click me!"/>
</div>`;
  static components = { MagicButton };

And that's it for a quick overview of the Framework, we will dive into other features via examples. From now on it's better if you follow along with your own repository so we create the RealWorld App together!

Starting our project

Prerequisites

Make sure that you have NodeJS installed. I use NVM (Node Version Manager) to handle different NodeJS versions on my system.

Follow the NVM install instructions here or install directly the following NodeJS version on your system.

For this tutorial, as of 26 September 2021, I'm using NodeJS stable version v14.17.6

β–Ά nvm list
       v10.22.0
       v10.24.0
        v14.7.0
       v14.15.1
->     v14.17.6
default -> stable (-> v14.17.6)
node -> stable (-> v14.17.6) (default)
stable -> 14.17 (-> v14.17.6) (default)

Using the OWL starter template

To make things a little easier, I've created a template project with Rollup as the bundling system to help us begin with modern JavaScript convention and bundling systems.

Coding-Dodo/OWL-JavaScript-Project-Starter
OWL JavaScript Project Starter with Rollup. Contribute to Coding-Dodo/OWL-JavaScript-Project-Starter development by creating an account on GitHub.

This is a template repo, so click on "Use this template" to create your own repo based on this one (You can also clone it like other repositories).

After pulling the repository we have this file structure:

β”œβ”€β”€ README.md
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
β”œβ”€β”€ public
β”‚Β Β  └── index.html
β”œβ”€β”€ rollup.config.js
β”œβ”€β”€ src
β”‚Β Β  β”œβ”€β”€ App.js
β”‚Β Β  β”œβ”€β”€ components
β”‚Β Β  β”‚Β Β  └── MyComponent.js
β”‚Β Β  └── main.js
└── tests
    β”œβ”€β”€ components
    β”‚Β Β  └── App.test.js
    └── helpers.js

Index.html is a basic HTML file containing minimum info, we will use the <head> tag to insert the Stylesheet given by the RealWorld app later.

The core of our app lives in the src folder, for now, it only contains 2 files. main.js is the entry point:

import { App } from "./app";
import { utils } from "@odoo/owl";

(async () => {
  const app = new App();
  await utils.whenReady();
  await app.mount(document.body);
})();

In this file, we import our main App Component, that we mount on the <body>tag of our index.html file.

Owl components are defined with ES6 (JavaScript - EcmaScript 20015) classes, they use QWeb templates, a virtual DOM to handle reactivity, and asynchronous rendering. Knowing that we simply instantiate our App object.

As its name may suggest utils package contains various utilities, here we use whenReady that tells us when the DOM is totally loaded so we can attach our component to the body.

App Component

The App Class Component represents our application, it will inject all other Components.

import { Component, tags } from "@odoo/owl";
import { MyComponent } from "./components/MyComponent";

const APP_TEMPLATE = tags.xml/*xml*/ `
<main t-name="App" class="" t-on-click="update">
  <MyComponent/>
</main>
`;

export class App extends Component {
  static template = APP_TEMPLATE;
  static components = { MyComponent };
}

MyComponent is a basic Component representing a span, when you click on it the text change. It's only here as an example and we will delete it later.

Installing dependencies and running the dev server.

First, we need to install the dependencies

cd OWL-JavaScript-Project-Starter
npm install

Then, to run the tests

npm run test

And finally, to run the development server

npm run dev

The output should be:

rollup v2.57.0
bundles src/main.js β†’ dist/bundle.js...
babelHelpers: 'bundled' option was used by default. It is recommended to configure this option explicitly, read more here: https://github.com/rollup/plugins/tree/master/packages/babel#babelhelpers
http://localhost:8080 -> /Users/codingdodo/Code/owl-realworld-app/dist
http://localhost:8080 -> /Users/codingdodo/Code/owl-realworld-app/public
LiveReload enabled
created dist/bundle.js in 1.5s
OWL Hello World App is running on localhost:8080
OWL Hello World App is running on localhost:8080

If you would prefer to run the server on a different port you have to edit rollup.config.js and search for the serve section

serve({
    open: false,
    verbose: true,
    contentBase: ["dist", "public"],
    host: "localhost",
    port: 8080, // Change Port here
}),

Importing styles from RealWorld App resources kit.

We will update Β public/index.html to include <link> to assets given by RealWorld App repository instructions. These assets include the font, the icons, and the CSS:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>RealWorld App in OWL</title>
    <!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
    <link
      href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"
      rel="stylesheet"
      type="text/css"
    />
    <!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
    <link rel="stylesheet" href="https://demo.productionready.io/main.css" />
    <script type="module" src="bundle.js"></script>
  </head>
  <body></body>
</html>

Navigating to http://localhost:8080/ should already show you the change of fonts.

Implementing the elements of the layout as Components.

The Conduit App has a classic design layout, composed of a Navbar Header, Content, and Footer.

For now, we will implement the Homepage and the different elements of the Layout as simple HTML content ("dumb" Components, with no logic).

Creating the Navbar Component

Inside src/components/ we will create a new file named Navbar.js

import { Component, tags } from "@odoo/owl";

const NAVBAR_TEMPLATE = tags.xml/*xml*/ `
<nav class="navbar navbar-light">
    <div class="container">
        <a class="navbar-brand" href="index.html">conduit</a>
        <ul class="nav navbar-nav pull-xs-right">
            <li class="nav-item">
                <!-- Add "active" class when you're on that page" -->
                <a class="nav-link active" href="">Home</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="">
                    <i class="ion-compose"></i> New Post
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="">
                    <i class="ion-gear-a"></i> Settings
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="">Sign in</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="">Sign up</a>
            </li>
        </ul>
    </div>
</nav>
`;
export class Navbar extends Component {
  static template = NAVBAR_TEMPLATE;
}

The template is defined as a const NAVBAR_TEMPLATE then added as a static property to our Navbar Component declaration.

The content of the template is surrounded by tags.xml/*xml*/. These xmlcomments are used so TextEditor extensions that handle Comment tagged templates can be used to have syntax highlight inside our components. For VisualStudio Code the plugin is here.

For the XML content itself, it is just copy-pasted from the instructions on the RealWorld Repo. We will not implement Navigation just yet.

Inside src/components/ we will create a new file named Footer.js

import { Component, tags } from "@odoo/owl";

const FOOTER_TEMPLATE = tags.xml/*xml*/ `
<footer>
    <div class="container">
        <a href="/" class="logo-font">conduit</a>
        <span class="attribution">
            An interactive learning project from <a href="https://thinkster.io">Thinkster</a>. Code &amp; design licensed under MIT.
        </span>
    </div>
</footer>
`;
export class Footer extends Component {
  static template = FOOTER_TEMPLATE;
}

Creating the Home Page Component

This component will hold the content of the Home page.

In this tutorial, we will create a new folder src/pages/ that will hold our "pages" Components. This is an architecture decision that you don't have to follow, but as the number of components will start to grow we would ultimately want to do some cleaning to keep things organized.

With the folder created, inside src/pages/, we will create a new file named Home.js, (full structure):

import { Component, tags, useState } from "@odoo/owl";

const HOME_TEMPLATE = tags.xml/*xml*/ `
<div class="home-page">

    <div class="banner" t-on-click="update">
        <div class="container">
            <h1 class="logo-font">conduit</h1>
            <p><t t-esc="state.text"/></p>
        </div>
    </div>

    <div class="container page">
        <div class="row">
            <div class="col-md-9">
                <div class="feed-toggle">
                    <ul class="nav nav-pills outline-active">
                        <li class="nav-item">
                            <a class="nav-link disabled" href="">Your Feed</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link active" href="">Global Feed</a>
                        </li>
                    </ul>
                </div>

                <div class="article-preview">
                    <div class="article-meta">
                        <a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
                        <div class="info">
                            <a href="" class="author">Eric Simons</a>
                            <span class="date">January 20th</span>
                        </div>
                        <button class="btn btn-outline-primary btn-sm pull-xs-right">
                            <i class="ion-heart"></i> 29
                        </button>
                    </div>
                    <a href="" class="preview-link">
                        <h1>How to build webapps that scale</h1>
                        <p>This is the description for the post.</p>
                        <span>Read more...</span>
                    </a>
                </div>
                <div class="article-preview">
                    <div class="article-meta">
                    <a href="profile.html"><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a>
                    <div class="info">
                        <a href="" class="author">Albert Pai</a>
                        <span class="date">January 20th</span>
                    </div>
                    <button class="btn btn-outline-primary btn-sm pull-xs-right">
                        <i class="ion-heart"></i> 32
                    </button>
                    </div>
                    <a href="" class="preview-link">
                    <h1>The song you won't ever stop singing. No matter how hard you try.</h1>
                    <p>This is the description for the post.</p>
                    <span>Read more...</span>
                    </a>
                </div>
            </div>

            <div class="col-md-3">
                <div class="sidebar">
                    <p>Popular Tags</p>

                    <div class="tag-list">
                        <a href="" class="tag-pill tag-default">programming</a>
                        <a href="" class="tag-pill tag-default">javascript</a>
                        <a href="" class="tag-pill tag-default">emberjs</a>
                        <a href="" class="tag-pill tag-default">angularjs</a>
                        <a href="" class="tag-pill tag-default">react</a>
                        <a href="" class="tag-pill tag-default">mean</a>
                        <a href="" class="tag-pill tag-default">node</a>
                        <a href="" class="tag-pill tag-default">rails</a>
                    </div>
                </div>
            </div>
        </div>
    </div>

</div>

`;
export class Home extends Component {
  static template = HOME_TEMPLATE;
  state = useState({ text: "A place to share your knowledge." });
  updateBanner() {
    this.state.text =
      this.state.text === "A place to share your knowledge."
        ? "An OWL (Odoo Web Library) RealWorld App"
        : "A place to share your knowledge.";
  }
}

Since we will delete ./components/MyComponent we will inject some logic inside this Home Component to test if the framework reactivity is working.

We registered a click event on the banner to fire the updateBanner function:

<div class="banner" t-on-click="update">
    <div class="container">
        <h1 class="logo-font">conduit</h1>
        <p><t t-esc="state.text"/></p>
    </div>
</div>

Inside the Component definition, we created the updateBanner function:

  updateBanner() {
    this.state.text =
      this.state.text === "A place to share your knowledge."
        ? "An OWL (Odoo Web Library) RealWorld App"
        : "A place to share your knowledge.";
  }

So every time the user clicks on the banner, the message will change.

Injecting our components into the main App Component

Now we need to make use of these fine Components. To do so, open the src/components/App.js file and use these Components.

import { Component, tags } from "@odoo/owl";
import { Navbar } from "./components/Navbar";
import { Footer } from "./components/Footer";
import { Home } from "./pages/Home";

const APP_TEMPLATE = tags.xml/*xml*/ `
<main>
  <Navbar/>
  <Home/>
  <Footer/>
</main>
`;

export class App extends Component {
  static components = { Navbar, Footer, Home };
  static template = APP_TEMPLATE;
}

First, we imported the different components/pages like import { Navbar } from "./Navbar";, etc... We use destructuring to get Navbar as a class from the file it is exported and the path of the file is relative (same folder) with the use of ./.

Inside the class App, we filled the static property components to "register" what components App will need to render itself.

Finally, in the XML template, we called these Components as if they were HTML elements with the same name as the ones defined in the static components property.

Our App template now reflects what the basic layout of the website is:

<main>
  <Navbar/>
  <Home/>
  <Footer/>
</main>

Update the tests to check that everything is working correctly.

Inside the ./tests/components/App.test.js we will update the logic to test the reactivity of our Home Component and the presence of Navbar and Footer.

describe("App", () => {
  test("Works as expected...", async () => {
    await mount(App, { target: fixture });
    expect(fixture.innerHTML).toContain("nav");
    expect(fixture.innerHTML).toContain("footer");
    expect(fixture.innerHTML).toContain("A place to share your knowledge.");
    click(fixture, "div.banner");
    await nextTick();
    expect(fixture.innerHTML).toContain(
      "An OWL (Odoo Web Library) RealWorld App"
    );
  });
});

Run the tests with the command:

npm run test

The tests should pass

> jest
 PASS  tests/components/App.test.js

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.628 s
Ran all test suites.

Implementing the different Pages Components of the App.

We will create each of the pages corresponding to the specs as components. There is the HomePage, Settings, LogIn, Register, Editor (New article), and Profile pages.

Settings Page

import { Component, tags, hooks } from "@odoo/owl";
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">Or click here to logout.</button>
      </div>

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

export class Settings extends Component {
  static template = SETTINGS_TEMPLATE;
}
./src/pages/Settings.js

LogIn Page

import { Component, tags } from "@odoo/owl";
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">
          <a href="#register">Need an account?</a>
        </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">
            Sign In
          </button>
        </form>
      </div>

    </div>
  </div>
</div>
`;
export class LogIn extends Component {
  static template = LOG_IN_TEMPLATE;
}
./src/pages/LogIn.js

Register Page

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

const REGISTER_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 up</h1>
        <p class="text-xs-center">
          <a href="#login">Have an account?</a>
        </p>

        <ul class="error-messages">
          <li>That email is already taken</li>
        </ul>

        <form>
          <fieldset class="form-group">
            <input class="form-control form-control-lg" type="text" placeholder="Your Name"/>
          </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">
            Sign up
          </button>
        </form>
      </div>

    </div>
  </div>
</div>
`;
export class Register extends Component {
  static template = REGISTER_TEMPLATE;
}
./src/pages/Register.js

Profile Page

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

const PROFILE_TEMPLATE = xml/* xml */ `
<div class="profile-page">
    <div class="user-info">
        <div class="container">
            <div class="row">

            <div class="col-xs-12 col-md-10 offset-md-1">
                <img src="http://i.imgur.com/Qr71crq.jpg" class="user-img" />
                <h4>Eric Simons</h4>
                <p>
                Cofounder @GoThinkster, lived in Aol's HQ for a few months, kinda looks like Peeta from the Hunger Games
                </p>
                <button class="btn btn-sm btn-outline-secondary action-btn">
                <i class="ion-plus-round"></i> Follow Eric Simons 
                </button>
            </div>

            </div>
        </div>
    </div>

    <div class="container">
    <div class="row">

        <div class="col-xs-12 col-md-10 offset-md-1">
        <div class="articles-toggle">
            <ul class="nav nav-pills outline-active">
            <li class="nav-item">
                <a class="nav-link active" href="">My Articles</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="">Favorited Articles</a>
            </li>
            </ul>
        </div>

        <div class="article-preview">
            <div class="article-meta">
            <a href=""><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
            <div class="info">
                <a href="" class="author">Eric Simons</a>
                <span class="date">January 20th</span>
            </div>
            <button class="btn btn-outline-primary btn-sm pull-xs-right">
                <i class="ion-heart"></i> 29
            </button>
            </div>
            <a href="" class="preview-link">
            <h1>How to build webapps that scale</h1>
            <p>This is the description for the post.</p>
            <span>Read more...</span>
            </a>
        </div>

        <div class="article-preview">
            <div class="article-meta">
            <a href=""><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a>
            <div class="info">
                <a href="" class="author">Albert Pai</a>
                <span class="date">January 20th</span>
            </div>
            <button class="btn btn-outline-primary btn-sm pull-xs-right">
                <i class="ion-heart"></i> 32
            </button>
            </div>
            <a href="" class="preview-link">
            <h1>The song you won't ever stop singing. No matter how hard you try.</h1>
            <p>This is the description for the post.</p>
            <span>Read more...</span>
            <ul class="tag-list">
                <li class="tag-default tag-pill tag-outline">Music</li>
                <li class="tag-default tag-pill tag-outline">Song</li>
            </ul>
            </a>
        </div>
        </div>
    </div>
    </div>
</div>
`;

export class Profile extends Component {
  static template = PROFILE_TEMPLATE;
}
./src/pages/Profile.js

Editor Page

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

const EDITOR_TEMPLATE = xml/* xml */ `
<div class="editor-page">
  <div class="container page">
    <div class="row">

      <div class="col-md-10 offset-md-1 col-xs-12">
        <form>
          <fieldset>
            <fieldset class="form-group">
                <input type="text" class="form-control form-control-lg" placeholder="Article Title"/>
            </fieldset>
            <fieldset class="form-group">
                <input type="text" class="form-control" placeholder="What's this article about?"/>
            </fieldset>
            <fieldset class="form-group">
                <textarea class="form-control" rows="8" placeholder="Write your article (in markdown)"></textarea>
            </fieldset>
            <fieldset class="form-group">
                <input type="text" class="form-control" placeholder="Enter tags"/><div class="tag-list"></div>
            </fieldset>
            <button class="btn btn-lg pull-xs-right btn-primary" type="button">
                Publish Article
            </button>
          </fieldset>
        </form>
      </div>

    </div>
  </div>
</div>
`;
export class Editor extends Component {
  static template = EDITOR_TEMPLATE;
}
./src/pages/Editor.js

Now that all our pages are created we will now handle the routing and navigation between them.

OWL Router to the rescue

To handle Single Page Applications most of the modern frameworks have a router. OWL is no different.

Creating the routes and adding the router to the env

The router in OWL is an object that has to be instantiated and "attached" to the env of our main App.

Env is an environment is an object which contains a QWeb instance. Whenever a root component is created, it is assigned an environment. This environment is then automatically given to all child components (and accessible in the this.env property).

A router can run in hash or history_mode. Here we will use the hash mode because the expected result for RealWorld App is URLs like /#/profile /#/settings, etc. The router will also handle direct, programmatically navigation/redirection, navigation guards, to protect some routes behind conditions, and routes also accept parameters. Official documentation of OWL router.

To instantiate an OWL router we need an environment and a list of routes.

Inside ./src/main.js we will create our Router. We will have to import router, QWeb from the @odoo/owl.

import { App } from "./App";
import { utils, router, QWeb } from "@odoo/owl";

Before we import each of our pages Components we will create a new file Β ./pages/index.js that will handle all the import/export of the classes so we can import every Component needed in one line later.

import { LogIn } from "./LogIn";
import { Register } from "./Register";
import { Home } from "./Home";
import { Settings } from "./Settings";
import { Editor } from "./Editor";
import { Profile } from "./Profile";

export { LogIn, Register, Home, Settings, Editor, Profile };
./src/pages/index.js

Then back inside our ./src/main.js we can import all the pages and declare the routes that adhere to the specifications of the RealWorld App. These routes have an internal name, a path (without the #), and an associated Component.

import { LogIn, Register, Home, Settings, Editor, Profile } from "./pages";

export const ROUTES = [
  { name: "HOME", path: "/", component: Home },
  { name: "LOG_IN", path: "/login", component: LogIn },
  { name: "REGISTER", path: "/register", component: Register },
  { name: "SETTINGS", path: "/settings", component: Settings },
  { name: "EDITOR", path: "/editor", component: Editor },
  { name: "PROFILE", path: "/profile/@{{username}}", component: Profile },
];

Then we will create our environment and attach the router to it inside a function called makeEnvironement

async function makeEnvironment() {
  const env = { qweb: new QWeb() };
  env.router = new router.Router(env, ROUTES, { mode: "hash" });
  await env.router.start();
  return env;
}

This is our final App.js Component

import { App } from "./App";
import { utils, router, mount, QWeb } from "@odoo/owl";
import { LogIn, Register, Home, Settings, Editor, Profile } from "./pages";

export const ROUTES = [
  { name: "HOME", path: "/", component: Home },
  { name: "LOG_IN", path: "/login", component: LogIn },
  { name: "REGISTER", path: "/register", component: Register },
  { name: "SETTINGS", path: "/settings", component: Settings },
  { name: "EDITOR", path: "/editor", component: Editor },
  { name: "PROFILE", path: "/profile", component: Profile },
];

async function makeEnvironment() {
  const env = { qweb: new QWeb() };
  env.router = new router.Router(env, ROUTES, { mode: "hash" });
  await env.router.start();
  return env;
}

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

utils.whenReady(setup);

Using <RouteComponent/>.

Now that our routes are registered we will update our App Component to make use of the OWL <RouteComponent/>. Inside "./src/App.js":

import { Component, tags, router } from "@odoo/owl";
import { Navbar } from "./components/Navbar";
import { Footer } from "./components/Footer";
import { Home } from "./pages/Home";
const RouteComponent = router.RouteComponent;

const APP_TEMPLATE = tags.xml/*xml*/ `
<main>
  <Navbar/>
  <RouteComponent/>
  <Footer/>
</main>
`;

export class App extends Component {
  static components = { Navbar, Footer, Home, RouteComponent };
  static template = APP_TEMPLATE;
}

What we did here is import the RouteComponent from the router package in @odoo/owl. Then register the RouteComponent inside the static components property and then add it inside the template.

Directly trying http://localhost:8080/#/settings in your browser will show you the setting page!

<Link> is an OWL Component that has a prop, (Attribute that you can pass directly to the Component from the Template and the value is scoped to inside that Component), named to that navigate to the route name.

Inside ./src/components/Navbar.js let's import Link Component and transform our <a href></a> to <Link to=""> Components

import { Component, tags, router } from "@odoo/owl";
const Link = router.Link;

const NAVBAR_TEMPLATE = tags.xml/*xml*/ `
<nav class="navbar navbar-light">
    <div class="container">
        <!-- <a class="navbar-brand" href="index.html">conduit</a> -->
        <Link to="'HOME'" class="navbar-brand">conduit</Link>
        <ul class="nav navbar-nav pull-xs-right">
            <li class="nav-item">
                <!-- Add "active" class when you're on that page" -->
                <Link to="'HOME'" class="nav-link">Home</Link>
            </li>
            <li class="nav-item">
                <Link to="'EDITOR'" class="nav-link"><i class="ion-compose"></i> New Post</Link>
            </li>
            <li class="nav-item">
                <Link to="'SETTINGS'" class="nav-link"><i class="ion-gear-a"></i> Settings</Link>
            </li>
            <li class="nav-item">
                <Link to="'LOG_IN'" class="nav-link">Sign in</Link>
            </li>
            <li class="nav-item">
                <Link to="'REGISTER'" class="nav-link">Sign up</Link>
            </li>
            <li class="nav-item">
                <Link to="'PROFILE'" class="nav-link">Coding Dodo</Link>
            </li>
        </ul>
    </div>
</nav>
`;
export class Navbar extends Component {
  static template = NAVBAR_TEMPLATE;
  static components = { Link };
}

We can see that class is also passed to the <Link/> Component as a prop, the end result is an "href" with the class that was given to the prop.

Going to http://localhost:8080/#/ we can see that our navigation is working!

But there is a little problem with the styles, the original <Link/> Component applies a class of router-active to the "href" if the route corresponds to that link. But our style guide uses the active class directly.

To handle that problem will create our own Custom NavbarLink component in ./src/components/NavbarLink.js

import { tags, router } from "@odoo/owl";
const Link = router.Link;
const { xml } = tags;

const LINK_TEMPLATE = xml/* xml */ `
<a  t-att-class="{'active': isActive }"
    t-att-href="href"
    t-on-click="navigate">
    <t t-slot="default"/>
</a>
`;
export class NavbarLink extends Link {
  static template = LINK_TEMPLATE;
}

As you can see we inherit the base Link Component class and just define another Template that is slightly different.

Then inside our Navbar.js component we update our imports, components and replace the <Link> with our own <NavbarLink>:

import { Component, tags, router } from "@odoo/owl";
const Link = router.Link;
import { NavbarLink } from "./NavbarLink";

const NAVBAR_TEMPLATE = tags.xml/*xml*/ `
<nav class="navbar navbar-light">
    <div class="container">
        <!-- <a class="navbar-brand" href="index.html">conduit</a> -->
        <Link to="'HOME'" class="navbar-brand">conduit</Link>
        <ul class="nav navbar-nav pull-xs-right">
            <li class="nav-item">
                <!-- Add "active" class when you're on that page" -->
                <NavbarLink to="'HOME'" class="nav-link">Home</NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'EDITOR'" class="nav-link"><i class="ion-compose"></i> New Post</NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'SETTINGS'" class="nav-link"><i class="ion-gear-a"></i> Settings</NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'LOG_IN'" class="nav-link">Sign in</NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'REGISTER'" class="nav-link">Sign up</NavbarLink>
            </li>
            <li class="nav-item">
                <NavbarLink to="'PROFILE'" class="nav-link">Coding Dodo</NavbarLink>
            </li>
        </ul>
    </div>
</nav>
`;
export class Navbar extends Component {
  static template = NAVBAR_TEMPLATE;
  static components = { Link, NavbarLink };
}

Conclusion

Ending this first part of the tutorial, we have a functional, albeit basic, routing system. Each of the pages has been created statically (no dynamic data inside) for now.

The source code for this part of the tutorial is available here. To directly clone that branch (that part of the tutorial):

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

In the next part, we will tackle:

  • authentication/registration
  • Using the OWL Store to get info of the currently logged-in user.
  • With that, we will add conditionals to our template to show the correct links if the user is logged in or not.

Part 1 (this one) - Basic Structure of the OWL Project and Components

Part 2 - Authentication, OWL Store

Part 3 (this one) - Dynamic Routing, and willStart functions.

Part 4 - Refactoring, and Hooks.

Thanks for reading and consider becoming a member to stay updated when the next part comes out!

Buy Me A Coffee

Join the conversation

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.