DEV Community

loading...

RealWorld App with OWL (Odoo Web Library) - Part 1

Coding Dodo
Software Engineer Dodo writing Analysis, Quick-tips, and Tutorials on Odoo, Python ERP, and eCommerce solutions. If you're interested in Odoo development, follow along!
Originally published at codingdodo.com on ・21 min read

RealWorld App with OWL (Odoo Web Library) - Part 1

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

What is the RealWorld 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:

GitHub logo gothinkster / realworld

"The mother of all demo apps" — Exemplary fullstack Medium.com clone powered by React, Angular, Node, Django, and many more 🏅

RealWorld Example Applications

Stay on the bleeding edge — join our GitHub Discussions! 🎉

See how the exact same Medium.com clone (called Conduit) is built using different frontends and backends. Yes, you can mix and match them, because they all adhere to the same API spec 😮😎

While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build real applications with it.

RealWorld solves this by allowing you to choose any frontend (React, Angular 2, & more) and any backend (Node, Django, & more) and see how they power a real world, beautifully designed fullstack app called "Conduit".

Read the full blog post announcing RealWorld on Medium.

Implementations

Over 100 implementations have been created using various languages, libraries, and frameworks.

See the list of implementations on the CodebaseShow website >>>

Create a new implementation




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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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";
  }
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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, I'm using NodeJS v14.15.1

▶ nvm list
       v10.22.0
       v10.24.0
        v14.7.0
-> v14.15.1
default -> 10 (-> v10.24.0)
node -> stable (-> v14.15.1) (default)
stable -> 14.15 (-> v14.15.1) (default)
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo Coding-Dodo / OWL-JavaScript-Project-Starter

OWL JavaScript Project Starter with Rollup

OWL Javascript Project Starter

This repo is an example on how to start a real project with the Odoo OWL framework.

Thanks to @SimonGenin for it's original Starter Project for OWL

Deploy on Netlify

Features

  • OWL
  • Javascript
  • Livereload
  • Rollup.js
  • Tests with Jest

Installation

This repo is a "template repository". It means you can quickly create repositories based on this one, without it being a fork.

Otherwise, you may clone it:

git clone https://github.com/Coding-Dodo/OWL-JavaScript-Project-Starter.git
Enter fullscreen mode Exit fullscreen mode

Install dependencies:

npm install
Enter fullscreen mode Exit fullscreen mode

Dev with livereload:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Production build

npm run build
Enter fullscreen mode Exit fullscreen mode

Run tests

npm run test
Enter fullscreen mode Exit fullscreen mode

Components

It is expected to create components in one file, following this convention:

import { Component, useState, tags } from "@odoo/owl";
const APP_TEMPLATE = tags.xml/*xml*/ `
<div t-name="App" class="" t-on-click="update">
  Hello <t t-esc="state.text"/>
</div>
`;

export class App extends Component {
  static template = APP_TEMPLATE;
  state = useState({ text: 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
})();
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then, to run the tests

npm run test
Enter fullscreen mode Exit fullscreen mode

And finally, to run the development server

npm run dev
Enter fullscreen mode Exit fullscreen mode

The output should be:

rollup v2.48.0
bundles src/main.js → dist/bundle.js...
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 608ms

[2021-05-20 14:33:10] waiting for changes...
Enter fullscreen mode Exit fullscreen mode

RealWorld App with OWL (Odoo Web Library) - Part 1
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
}),
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

Creating the Footer Component

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

Enter fullscreen mode Exit fullscreen mode

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.";
  }
}

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.";
  }
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

Run the tests with the command:

npm run test
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
./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;
}

Enter fullscreen mode Exit fullscreen mode
./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;
}

Enter fullscreen mode Exit fullscreen mode
./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;
}

Enter fullscreen mode Exit fullscreen mode
./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;
}

Enter fullscreen mode Exit fullscreen mode
./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";
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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 },
];
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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!

Adding the <Link> Components to handle navigation.

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

Enter fullscreen mode Exit fullscreen mode

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.

Creating our custom NavbarLink component via inheritance.

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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

Part 2 of this tutorial is available here.

RealWorld App with OWL (Odoo Web Library) - Part 1

Discussion (0)