Each software project has its specific needs. Many of these needs can be solved
with some tooling: webpack
, gulp
, css preprocessor, bundlers, transpilers, ...
Because of that, it is usually not simple to just start a project. Some frameworks provide their own tooling to help with that. But then, you have to integrate and learn how these applications work.
Owl is designed to be used with no tooling at all. Because of that, Owl can "easily" be integrated in a modern build toolchain. In this section, we will discuss a few different setups to start a project. Each of these setups has advantages and disadvantages in different situations.
The simplest possible setup is the following: a simple javascript file with your code. To do that, let us create the following file structure:
hello_owl/
index.html
owl.js
app.js
The file owl.js
can be downloaded from the last release published at
https://github.com/odoo/owl/releases. It
is a single javascript file which export all Owl into the global owl
object.
Note that there are multiple files, and in this case, we need one of the two
files suffixed with .iife
: they are built to be directly used in a browser.
Now, index.html
should contain the following:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="app.js"></script>
</body>
</html>
And app.js
should look like this:
const { Component, mount, xml } = owl;
// Owl Components
class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
mount(Root, document.body);
Now, simply loading this html file in a browser should display a welcome message. This setup is not fancy, but it is extremely simple. There are no tooling at all required. It can be slightly optimized by using the minified build of Owl.
The previous setup has a big disadvantage: the application code is located in a
single file. Obviously, we could split it in several files and add multiple
<script>
tags in the html page, but then we need to make sure the script are
inserted in the proper order, we need to export each file content in global
variables and we lose autocompletion across files.
There is a low tech solution to this issue: using native javascript modules.
This however has a requirement: for security reasons, browsers will not accept
modules on content served through the file
protocol. This means that we need
to use a static server.
Let us start a new project with the following file structure:
hello_owl/
src/
index.html
main.js
owl.js
root.js
As previously, the file owl.js
can be downloaded from the last release published at
https://github.com/odoo/owl/releases.
Note that there are multiple files, and in this case, we need one of the two
files suffixed with .iife
: they are built to be directly used in a browser.
Now, index.html
should contain the following:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="main.js" type="module"></script>
</body>
</html>
Not that the main.js
script tag has the type="module"
attribute. This means
that the browser will parse the script as a module, and load all its dependencies.
Here is the content of root.js
and main.js
:
// root.js ----------------------------------------------------------------------
const { Component, mount, xml } = owl;
export class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
// main.js ---------------------------------------------------------------------
import { Root } from "./root.js";
mount(Root, document.body);
The main.js
file imports the root.js
file. Note that the import statement has
a .js
suffix, which is important. Most text editor can understand this syntax
and will provide autocompletion.
Now, to execute this code, we need to serve the src
folder statically. A low
tech way to do that is to use for example the python SimpleHTTPServer
feature:
$ cd src
$ python -m SimpleHTTPServer 8022 # now content is available at localhost:8022
Another more "javascripty" way to do it is to create a npm
application. To do
that, we can add the following package.json
file at the root of the project:
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Starting Owl app",
"main": "src/index.html",
"scripts": {
"serve": "serve src"
},
"author": "John",
"license": "ISC",
"devDependencies": {
"serve": "^11.3.0"
}
}
We can now install the serve
tool with the command npm install
, and then,
start a static server with the simple npm run serve
command.
The previous setup works, and is certainly good for some usecases, including quick prototyping. However, it lacks some useful features, such as livereload, a test suite, or bundling the code in a single file.
Each of these features, and many others, can be done in many different ways. Since it is really not trivial to configure such a project, we provide here an example that can be used as a starting point.
Our standard Owl project has the following file structure:
hello_owl/
public/
index.html
src/
components/
Root.js
main.js
tests/
components/
Root.test.js
helpers.js
.gitignore
package.json
webpack.config.js
This project as a public
folder, meant to contain all static assets, such as
images and styles. The src
folder has the javascript source code, and finally,
tests
contains the test suite.
Here is the content of index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
</head>
<body></body>
</html>
Note that there are no <script>
tag here. They will be injected by webpack.
Now, let's have a look at the javascript files:
// src/components/Root.js -------------------------------------------------------
import { Component, xml, useState } from "@odoo/owl";
export class Root extends Component {
static template = xml`
<div t-on-click="update">
Hello <t t-esc="state.text"/>
</div>`;
state = useState({ text: "Owl" });
update() {
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
}
}
// src/main.js -----------------------------------------------------------------
import { utils, mount } from "@odoo/owl";
import { Root } from "./components/Root";
mount(Root, document.body);
// tests/components/Root.test.js ------------------------------------------------
import { Root } from "../../src/components/Root";
import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("Root", () => {
test("Works as expected...", async () => {
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div");
await nextTick();
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
});
});
// tests/helpers.js ------------------------------------------------------------
import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime";
export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
return fixture;
}
export function click(elem, selector) {
elem.querySelector(selector).dispatchEvent(new Event("click"));
}
Finally, here is the configuration files .gitignore
, package.json
and
webpack.config.js
:
node_modules/
package-lock.json
dist/
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Demo app",
"main": "src/index.html",
"scripts": {
"test": "jest",
"build": "webpack --mode production",
"dev": "webpack-dev-server --mode development"
},
"author": "Someone",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"babel-jest": "^25.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"regenerator-runtime": "^0.13.3",
"serve": "^11.3.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.2"
},
"dependencies": {
"@odoo/owl": "^1.0.4"
},
"babel": {
"plugins": ["@babel/plugin-proposal-class-properties"],
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
},
"jest": {
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.js?$",
"moduleFileExtensions": ["js"],
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
}
}
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost";
module.exports = function (env, argv) {
const mode = argv.mode || "development";
return {
mode: mode,
entry: "./src/main.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".js", ".jsx"],
},
devServer: {
contentBase: path.resolve(__dirname, "public/index.html"),
compress: true,
hot: true,
host,
port: 3000,
publicPath: "/",
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, "public/index.html"),
}),
],
};
};
With this setup, we can now use the following script commands:
npm run build # build the full application in prod mode in dist/
npm run dev # start a dev server with livereload
npm run test # run the jest test suite