We are now in the third part of our tutorial series about creating the RealWorld App (Demo link) and our current implementation in OWL is available here:

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

To continue directly where we left off last time, you can clone this branch:

git clone -b feature/authentication-registration https://github.com/Coding-Dodo/owl-realworld-app.git

Or continue with your own project, in this part, we will manage the Articles display, in the list and the single article page:

  • Reactive component tightly coupled with data with the use of willUpdate and willUpdateProps.
  • Dynamic routing in the form of /#/article/:slug.

The Article Component

We will begin by creating the first version of the Article Component because it is a pre-requisite of the ArticleList Component that we will build later. This Component will mainly be a "presentation" component, responsible for formatting some data and presenting it, the only real logic it will contain will be the "Like" button. But we will handle that later, for now, we will handle only the presentation layer.

We need to take a quick look at the Conduit API Specs to know how an article looks like when returned from API:

{
slug: "how-to-train-your-dragon",
title: "How to train your dragon",
description: "Ever wonder how?",
body: "It takes a Jacobian",
tagList: ["dragons", "training"],
createdAt: "2016-02-18T03:22:56.637Z",
updatedAt: "2016-02-18T03:48:35.824Z",
favorited: false,
favoritesCount: 0,
author: {
    username: "jake",
    bio: "I work at statefarm",
    image: "https://i.stack.imgur.com/xHWG8.jpg",
    following: false,
},
}

With that knowledge, we will create our ./src/components/Article.js to behave as if it was getting an article object as a prop and displaying its data.

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

const ARTICLE_TEMPLATE = tags.xml/*xml*/ `
<div class="article-preview">
    <div class="article-meta">
        <Link to="'PROFILE'"><img t-att-src="props.article.author.image" /></Link>
        <div class="info">
            <a href="" class="author"><t t-esc="props.article.author.username"/></a>
            <span class="date"><t t-esc="getArticleDate()"/></span>
        </div>
        <button class="btn btn-outline-primary btn-sm pull-xs-right">
            <em class="ion-heart"></em> <t t-esc="props.article.favoritesCount"/>
        </button>
    </div>
    <a href="" class="preview-link">
        <h1><t t-esc="props.article.title"/></h1>
        <p><t t-esc="props.article.description"/></p>
        <span>Read more...</span>
    </a>
</div>
`;
export class Article extends Component {
  static template = ARTICLE_TEMPLATE;
  static components = { Link };
  getters = useGetters();
  static props = {
    article: { type: Object },
  };
  getArticleDate() {
    let articleDate = new Date(this.props.article.createdAt);
    return articleDate.toLocaleDateString("en-US", {
      year: "numeric",
      month: "long",
      day: "numeric",
    });
  }
}

We added a function getArticleDate() that format the Date received from the API to the format expected by the others examples of RealWorld App. In this function, we use the native JavaScript Date object and transform it to the format needed by the specs of the FrontEnd.

With that done, let's tackle the creation of the ArticlesList.

The ArticlesList Component

New functions in our useApi custom hook

First, we will add 2 new API calls inside ./src/hooks/useApi.js to fetch articles.

  async getArticles(queryOptions) {
    let response = {};
    await this.service
      .get("/articles", { params: queryOptions })
      .then((res) => {
        if (res.data && res.data) {
          response = res.data;
        }
      })
      .catch((error) => {
        if (error && error.response) {
          response = error.response.data;
        }
      });
    return response;
  }
  async getArticlesFeed(queryOptions) {
    let response = {};
    await this.service
      .get("/articles/feed", { params: queryOptions })
      .then((res) => {
        if (res.data && res.data) {
          response = res.data;
        }
      })
      .catch((error) => {
        if (error && error.response) {
          response = error.response.data;
        }
      });
    return response;
  }

The getArticle function will take queryParameters as described by the API specs:

Query Parameters:

  • Filter by tag:?tag=AngularJS
  • Filter by author:?author=jake
  • Favorited by user:?favorited=jake
  • Limit number of articles (default is 20):?limit=10
  • Offset/skip number of articles (default is 0):?offset=0

Imagining our Component via analysis of the API

The ArticlesList Component will be the container of the <Article> components, and will also be responsible for fetching and holding the data (list of articles).

The props

The ArticlesList will take different options as props that should reflect the API options to the articles endpoint to keep the logic simple. Reading the props of a current ArticlesList component should reflect the exact parameters request sent to the API.

  • tag (string) prop to render an ArticleList filtered by tag
  • author (string) prop to render an ArticleList filtered by author
  • favorited (string) prop to render ArticleList filtered by "favorited by user=[USERNAME]"
  • limit (number) prop to render ArticleList limit number of articles (default 20)
  • offset (number) of the data being read from the API. The Component will then responsible for its pagination by keeping the same initials filters and updating its own props.
  • feed (boolean) prop to render ArticleList via the /articles/feed endpoint. Only logged-in users, show articles list created by authors that user follow.

For example, the parent component Homepage will be responsible for the props passed to the ArticlesList Component.

The local state

Inside its local state, the ArticlesList component will hold:

  • articles (Array) the list of article objects.
  • articlesCount (Number) the total number of articles.
  • loading (Boolean) state
  • currentOffset (Number) that will be a copy of the passed props but saved to be passed itself to sub-component "Pagination".

Child Components

This Component will probably have 2 child Component:

  • Multiple Article Components, each responsible for rendering a single article
  • A Pagination Component to navigate through the records set.

Creating the ArticlesList Component, props validation and willStart hook.

Now that we know our props  we can create our ./src/components/ArticlesList.js file

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

const ARTICLESLIST_TEMPLATE = tags.xml/*xml*/ `
<section>
    <span>Article count: <t t-esc="state.articlesCount"/></span>
    <span class="loading-articles" t-if="state.loading">
        Loading Articles...
    </span>
    <t t-foreach="state.articles" t-as="article">
        <Article article="article"/>
    </t>
    <span class="loading-articles" t-if="state.loading">
        Loading Articles...
    </span>
</section>
`;
export class ArticlesList extends Component {
  static template = ARTICLESLIST_TEMPLATE;
  static components = { Article };
  static props = {
    options: {
      type: Object,
      optional: true,
      tag: { type: String, optional: true },
      author: { type: String, optional: true },
      favorited: { type: String, optional: true },
      limit: { type: Number, optional: true },
      feed: { type: Boolean, optional: true },
      offset: { type: Number, optional: true },
    },
  };
}

Props validation

  static props = {
    options: {
      type: Object,
      optional: true,
      tag: { type: String, optional: true },
      author: { type: String, optional: true },
      favorited: { type: String, optional: true },
      limit: { type: Number, optional: true },
      feed: { type: Boolean, optional: true },
      offset: { type: Number, optional: true },
    },
  };

Leveraging props validation helps us and other developers of the team, to keep everything clean, and safer to use as our application grows in size.

props are required by default. You will not see any error in production mode but if you are in dev mode you will get an error if you omit any of the props that are not optional: ture.

Also if you are in dev mode and you pass the wrong kind of type for props it will trigger an error.

Handling state with willUpdateProps, willStart

The <ArticlesList> component is tightly coupled with the need to fetch articles from theAPI. Data needs to be present the first time the Component is rendering itself and each time the props are updated.

//imports ...
//template...
export class ArticlesList extends Component {
  static template = ARTICLESLIST_TEMPLATE;
  static components = { Article, Pagination };
  conduitApi = useApi();
  state = useState({
    articles: [],
    articlesCount: 0,
    loading: false,
  });
  static props = {
    queryOptions: {
      type: Object,
      optional: true,
      tag: { type: String, optional: true },
      author: { type: String, optional: true },
      favorited: { type: String, optional: true },
      limit: { type: Number, optional: true },
      feed: { type: Boolean, optional: true },
      offset: { type: Number, optional: true },
    },
  };
  conduitApi = useApi();

  async fetchArticles(queryOptions) {
    let response = {};
    Object.assign(this.state, { loading: true });
    if (queryOptions.feed == true) {
      response = await this.conduitApi.getArticlesFeed(queryOptions);
    } else {
      response = await this.conduitApi.getArticles(queryOptions);
    }
    Object.assign(this.state, response);
    Object.assign(this.state, { loading: false });
  }

  async willStart() {
    this.fetchArticles(this.props.queryOptions);
  }

  async willUpdateProps(nextProps) {
    if (deepEqual(nextProps.queryOptions, this.props.queryOptions)) {
      return;
    }
    this.fetchArticles(nextProps.queryOptions);
  }

First, we define our fetchArticles async function that will do the API call. This function will then be used in the next 2 functions:

willStart

This async function is the perfect spot to fetch data from an API, willStart will be called called just before the first rendering.

willUpdateProps(newProps)

This async function will be called after the props have changed. Here we will also call the API to fetch the articles again with new query parameters.

It will trigger if any change is made to the state of the Component passing the props so we will make a comparison of the old queryOptions with the new queryOptions. Since these are different objects we should do a deep comparison on the keys of the object. That's why we call deepEqual. On top of the file import the function:

import { deepEqual } from "../utils/utils.js";

We will create a utils.js file inside ./utils/ folder with that content

function isObject(object) {
  return object != null && typeof object === "object";
}

export function deepEqual(object1, object2) {
  const keys1 = Object.keys(object1);
  const keys2 = Object.keys(object2);

  if (keys1.length !== keys2.length) {
    return false;
  }

  for (const key of keys1) {
    const val1 = object1[key];
    const val2 = object2[key];
    const areObjects = isObject(val1) && isObject(val2);
    if (
      (areObjects && !deepEqual(val1, val2)) ||
      (!areObjects && val1 !== val2)
    ) {
      return false;
    }
  }

  return true;
}
./utils/utils.js

Refactoring the Home Component and adding the ArticlesList

The Home Component can show different Article List via a Tabs system, specifically the Feed list, the Global list and, the filter by Tags list.

That means that the Home Component should hold in its state the current navigation mode it is on. Then create the config object to pass to <ArticlesList> Component.

First, we will refactor the useState() directive and put it inside the constructor of the component. This will help us to have the initial state of the component:

  • If the user is logged in the current navigation mode by default is FEED
  • If the user is not logged in the current navigation mode by default is GLOBAL
export class Home extends Component {
  static template = HOME_TEMPLATE;
  static components = { ArticlesList };
  getters = useGetters();

  constructor(...args) {
    super(...args);
    let initialNavMode = "GLOBAL";
    let initialArticlesOptions = { limit: 10, offset: 0 };
    if (this.getters.userLoggedIn()) {
      initialNavMode = "FEED";
      initialArticlesOptions = { feed: true, limit: 10, offset: 0 };
    }
    this.state = useState({
      text: "A place to share your knowledge.",
      navigationMode: initialNavMode,
      articlesOptions: initialArticlesOptions,
    });
  }

Inside the XML Template we also do some modifications:

  • Pass our state.articlesOptions to the ArticlesList Component as props
<ArticlesList queryOptions="state.articlesOptions"/>

Creating the Tabs system Feed/Global Feed/Tags inside our Home Component

Inside the Home component, we create a function to handle the change of navigation mode.

  changeNavigationMode(navigationMode, tag) {
    if (navigationMode == "FEED" && !this.getters.userLoggedIn()) {
      return;
    }
    let articlesOptions = {};
    switch (navigationMode) {
      case "FEED":
        articlesOptions = { feed: true, limit: 10, offset: 0 };
        break;
      case "TAGS":
        articlesOptions = { tag: tag, limit: 10, offset: 0 };
        break;
      default:
        articlesOptions = { limit: 10, offset: 0 };
    }
    Object.assign(this.state, {
      navigationMode: navigationMode,
      articlesOptions: articlesOptions,
    });
  }
./src/pages/Home.js

Calling that function will update the state of the Home Component to change the navigationMode and the articlesOptions (that is passed via props to the <ArticlesList/>).

Then we update the Template of the Home Component to make use of these new functions and states.

<div class="col-md-9">
    <div class="feed-toggle">
        <ul class="nav nav-pills outline-active">
            <li class="nav-item">
                <a  t-attf-class="nav-link {{ getters.userLoggedIn() ? '' : 'disabled' }} {{ state.navigationMode == 'FEED' ? 'active' : '' }}" 
                    t-on-click.prevent="changeNavigationMode('FEED')"
                    href="/">
                    Your Feed
                </a>
            </li>
            <li class="nav-item">
                <a  t-attf-class="nav-link {{ state.navigationMode == 'GLOBAL' ? 'active' : '' }}" 
                    t-on-click.prevent="changeNavigationMode('GLOBAL')"
                    href="/">
                    Global Feed
                </a>
            </li>
            <li class="nav-item" t-if="state.navigationMode == 'TAGS' and state.articlesOptions.tag">
                <a  t-attf-class="nav-link {{ state.navigationMode == 'TAGS' ? 'active' : '' }}" 
                    href="#">
                    <i class="ion-pound"></i> <t t-esc="state.articlesOptions.tag"/>
                </a>
            </li>
        </ul>
    </div>
    <ArticlesList queryOptions="state.articlesOptions"/>
</div>

Now we have the Home component that will change its state navigation mode and the query parameters to the ArticlesList. From the articles list, we used the hook onWillUpdateProps to listen to the change of props and fetch again the articles from the API with these new params.

We don't have any Pagination yet be we can now test that our Home is working correctly and displaying articles.

Pagination: Emitting events from Child to Parent component

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