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:
gothinkster
/
realworld
"The mother of all demo apps" โ Exemplary fullstack Medium.com clone powered by React, Angular, Node, Django, and many more ๐
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();
}
}
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, 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)
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
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
Features
- OWL
- Javascript
- Livereload
- Rollup.js
- Tests with Jest
Installation
Otherwise, you may clone it:
git clone https://github.com/Coding-Dodo/OWL-JavaScript-Project-Starter.git
Install dependencies:
npm install
Dev with livereload:
npm run dev
Production build
npm run build
Run tests
npm run test
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:
โฆ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.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...
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 xml
comments 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 & 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;
}
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;
}
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;
}
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;
}
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;
}
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 };
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!
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 };
}
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;
}
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.
Thanks for reading and consider becoming a member to stay updated when the next part comes out!
Top comments (0)