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".
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)
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.
classMagicButtonextendsComponent{statictemplate=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;classMagicButtonextendsComponent{statictemplate=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;classMagicButtonextendsComponent{statictemplate=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 ComponentclassParentextendsComponent{statictemplate=xml`
<div>
<MagicButton initialText="Dont click me!"/>
</div>`;staticcomponents={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.
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.
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.
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><metacharset="UTF-8"/><metaname="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 --><linkhref="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"rel="stylesheet"type="text/css"/><linkhref="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 --><linkrel="stylesheet"href="https://demo.productionready.io/main.css"/><script type="module"src="bundle.js"></script></head><body></body></html>
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";constFOOTER_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>
`;exportclassFooterextendsComponent{statictemplate=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";constHOME_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>
`;exportclassHomeextendsComponent{statictemplate=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:
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.
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 propertycomponents 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()=>{awaitmount(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");awaitnextTick();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
LogIn Page
Register Page
Profile Page
Editor Page
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.
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.
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.
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.
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
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
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.