diff --git a/tools/playground/export_snippet.js b/tools/playground/export_snippet.js
deleted file mode 100644
index 7e0b8aef..00000000
--- a/tools/playground/export_snippet.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import { loadJS } from "./utils.js";
-
-const sourceCache = new Map();
-async function getSourceCode(filePath) {
- if (sourceCache.has(filePath)) {
- return sourceCache.get(filePath);
- }
- let source = await fetch(`${filePath}.js`);
- source = await source.text();
- sourceCache.set(filePath, source);
- return source;
-}
-
-const DEFAULT_HTML = `
-
-
-
- OWL App
-
-
-
-
-
-
-
-
-
-
-`;
-
-const APP_PY = `#!/usr/bin/env python3
-
-import threading
-import time
-
-from http.server import SimpleHTTPRequestHandler, HTTPServer
-
-def start_server():
- SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
- httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
- httpd.serve_forever()
-
-url = 'http://127.0.0.1:3600'
-
-if __name__ == "__main__":
- print("Owl Application")
- print("---------------")
- print("Server running on: {}".format(url))
- threading.Thread(target=start_server, daemon=True).start()
-
- while True:
- try:
- time.sleep(1)
- except KeyboardInterrupt:
- httpd.server_close()
- quit(0)
-`;
-
-/**
- * Make a zip file containing a functioning application
- */
-export async function exportStandaloneApp(js, css, xml) {
- await loadJS("libs/jszip.min.js");
-
- const zip = new JSZip();
- const processedJS = js
- .split("\n")
- .map(l => (l === "" ? "" : " " + l))
- .join("\n");
-
- const JS = `
-/**
- * This is the javascript code defined in the playground.
- * In a larger application, this code should probably be moved in different
- * sub files.
- */
-import * as utils from "./utils.js";
-async function app() {
-${processedJS}
-}
-
-/**
- * Initialization code
- * This code load templates, and make sure everything is properly connected.
- */
-function prepareOWLApp(templates) {
- const _configure = owl.App.prototype.configure;
- owl.App.prototype.configure = function configureOverriden(config) {
- config = Object.assign({ dev: true }, config);
- this.addTemplates(templates);
- return _configure.call(this, config);
- }
-}
-
-async function start() {
- let templates;
- try {
- templates = await owl.loadFile('app.xml');
- } catch(e) {
- console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
- return;
- }
- prepareOWLApp(templates);
- app();
-}
-
-start();
-`;
-
- const [owlSrc, utilsSrc] = await Promise.all([
- getSourceCode("../owl"),
- getSourceCode("./utils"),
- ]);
-
- zip.file("app.js", JS);
- zip.file("utils.js", utilsSrc);
- zip.file("app.css", css);
- zip.file("app.py", APP_PY);
- zip.file("app.xml", xml);
- zip.file("index.html", DEFAULT_HTML);
- zip.file("owl.js", owlSrc);
- return zip.generateAsync({ type: "blob" });
-}
diff --git a/tools/playground/index.html b/tools/playground/index.html
index 6b9ba063..00875d04 100644
--- a/tools/playground/index.html
+++ b/tools/playground/index.html
@@ -6,16 +6,12 @@
-
-
-
-
+
+
+
-
-
-
-
+
diff --git a/tools/playground/playground.css b/tools/playground/playground.css
index 191bf196..907ac7bf 100644
--- a/tools/playground/playground.css
+++ b/tools/playground/playground.css
@@ -20,7 +20,7 @@ body {
display: grid;
height: 100%;
width: 100%;
- grid-template-columns: auto 8px 100%;
+ grid-template-columns: auto 8px 1fr;
}
/* LEFT BAR ****************************************/
diff --git a/tools/playground/app.js b/tools/playground/playground.js
similarity index 62%
rename from tools/playground/app.js
rename to tools/playground/playground.js
index 8fd5ce6e..068161a4 100644
--- a/tools/playground/app.js
+++ b/tools/playground/playground.js
@@ -1,7 +1,19 @@
-import { SAMPLES } from "./samples.js";
import { debounce, loadJS } from "./utils.js";
-import { exportStandaloneApp } from "./export_snippet.js";
-const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl;
+import {
+ App,
+ Component,
+ useState,
+ useRef,
+ onMounted,
+ onWillUnmount,
+ onPatched,
+ onWillUpdateProps,
+ loadFile as _loadFile,
+ whenReady,
+ __info__,
+ useEffect,
+ onWillStart,
+} from "../owl.js";
//------------------------------------------------------------------------------
// Constants, helpers, utils
@@ -16,40 +28,38 @@ const MODES = {
const DEFAULT_XML = `
`;
+// Memoize loadFile so that samples aren't reloaded whenever a sample is selected
+const fileCache = {};
+const loadFile = (path) => {
+ if (!(path in fileCache)) {
+ fileCache[path] = _loadFile(path);
+ }
+ return fileCache[path];
+}
+
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
- const sanitizedXML = xml.replace(//g, "").replace(/`/g, '\\\`');
+ // escape backticks in the xml so they don't close the template string
+ const escapedXml = xml.replace(/`/g, '\\\`');
- // create iframe
const iframe = document.createElement("iframe");
-
iframe.onload = () => {
const doc = iframe.contentDocument;
- const utilsMod = doc.createElement("script");
- utilsMod.setAttribute("type", "module");
- utilsMod.setAttribute("src", "utils.js");
- doc.head.appendChild(utilsMod);
- // inject js
- const owlScript = doc.createElement("script");
- owlScript.type = "text/javascript";
- owlScript.src = "../owl.js";
- owlScript.addEventListener("load", () => {
- const script = doc.createElement("script");
- script.type = "module";
- const content = `
- (async function(TEMPLATES) {
- ${js}
- })(\`${sanitizedXML}\`)`;
- script.innerHTML = content;
- doc.body.appendChild(script);
- });
- doc.head.appendChild(owlScript);
- // inject css
+ const importMap = doc.createElement("script");
+ importMap.type = "importmap";
+ importMap.textContent = `{ "imports": { "@odoo/owl": "../owl.js" } }`;
+ doc.head.appendChild(importMap);
+
+ const script = doc.createElement("script");
+ script.type = "module";
+ script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
+ doc.body.appendChild(script);
+
const style = document.createElement("style");
- style.innerHTML = css;
+ style.innerText = css;
doc.head.appendChild(style);
};
return iframe;
@@ -58,53 +68,84 @@ function makeCodeIframe(js, css, xml) {
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
+
+const SAMPLES = [
+ {
+ description: "Components",
+ folder: "components",
+ code: ["js", "xml", "css"],
+ },
+ {
+ description: "Form Input Bindings",
+ folder: "form",
+ code: ["js", "xml"],
+ },
+ {
+ description: "Inline templates",
+ folder: "single_file_component",
+ code: ["js"],
+ },
+ {
+ description: "Lifecycle demo",
+ folder: "lifecycle_demo",
+ code: ["js", "xml", "css"],
+ },
+ {
+ description: "Customized hook",
+ folder: "custom_hooks",
+ code: ["js", "xml", "css"],
+ },
+ {
+ description: "Todo List App (with reactivity)",
+ folder: "todo_app",
+ code: ["js", "xml", "css"],
+ },
+ {
+ description: "Responsive app",
+ folder: "responsive_app",
+ code: ["js", "xml", "css"],
+ },
+ {
+ description: "Slots And Generic Components",
+ folder: "slots",
+ code: ["js", "xml", "css"],
+ },
+ {
+ description: "Window Management System",
+ folder: "window_manager",
+ code: ["js", "xml", "css"],
+ },
+ {
+ description: "Benchmark example",
+ folder: "benchmark",
+ code: ["js", "xml", "css"],
+ },
+]
+
function loadSamples() {
- let result = SAMPLES.slice();
+ const result = SAMPLES.map(({ description, folder, code }) => ({
+ description,
+ code: async () => Object.fromEntries(
+ await Promise.all(
+ code.map(async (type) => [type, await loadFile(`./samples/${folder}/${folder}.${type}`)])
+ )
+ ),
+ }));
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
- code: js,
- xml,
- css
+ code: () => Promise.resolve({ js, xml, css }),
});
}
return result;
}
-function saveLocalSample(js, css, xml) {
- const str = JSON.stringify({ js, css, xml });
- localStorage.setItem("owl-playground-local-sample", str);
-}
-
-function deleteLocalSample() {
- localStorage.removeItem("owl-playground-local-sample");
-}
-
-function useSamples() {
- const samples = loadSamples();
- const component = owl.useComponent();
- let interval;
-
- onMounted(() => {
- const state = component.state;
- interval = setInterval(() => {
- if (component.isDirty) {
- saveLocalSample(state.js, state.css, state.xml);
- }
- }, 1000);
- });
- onWillUnmount(() => {
- clearInterval(interval);
- });
- return samples;
-}
-
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
-class TabbedEditor extends owl.Component {
+class TabbedEditor extends Component {
setup() {
const props = this.props;
this.state = useState({
@@ -168,7 +209,7 @@ class TabbedEditor extends owl.Component {
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
- this.trigger("updatePanelHeight", { delta });
+ this.props.updatePanelHeight({ delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
@@ -205,36 +246,53 @@ TabbedEditor.template = "TabbedEditor";
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
-class App extends owl.Component {
+class Playground extends Component {
+ static template = "Playground";
+ static components = { TabbedEditor };
setup() {
- this.version = owl.__info__.version;
- this.SAMPLES = useSamples();
- this.isDirty = false;
+ this.version = __info__.version;
+ this.isDirty = false;
this.state = useState({
- js: this.SAMPLES[0].code,
- css: this.SAMPLES[0].css || "",
- xml: this.SAMPLES[0].xml || DEFAULT_XML,
+ js: "",
+ css: "",
+ xml: DEFAULT_XML,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
- this.toggleLayout = debounce(this.toggleLayout, 250, true);
- this.runCode = debounce(this.runCode, 250, true);
- this.downloadCode = debounce(this.downloadCode, 250, true);
- this.content = useRef("content");
- this.updateCode = this.updateCode.bind(this);
+ this.samples = loadSamples();
if (window.location.hash) {
try {
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
- if (![js, css, xml].every(item => typeof item === "string")) {
- return;
+ if ([js, css, xml].every(item => typeof item === "string")) {
+ Object.assign(this.state, { js, css, xml });
}
- Object.assign(this.state, { js, css, xml });
} catch {}
}
+ onWillStart(async () => {
+ if (!this.state.js) {
+ this.setSample(await this.samples[0].code());
+ }
+ })
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (this.isDirty) {
+ const { js, css, xml } = this.state;
+ const str = JSON.stringify({ js, css, xml });
+ localStorage.setItem("owl-playground-local-sample", str);
+ }
+ }, 1000);
+ return () => clearInterval(interval);
+ }, () => []);
+
+ this.toggleLayout = debounce(this.toggleLayout, 250, true);
+ this.runCode = debounce(this.runCode, 250, true);
+ this.exportStandaloneApp = debounce(this.exportStandaloneApp, 250, true);
+ this.content = useRef("content");
+ this.updateCode = this.updateCode.bind(this);
}
runCode() {
@@ -258,12 +316,11 @@ class App extends owl.Component {
window.location.href = link.href;
}
- setSample(ev) {
- const sample = this.SAMPLES.find(s => s.description === ev.target.value);
- this.state.js = sample.code;
+ setSample(sample) {
+ this.state.js = sample.js;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
- deleteLocalSample();
+ localStorage.removeItem("owl-playground-local-sample");
this.isDirty = false;
}
@@ -271,14 +328,14 @@ class App extends owl.Component {
return `width:${this.state.leftPaneWidth}px`;
}
- get rightPaneStyle() {
- return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
- }
-
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
+ async onSampleChange(ev) {
+ this.setSample(await this.samples.find(s => s.description === ev.target.value).code());
+ }
+
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
@@ -305,39 +362,40 @@ class App extends owl.Component {
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
- updatePanelHeight(ev) {
- if (!ev.detail.delta) {
- return;
- }
- let height = this.state.topPanelHeight;
- if (!height) {
- height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
- }
- this.state.topPanelHeight = height + ev.detail.delta;
+ updatePanelHeight({ delta }) {
+ let height = this.state.topPanelHeight || document.querySelector(".tabbed-editor").clientHeight;
+ this.state.topPanelHeight = height + delta;
}
- async downloadCode() {
+ async exportStandaloneApp() {
const { js, css, xml } = this.state;
- const content = await exportStandaloneApp(js, css, xml);
+
+ await loadJS("libs/jszip.min.js");
+ const zip = new JSZip();
+ zip.file("app.py", await loadFile("./standalone_app/app.py"));
+ zip.file("index.html", await loadFile("./standalone_app/index.html"));
+ zip.file("owl.js", await loadFile("../owl.js"));
+ zip.file("app.js", `const TEMPLATES = await (await fetch('app.xml')).text();\n${js}`);
+ zip.file("app.css", css);
+ zip.file("app.xml", xml);
+
await loadJS("libs/FileSaver.min.js");
- saveAs(content, "app.zip");
+ saveAs(await zip.generateAsync({ type: "blob" }), "app.zip");
}
}
-App.components = { TabbedEditor };
-App.template = "App";
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
- document.title = `${document.title} (v${owl.__info__.version})`;
- const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
+ document.title = `${document.title} (v${__info__.version})`;
+ const commit = `https://github.com/odoo/owl/commit/${__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
- owl.loadFile("templates.xml"),
- owl.whenReady()
+ loadFile("templates.xml"),
+ whenReady()
]);
- const rootApp = new owl.App(App);
+ const rootApp = new App(Playground);
rootApp.addTemplates(templates);
await rootApp.mount(document.body);
diff --git a/tools/playground/samples.js b/tools/playground/samples.js
deleted file mode 100644
index 8299d673..00000000
--- a/tools/playground/samples.js
+++ /dev/null
@@ -1,1818 +0,0 @@
-const COMPONENTS = /*js*/ `
-// In this example, we show how components can be defined and created.
-const { Component, useState, mount } = owl;
-
-class Greeter extends Component {
- static template = "Greeter";
-
- setup() {
- this.state = useState({ word: 'Hello' });
- }
-
- toggle() {
- this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
- }
-}
-
-// Main root component
-class Root extends Component {
- static components = { Greeter };
- static template = "Root"
-
- setup() {
- this.state = useState({ name: 'World'});
- }
-}
-
-mount(Root, document.body, { templates: TEMPLATES, dev: true });`;
-
-const COMPONENTS_XML = /*xml*/`
-
-
- ,
-
-
-
-
-
-
-`;
-
-const COMPONENTS_CSS = /*css*/`
-.greeter {
- font-size: 20px;
- width: 300px;
- height: 100px;
- margin: 5px;
- text-align: center;
- line-height: 100px;
- background-color: #eeeeee;
- user-select: none;
-}`;
-
-const LIFECYCLE_DEMO = /*js*/`
-// This example shows all the possible lifecycle hooks
-//
-// The root component controls a sub component (DemoComponent). It logs all its lifecycle
-// methods in the console. Try modifying its state by clicking on it, or by
-// clicking on the two main buttons, and look into the console to see what
-// happens.
-const { Component, useState, mount, useComponent, onWillStart, onMounted, onWillUnmount, onWillUpdateProps, onPatched, onWillPatch, onWillRender, onRendered, onWillDestroy} = owl;
-
-function useLogLifecycle() {
- const component = useComponent();
- const name = component.constructor.name;
- onWillStart(() => console.log(\`\${name}:willStart\`));
- onMounted(() => console.log(\`\${name}:mounted\`));
- onWillUpdateProps(() => console.log(\`\${name}:willUpdateProps\`));
- onWillRender(() => console.log(\`\${name}:willRender\`));
- onRendered(() => console.log(\`\${name}:rendered\`));
- onWillPatch(() => console.log(\`\${name}:willPatch\`));
- onPatched(() => console.log(\`\${name}:patched\`));
- onWillUnmount(() => console.log(\`\${name}:willUnmount\`));
- onWillDestroy(() => console.log(\`\${name}:willDestroy\`));
-}
-
-class DemoComponent extends Component {
- static template = "DemoComponent";
-
- setup() {
- useLogLifecycle();
- this.state = useState({ n: 0 });
- }
- increment() {
- this.state.n++;
- }
-}
-
-class Root extends Component {
- static template = "Root";
- static components = { DemoComponent };
-
- setup() {
- useLogLifecycle();
- this.state = useState({ n: 0, flag: true });
- }
-
- increment() {
- this.state.n++;
- }
-
- toggleSubComponent() {
- this.state.flag = !this.state.flag;
- }
-}
-
-mount(Root, document.body, { templates: TEMPLATES, dev: true });
-`;
-
-const LIFECYCLE_DEMO_XML = /*xml*/ `
-
-
-
Demo Sub Component
-
(click on me to update me)
-
Props: , State: .
-
-
-
-
Increment Parent State
-
Toggle SubComponent
-
-
-
-
- `;
-
-const LIFECYCLE_CSS = /*css*/`
-button {
- font-size: 18px;
- margin: 5px;
-}
-
-.demo {
- margin: 10px;
- padding: 10px;
- background-color: #dddddd;
- width: 250px;
-}`;
-
-const HOOKS_DEMO = /*js*/ `
-// In this example, we show how hooks can be used or defined.
-const { Component, mount, useState, onWillDestroy } = owl;
-
-// We define here a custom behaviour: this hook tracks the state of the mouse
-// position
-function useMouse() {
- const position = useState({x:0, y: 0});
-
- function update(e) {
- position.x = e.clientX;
- position.y = e.clientY;
- }
- window.addEventListener('mousemove', update);
- onWillDestroy(() => {
- window.removeEventListener('mousemove', update);
- });
-
- return position;
-}
-
-
-// Main root component
-class Root extends Component {
- static template = "Root";
-
- setup() {
- // simple state hook (reactive object)
- this.counter = useState({ value: 0 });
-
- // this hooks is bound to the 'mouse' property.
- this.mouse = useMouse();
- }
-
- increment() {
- this.counter.value++;
- }
-}
-
-// Application setup
-mount(Root, document.body, { templates: TEMPLATES, dev: true });
-`;
-
-const HOOKS_DEMO_XML = /*xml*/ `
-
-
-
-`;
-
-const HOOKS_CSS = /*css*/ `button {
- width: 120px;
- height: 35px;
- font-size: 16px;
-}`;
-
-const TODO_APP_REACTIVITY = /*js*/ `
-
-// This example is an implementation of the TodoList application, from the
-// www.todomvc.com project. This is a non trivial application with some
-// interesting user interactions. It uses the local storage for persistence.
-//
-// In this implementation, we use the owl reactivity mechanism.
-const { Component, useState, mount, useRef, onPatched, onMounted, reactive, useEnv, useEffect } = owl;
-
-//------------------------------------------------------------------------------
-// Constants, helpers
-//------------------------------------------------------------------------------
-const ENTER_KEY = 13;
-const ESC_KEY = 27;
-const LOCALSTORAGE_KEY = "todomvc";
-
-function useAutofocus(name) {
- let ref = useRef(name);
- useEffect(el => el && el.focus(), () => [ref.el]);
-}
-
-function useStore() {
- const env = useEnv();
- return useState(env.store);
-}
-
-//------------------------------------------------------------------------------
-// Task store
-//------------------------------------------------------------------------------
-class TaskList {
- constructor(tasks) {
- this.tasks = tasks || [];
- const taskIds = this.tasks.map((t) => t.id);
- this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
- }
-
- addTask(text) {
- text = text.trim();
- if (text) {
- const task = {
- id: this.nextId++,
- text: text,
- isCompleted: false,
- };
- this.tasks.push(task);
- }
- }
-
- toggleTask(task) {
- task.isCompleted = !task.isCompleted;
- }
-
- toggleTask(id) {
- const task = this.tasks.find(t => t.id === id);
- task.isCompleted = !task.isCompleted;
- }
-
- toggleAll(value) {
- for (let task of this.tasks) {
- task.isCompleted = value;
- }
- }
-
- clearCompleted() {
- const tasks = this.tasks.filter(t => t.isCompleted);
- for (let task of tasks) {
- this.deleteTask(task);
- }
- }
-
- deleteTask(id) {
- const index = this.tasks.findIndex((t) => t.id === id);
- this.tasks.splice(index, 1);
- }
-
- updateTask(id, text) {
- const value = text.trim();
- if (!value) {
- this.deleteTask(id);
- } else {
- const task = this.tasks.find(t => t.id === id);
- task.text = value;
- }
- }
-}
-
-function createTaskStore() {
- const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
- const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
- const taskStore = reactive(new TaskList(initialTasks), saveTasks);
- saveTasks();
- return taskStore;
-}
-
-//------------------------------------------------------------------------------
-// Todo
-//------------------------------------------------------------------------------
-class Todo extends Component {
- static template = "Todo";
-
- setup() {
- useAutofocus("input");
- this.store = useStore();
- this.state = useState({
- isEditing: false
- });
- }
-
- handleKeyup(ev) {
- if (ev.keyCode === ENTER_KEY) {
- this.updateText(ev.target.value);
- }
- if (ev.keyCode === ESC_KEY) {
- ev.target.value = this.props.text;
- this.state.isEditing = false;
- }
- }
-
- handleBlur(ev) {
- this.updateText(ev.target.value);
- }
-
- updateText(text) {
- this.store.updateTask(this.props.id, text);
- this.state.isEditing = false;
- }
-}
-
-//------------------------------------------------------------------------------
-// TodoList
-//------------------------------------------------------------------------------
-class TodoList extends Component {
- static template = "TodoList";
- static components = { Todo };
-
- setup() {
- this.store = useStore();
- this.state = useState({ filter: "all" });
- }
-
- get displayedTasks() {
- const tasks = this.store.tasks;
- switch (this.state.filter) {
- case "active":
- return tasks.filter((t) => !t.isCompleted);
- case "completed":
- return tasks.filter((t) => t.isCompleted);
- case "all":
- return tasks;
- }
- }
-
- get allChecked() {
- return this.store.tasks.every(todo => todo.isCompleted);
- }
-
- get remaining() {
- return this.store.tasks.filter(todo => !todo.isCompleted).length;
- }
-
- get remainingText() {
- const items = this.remaining < 2 ? "item" : "items";
- return \` \${items} left\`;
- }
-
- addTodo(ev) {
- if (ev.keyCode === ENTER_KEY) {
- const text = ev.target.value;
- if (text.trim()) {
- this.store.addTask(text);
- }
- ev.target.value = "";
- }
- }
-
- setFilter(filter) {
- this.state.filter = filter;
- }
-}
-
-//------------------------------------------------------------------------------
-// App Initialization
-//------------------------------------------------------------------------------
-const env = { store: createTaskStore() };
-mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
-`;
-
-const TODO_APP_REACTIVITY_XML = /*xml*/ `
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
-const TODO_APP_REACTIVITY_CSS = /*css*/`
-html,body {
- margin: 0;
- padding: 0;
-}
-
-button {
- margin: 0;
- padding: 0;
- border: 0;
- background: none;
- font-size: 100%;
- vertical-align: baseline;
- font-family: inherit;
- font-weight: inherit;
- color: inherit;
- -webkit-appearance: none;
- appearance: none;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-body {
- font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
- line-height: 1.4em;
- background: #f5f5f5;
- color: #4d4d4d;
- min-width: 230px;
- max-width: 550px;
- margin: 0 auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- font-weight: 300;
-}
-
-:focus {
- outline: 0;
-}
-
-.hidden {
- display: none;
-}
-
-.todoapp {
- background: #fff;
- margin: 130px 0 40px 0;
- position: relative;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
-}
-
-.todoapp input::-webkit-input-placeholder {
- font-style: italic;
- font-weight: 300;
- color: #e6e6e6;
-}
-
-.todoapp input::-moz-placeholder {
- font-style: italic;
- font-weight: 300;
- color: #e6e6e6;
-}
-
-.todoapp input::input-placeholder {
- font-style: italic;
- font-weight: 300;
- color: #e6e6e6;
-}
-
-.todoapp h1 {
- position: absolute;
- top: -155px;
- width: 100%;
- font-size: 100px;
- font-weight: 100;
- text-align: center;
- color: rgba(175, 47, 47, 0.15);
- -webkit-text-rendering: optimizeLegibility;
- -moz-text-rendering: optimizeLegibility;
- text-rendering: optimizeLegibility;
-}
-
-.new-todo,
-.edit {
- position: relative;
- margin: 0;
- width: 100%;
- font-size: 24px;
- font-family: inherit;
- font-weight: inherit;
- line-height: 1.4em;
- border: 0;
- color: inherit;
- padding: 6px;
- border: 1px solid #999;
- box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
- box-sizing: border-box;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.new-todo {
- padding: 16px 16px 16px 60px;
- border: none;
- background: rgba(0, 0, 0, 0.003);
- box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
-}
-
-.main {
- position: relative;
- z-index: 2;
- border-top: 1px solid #e6e6e6;
-}
-
-.toggle-all {
- width: 1px;
- height: 1px;
- border: none; /* Mobile Safari */
- opacity: 0;
- position: absolute;
- right: 100%;
- bottom: 100%;
-}
-
-.toggle-all + label {
- width: 60px;
- height: 34px;
- font-size: 0;
- position: absolute;
- top: -52px;
- left: -13px;
- -webkit-transform: rotate(90deg);
- transform: rotate(90deg);
-}
-
-.toggle-all + label:before {
- content: "❯";
- font-size: 22px;
- color: #e6e6e6;
- padding: 10px 27px 10px 27px;
-}
-
-.toggle-all:checked + label:before {
- color: #737373;
-}
-
-.todo-list {
- margin: 0;
- padding: 0;
- list-style: none;
-}
-
-.todo-list li {
- position: relative;
- font-size: 24px;
- border-bottom: 1px solid #ededed;
-}
-
-.todo-list li:last-child {
- border-bottom: none;
-}
-
-.todo-list li.editing {
- border-bottom: none;
- padding: 0;
-}
-
-.todo-list li.editing .edit {
- display: block;
- width: calc(100% - 43px);
- padding: 12px 16px;
- margin: 0 0 0 43px;
-}
-
-.todo-list li.editing .view {
- display: none;
-}
-
-.todo-list li .toggle {
- text-align: center;
- width: 40px;
- /* auto, since non-WebKit browsers doesn't support input styling */
- height: auto;
- position: absolute;
- top: 0;
- bottom: 0;
- margin: auto 0;
- border: none; /* Mobile Safari */
- -webkit-appearance: none;
- appearance: none;
-}
-
-.todo-list li .toggle {
- opacity: 0;
-}
-
-.todo-list li .toggle + label {
- /*
- Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
- IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
- */
- background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: center left;
-}
-
-.todo-list li .toggle:checked + label {
- background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
-}
-
-.todo-list li label {
- word-break: break-all;
- padding: 15px 15px 15px 60px;
- display: block;
- line-height: 1.2;
- transition: color 0.4s;
-}
-
-.todo-list li.completed label {
- color: #d9d9d9;
- text-decoration: line-through;
-}
-
-.todo-list li .destroy {
- display: none;
- position: absolute;
- top: 0;
- right: 10px;
- bottom: 0;
- width: 40px;
- height: 40px;
- margin: auto 0;
- font-size: 30px;
- color: #cc9a9a;
- margin-bottom: 11px;
- transition: color 0.2s ease-out;
-}
-
-.todo-list li .destroy:hover {
- color: #af5b5e;
-}
-
-.todo-list li .destroy:after {
- content: "×";
-}
-
-.todo-list li:hover .destroy {
- display: block;
-}
-
-.todo-list li .edit {
- display: none;
-}
-
-.todo-list li.editing:last-child {
- margin-bottom: -1px;
-}
-
-.footer {
- color: #777;
- padding: 10px 15px;
- height: 20px;
- text-align: center;
- border-top: 1px solid #e6e6e6;
-}
-
-.footer:before {
- content: "";
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- height: 50px;
- overflow: hidden;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
- 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
- 0 17px 2px -6px rgba(0, 0, 0, 0.2);
-}
-
-.todo-count {
- float: left;
- text-align: left;
-}
-
-.todo-count strong {
- font-weight: 300;
-}
-
-.filters {
- margin: 0;
- padding: 0;
- list-style: none;
- position: absolute;
- right: 0;
- left: 0;
-}
-
-.filters li {
- display: inline;
-}
-
-.filters li a {
- color: inherit;
- margin: 3px;
- padding: 3px 7px;
- text-decoration: none;
- border: 1px solid transparent;
- border-radius: 3px;
-}
-
-.filters li a:hover {
- border-color: rgba(175, 47, 47, 0.1);
-}
-
-.filters li a.selected {
- border-color: rgba(175, 47, 47, 0.2);
-}
-
-.clear-completed,
-html .clear-completed:active {
- float: right;
- position: relative;
- line-height: 20px;
- text-decoration: none;
- cursor: pointer;
-}
-
-.clear-completed:hover {
- text-decoration: underline;
-}
-
-.info {
- margin: 65px auto 0;
- color: #bfbfbf;
- font-size: 10px;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
- text-align: center;
-}
-
-.info p {
- line-height: 1;
-}
-
-.info a {
- color: inherit;
- text-decoration: none;
- font-weight: 400;
-}
-
-.info a:hover {
- text-decoration: underline;
-}
-
-/*
- Hack to remove background from Mobile Safari.
- Can't use it globally since it destroys checkboxes in Firefox
-*/
-@media screen and (-webkit-min-device-pixel-ratio: 0) {
- .toggle-all,
- .todo-list li .toggle {
- background: none;
- }
-
- .todo-list li .toggle {
- height: 40px;
- }
-}
-
-@media (max-width: 430px) {
- .footer {
- height: 50px;
- }
-
- .filters {
- bottom: 10px;
- }
-}
-
-.filters a {
- cursor: pointer;
-}
-`;
-
-const RESPONSIVE = /*js*/ `
-// In this example, we show how one can design an application that is responsive:
-// its UI is different in mobile mode or in desktop mode.
-//
-// The main idea is to have a "isMobile" key in the environment, then listen
-// to resize events and update the env if needed. Then, the whole interface
-// will be updated, creating and destroying components as needed.
-//
-// To see this in action, try resizing the window. The application will switch
-// to mobile mode whenever it has less than 768px.
-const { Component, useState, mount, reactive, useEnv } = owl;
-
-//------------------------------------------------------------------------------
-// Helpers
-//------------------------------------------------------------------------------
-
-function debounce(func, wait, immediate) {
- let timeout;
- return function () {
- const context = this;
- const args = arguments;
- function later() {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
- }
- const callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
- }
- };
-}
-
-//------------------------------------------------------------------------------
-// Responsive hook
-//------------------------------------------------------------------------------
-
-function createUI() {
- const getIsMobile = () => window.innerWidth <= 768;
-
- const ui = reactive({ isMobile: getIsMobile() });
-
- const updateEnv = debounce(() => {
- const isMobile = getIsMobile();
- if (ui.isMobile !== isMobile) {
- ui.isMobile = isMobile;
- }
- }, 15);
- window.addEventListener("resize", updateEnv);
- return ui;
-}
-
-function useUI() {
- const env = useEnv();
- return useState(env.ui);
-}
-
-
-//------------------------------------------------------------------------------
-// Components
-//------------------------------------------------------------------------------
-class Navbar extends owl.Component {
- static template = "Navbar";
-}
-
-class MobileSearchView extends Component {
- static template = "MobileSearchView";
-}
-
-class ControlPanel extends Component {
- static template = "ControlPanel";
- static components = { MobileSearchView };
- setup() {
- this.ui = useUI();
- }
-}
-
-class AdvancedComponent extends Component {
- static template = "AdvancedComponent";
-}
-
-class FormView extends Component {
- static template = "FormView";
- static components = { AdvancedComponent };
- setup() {
- this.ui = useUI();
- }
-}
-
-class Chatter extends Component {
- static template = "Chatter";
-
- setup() {
- this.messages = Array.from(Array(100).keys());
- }
-}
-
-class Root extends Component {
- static template = "Root";
- static components = { Navbar, ControlPanel, FormView, Chatter };
-
- setup() {
- this.ui = useUI();
- }
-}
-
-
-
-//------------------------------------------------------------------------------
-// Application Startup
-//------------------------------------------------------------------------------
-const env = {
- ui: createUI()
-};
-
-mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
-`;
-
-const RESPONSIVE_XML = /*xml*/`
-
- Navbar
-
-
-
Control Panel
-
-
-
-
-
-
-
- Mobile searchview
-
-
- This component is only created in desktop mode.
- Button!
-
-
-
-
-
-
-
-
-`;
-
-const RESPONSIVE_CSS = /*css*/`
-body {
- margin: 0;
-}
-
-.app {
- height: 100%;
- flex-direction: column;
-}
-
-.app.desktop {
- display: flex;
-}
-
-.navbar {
- flex: 0 0 30px;
- height: 30px;
- background-color: cadetblue;
- color: white;
- line-height: 30px;
-}
-
-.controlpanel {
- flex: 0 0 100px;
- height: 100px;
- background-color: #dddddd;
- padding: 8px;
-}
-
-.content-wrapper {
- flex: 1 1 auto;
- position: relative;
-}
-
-.content {
- display: flex;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-}
-
-.formview {
- overflow-y: auto;
- flex: 1 1 60%;
- min-height: 200px;
- padding: 8px;
-}
-
-.chatter {
- overflow-y: auto;
- flex: 1 1 40%;
- background-color: #eeeeee;
- color: #333333;
- padding: 8px;
-}
-`;
-
-const SLOTS = /*js*/ `
-
-// We show here how slots can be used to create generic components.
-// In this example, the Card component is basically only a container. It is not
-// aware of its content. It just knows where it should be (with t-slot).
-// The parent component define the content with t-set-slot.
-//
-// Note that the t-on-click event, defined in the Root template, is executed in
-// the context of the Root component, even though it is inside the Card component
-const { Component, useState, mount } = owl;
-
-class Card extends Component {
- static template = "Card";
-
- setup() {
- this.state = useState({ showContent: true });
- }
-
- toggleDisplay() {
- this.state.showContent = !this.state.showContent;
- }
-}
-
-class Counter extends Component {
- static template = "Counter";
-
- setup() {
- this.state = useState({val: 1});
- }
-
- inc() {
- this.state.val++;
- }
-}
-
-// Main root component
-class Root extends Component {
- static template = "Root"
- static components = { Card, Counter };
-
- setup() {
- this.state = useState({a: 1, b: 3});
- }
-
- inc(key, delta) {
- this.state[key] += delta;
- }
-}
-
-// Application setup
-mount(Root, document.body, { templates: TEMPLATES, dev: true});
-`;
-
-const SLOTS_XML = /*xml*/`
-
-
-
- Toggle
-
-
-
-
-
-
-
-
-
-
- Inc
-
-
-
-
- Content of card 1... [ ]
- Increment A
-
-
-
- Card 2... [ ]
-
-
- Decrement B
-
-
-
-`;
-
-const SLOTS_CSS = /*css*/ `
-.main {
- display: flex;
-}
-
-.card {
- display: flex;
- flex-direction: column;
- background-color: #eeeeee;
- width: 200px;
- height: 100px;
- margin: 10px;
- border: 1px solid gray;
-}
-
-.card.full {
- height: 100px;
-}
-
-.card.small {
- height: 25px;
-}
-
-.card-title {
- flex: 0 0 25px;
- font-weight: bold;
- background-color: darkcyan;
- color: white;
- padding: 2px;
-}
-
-.card-title button {
- float: right;
-}
-
-.card-content {
- flex: 1 1 auto;
- padding: 5px;
- border-top: 1px solid white;
-}
-
-.card-footer {
- border-top: 1px solid white;
-}`;
-
-const FORM = /*js*/`
-// This example illustrate how the t-model directive can be used to synchronize
-// data between html inputs (and select/textareas) and the state of a component.
-// Note that there are two controls with t-model="color": they are totally
-// synchronized.
-const { Component, useState, mount } = owl;
-
-class Form extends Component {
- static template = "Form";
-
- setup() {
- this.state = useState({
- text: "",
- othertext: "",
- number: 11,
- color: "",
- bool: false
- });
- }
-}
-
-// Application setup
-mount(Form, document.body, { templates: TEMPLATES, dev: true });
-`;
-
-const FORM_XML = /*xml*/ `
-
-
-
Form
-
- Text (immediate):
-
-
- Other text (lazy):
-
-
- Number:
-
-
- Boolean:
-
-
- Color, with a select:
-
- Select a color
- Red
- Blue
-
-
-
- Color, with radio buttons:
- Red
- Blue
-
-
-
State
-
Text:
-
Other Text:
-
Number:
-
Boolean: True False
-
Color:
-
-
-`;
-
-const WMS = /*js*/`
-// This example is slightly more complex than usual. We demonstrate
-// here a way to manage sub windows in Owl, declaratively. This is still just a
-// demonstration. Managing windows can be as complex as we want. For example,
-// we could implement the following features:
-// - resizing windows
-// - minimizing windows
-// - configuration options for windows to make a window non resizeable
-// - minimal width/height
-// - better heuristic for initial window position
-// - ...
-const { Component, useState, mount, useRef, reactive, useEnv, onMounted } = owl;
-
-// -----------------------------------------------------------------------------
-// Window manager code
-// -----------------------------------------------------------------------------
-
-class WindowManager {
- // contains all components with metadata
- static Windows = {};
- windows = {}; // mapping id => info
- nextId = 1;
-
- add(type) {
- const Comp = WindowManager.Windows[type];
- const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth));
- const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight));
- const id = this.nextId++;
- this.windows[id] = {
- id,
- title: Comp.defaultTitle,
- width: Comp.defaultWidth,
- height: Comp.defaultHeight,
- left,
- top,
- Component: Comp,
- };
- }
-
- close(id) {
- delete this.windows[id];
- }
-
- updatePosition(id, left, top) {
- const w = this.windows[id];
- w.left = left;
- w.top = top;
- }
-
- getWindows() {
- return Object.values(this.windows);
- }
-}
-
-function createWindowService() {
- return reactive(new WindowManager());
-}
-
-function useWindowService() {
- const env = useEnv();
- return useState(env.windowService);
-}
-
-// -----------------------------------------------------------------------------
-// Generic Window Component
-// -----------------------------------------------------------------------------
-
-class Window extends Component {
- static template = "Window";
- static nextZIndex = 1;
- zIndex = 0;
-
- setup() {
- this.windowService = useWindowService();
- this.root = useRef('root');
- onMounted(this.updateZIndex);
- }
-
- get style() {
- let { width, height, top, left } = this.props.info;
- return \`width: $\{width}px;height: $\{height}px;top:$\{top}px;left:$\{left}px;z-index:\${this.zIndex}\`;
- }
-
- close() {
- this.windowService.close(this.props.info.id);
- }
-
- startDragAndDrop(ev) {
- this.updateZIndex();
- const self = this;
- const root = this.root;
-
- const el = root.el;
- el.classList.add('dragging');
-
- const current = this.props.info;
- const offsetX = current.left - ev.pageX;
- const offsetY = current.top - ev.pageY;
- let left, top;
-
- window.addEventListener("mousemove", moveWindow);
- window.addEventListener("mouseup", stopDnD, { once: true });
-
- function moveWindow(ev) {
- left = Math.max(offsetX + ev.pageX, 0);
- top = Math.max(offsetY + ev.pageY, 0);
- el.style.left = \`\${left}px\`;
- el.style.top = \`\${top}px\`;
- }
- function stopDnD() {
- window.removeEventListener("mousemove", moveWindow);
- el.classList.remove('dragging');
-
- if (top !== undefined && left !== undefined) {
- self.windowService.updatePosition(current.id, left, top);
- }
- }
- }
-
- updateZIndex() {
- this.zIndex = Window.nextZIndex++;
- this.root.el.style['z-index'] = this.zIndex;
- }
-}
-
-// -----------------------------------------------------------------------------
-// Two concrete Window type implementations
-// -----------------------------------------------------------------------------
-
-class HelloWorld extends Component {
- static template = "HelloWorld";
- static defaultTitle = "Hello Owl!";
- static defaultWidth = 200;
- static defaultHeight = 100;
-}
-
-
-class Counter extends Component {
- static template = "Counter";
- static defaultTitle = "Click Counter";
- static defaultWidth = 300;
- static defaultHeight = 120;
-
- state = useState({ value: 0 });
-
- inc() {
- this.state.value++;
- }
-}
-
-// register window components
-WindowManager.Windows.Hello = HelloWorld;
-WindowManager.Windows.Counter = Counter;
-
-// -----------------------------------------------------------------------------
-// Window Container
-// -----------------------------------------------------------------------------
-
-class WindowContainer extends Component {
- static template = "WindowContainer";
- static components = { Window };
-
- setup() {
- this.windowService = useWindowService();
- }
-}
-
-// -----------------------------------------------------------------------------
-// Root Component
-// -----------------------------------------------------------------------------
-
-class Root extends Component {
- static template = "Root";
- static components = { WindowContainer };
-
- setup() {
- this.windowService = useWindowService();
- }
-
- addWindow(type) {
- this.windowService.add(type);
- }
-}
-
-// -----------------------------------------------------------------------------
-// Setup
-// -----------------------------------------------------------------------------
-
-const env = {
- windowService: createWindowService(),
-};
-
-mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
-`;
-
-const WMS_XML =/*xml*/`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Some content here...
-
-
-
- Click
-
-
-
-`;
-
-const WMS_CSS= /*css*/`
-body {
- margin: 0;
-}
-
-.app {
- width: 100%;
- height: 100%;
- display: grid;
- grid-template-rows: auto 50px;
-}
-
-.window-manager {
- position: relative;
- width: 100%;
- height: 100%;
- background-color: #eeeeee;
- overflow: hidden;
-}
-
-.menubar {
- background-color: #875a7b;
- color: white;
-}
-
-.menubar button {
- height: 40px;
- font-size: 18px;
- margin: 5px;
-}
-
-.window {
- display: grid;
- grid-template-rows: 30px auto;
- border: 1px solid gray;
- background-color: white;
- position: absolute;
- box-shadow: 1px 1px 2px 1px grey;
-}
-
-.window.dragging {
- opacity: 0.75;
-}
-
-.window .header {
- background-color: #875a7b;
- display: grid;
- grid-template-columns: auto 24px;
- color: white;
- line-height: 30px;
- padding-left: 5px;
- cursor: default;
- user-select: none;
-}
-
-.window .header .close {
- cursor: pointer;
- font-size: 22px;
- padding-left: 4px;
- padding-right: 4px;
- font-weight: bold;
-}
-
-.counter {
- font-size: 20px;
-}
-.counter button {
- width: 80px;
- height:40px;
- font-size: 20px;
-}`;
-
-const SFC =/*js*/`
-// This example illustrates how one can write Owl components with
-// inline templates.
-
-const { Component, useState, xml, css, mount } = owl;
-
-// Counter component
-class Counter extends Component {
- static template = xml\`
-
- Click! [ ]
- \`;
-
- state = useState({ value: 0})
-}
-
-// Root
-class Root extends Component {
- static template = xml\`
-
-
-
-
\`;
-
- static components = { Counter };
-}
-
-// Application setup
-mount(Root, document.body, { templates: TEMPLATES, dev: true});
-`;
-
-const BENCHMARK_JS = `
-const { Component, mount, xml, useState, onWillRender, onPatched} = owl;
-
-// -----------------------------------------------------------------------------
-// Data generation
-// -----------------------------------------------------------------------------
-
-let idCounter = 1;
-const adjectives = [
- "pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
- "quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
- "unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
-const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
-const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
-
-function _random (max) { return Math.round(Math.random() * 1000) % max; };
-
-function buildData(count) {
- const data = new Array(count);
- for (let i = 0; i < count; i++) {
- const label = \`\${adjectives[_random(adjectives.length)]} \${colours[_random(colours.length)]} \${nouns[_random(nouns.length)]}\`;
- data[i] = {
- id: idCounter++,
- label,
- };
- }
- return data;
-}
-
-// -----------------------------------------------------------------------------
-// Components
-// -----------------------------------------------------------------------------
-class Button extends Component {
- static template = xml\`
-
-
-
-
-
\`;
-}
-
-
-class Row extends Component {
- static template = xml\`
-
-
-
-
-
-
- [x]
-
-
-
-
- \`
-}
-
-class Root extends Component {
- static template = xml\`
- \`;
-
- static components = { Button, Row };
-
- setup() {
- this.state = useState({
- rows: [],
- selectedRowId: null
- });
- this.benchmarking = false;
- onPatched(() => {
- if (this.benchmarking) {
- this.stop();
- }
- });
- }
-
- start(descr) {
- this.benchmarking = \`[\${descr}]\`;
- console.time(this.benchmarking);
- }
- stop() {
- console.timeEnd(this.benchmarking);
- this.benchmarking = false;
- }
-
- run() {
- this.start('add1000');
- this.state.rows = buildData(1000);
- this.state.selectedRowId = null;
- }
-
- runLots() {
- this.start('add10_000');
- this.state.rows = buildData(10_000);
- this.state.selectedRowId = null;
- }
-
- add() {
- this.start('append1000');
- this.state.rows = this.state.rows.concat(buildData(1000));
- }
-
- update() {
- this.start('update1/10th');
- let index = 0;
- const rows = this.state.rows;
- while (index < rows.length) {
- rows[index].label = rows[index].label + " !!!";
- index += 10;
- }
- }
-
- clear() {
- this.start('clear');
- this.state.rows = [];
- this.state.selectedRowId = null;
- }
-
- swapRows() {
- this.start('swap');
- const rows = this.state.rows;
- if (rows.length > 998) {
- let tmp = rows[1];
- rows[1] = rows[998];
- rows[998] = tmp;
- }
- }
-
- selectRow(id) {
- this.start('select');
- this.state.selectedRowId = id;
- }
-
- removeRow(id) {
- this.start('remove1');
- const rows = this.state.rows;
- rows.splice(rows.findIndex(row => row.id === id), 1);
- }
-}
-
-
-// dev=false for benchmarking. we don't want to benchmark dev code!
-mount(Root, document.body, { templates: TEMPLATES, dev: false });
-
-`;
-
-const BENCHMARK_CSS = `
-tr.danger {
- font-weight: bold;
-}
-
-.remove:hover {
- font-weight: bold;
-}
-.remove {
- cursor: pointer;
-}
-`;
-
-export const SAMPLES = [
- {
- description: "Components",
- code: COMPONENTS,
- xml: COMPONENTS_XML,
- css: COMPONENTS_CSS
- },
- {
- description: "Form Input Bindings",
- code: FORM,
- xml: FORM_XML
- },
- {
- description: "Inline templates",
- code: SFC
- },
- {
- description: "Lifecycle demo",
- code: LIFECYCLE_DEMO,
- xml: LIFECYCLE_DEMO_XML,
- css: LIFECYCLE_CSS
- },
- {
- description: "Customized hook",
- code: HOOKS_DEMO,
- xml: HOOKS_DEMO_XML,
- css: HOOKS_CSS
- },
- {
- description: "Todo List App (with reactivity)",
- code: TODO_APP_REACTIVITY,
- css: TODO_APP_REACTIVITY_CSS,
- xml: TODO_APP_REACTIVITY_XML
- },
- {
- description: "Responsive app",
- code: RESPONSIVE,
- css: RESPONSIVE_CSS,
- xml: RESPONSIVE_XML
- },
- {
- description: "Slots And Generic Components",
- code: SLOTS,
- xml: SLOTS_XML,
- css: SLOTS_CSS
- },
- {
- description: "Window Management System",
- code: WMS,
- xml: WMS_XML,
- css: WMS_CSS,
- },
- {
- description: "Benchmark example",
- code: BENCHMARK_JS,
- css: BENCHMARK_CSS,
- },
-];
diff --git a/tools/playground/samples/benchmark/benchmark.css b/tools/playground/samples/benchmark/benchmark.css
new file mode 100644
index 00000000..67ba2ef7
--- /dev/null
+++ b/tools/playground/samples/benchmark/benchmark.css
@@ -0,0 +1,11 @@
+tr.danger {
+ font-weight: bold;
+}
+
+.remove:hover {
+ font-weight: bold;
+}
+.remove {
+ cursor: pointer;
+}
+
\ No newline at end of file
diff --git a/tools/playground/samples/benchmark/benchmark.js b/tools/playground/samples/benchmark/benchmark.js
new file mode 100644
index 00000000..a09689d8
--- /dev/null
+++ b/tools/playground/samples/benchmark/benchmark.js
@@ -0,0 +1,124 @@
+import { Component, mount, useState, onPatched} from "@odoo/owl";
+
+// -----------------------------------------------------------------------------
+// Data generation
+// -----------------------------------------------------------------------------
+
+let idCounter = 1;
+const adjectives = [
+ "pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
+ "quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
+ "unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
+const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
+const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
+
+function _random (max) { return Math.round(Math.random() * 1000) % max; };
+
+function buildData(count) {
+ const data = new Array(count);
+ for (let i = 0; i < count; i++) {
+ const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`;
+ data[i] = {
+ id: idCounter++,
+ label,
+ };
+ }
+ return data;
+}
+
+// -----------------------------------------------------------------------------
+// Components
+// -----------------------------------------------------------------------------
+class Button extends Component {
+ static template = "Button";
+}
+
+
+class Row extends Component {
+ static template = "Row";
+}
+
+class Root extends Component {
+ static template = "Root";
+ static components = { Button, Row };
+
+ setup() {
+ this.state = useState({
+ rows: [],
+ selectedRowId: null
+ });
+ this.benchmarking = false;
+ onPatched(() => {
+ if (this.benchmarking) {
+ this.stop();
+ }
+ });
+ }
+
+ start(descr) {
+ this.benchmarking = `[${descr}]`;
+ console.time(this.benchmarking);
+ }
+ stop() {
+ console.timeEnd(this.benchmarking);
+ this.benchmarking = false;
+ }
+
+ run() {
+ this.start('add1000');
+ this.state.rows = buildData(1000);
+ this.state.selectedRowId = null;
+ }
+
+ runLots() {
+ this.start('add10_000');
+ this.state.rows = buildData(10_000);
+ this.state.selectedRowId = null;
+ }
+
+ add() {
+ this.start('append1000');
+ this.state.rows = this.state.rows.concat(buildData(1000));
+ }
+
+ update() {
+ this.start('update1/10th');
+ let index = 0;
+ const rows = this.state.rows;
+ while (index < rows.length) {
+ rows[index].label = rows[index].label + " !!!";
+ index += 10;
+ }
+ }
+
+ clear() {
+ this.start('clear');
+ this.state.rows = [];
+ this.state.selectedRowId = null;
+ }
+
+ swapRows() {
+ this.start('swap');
+ const rows = this.state.rows;
+ if (rows.length > 998) {
+ let tmp = rows[1];
+ rows[1] = rows[998];
+ rows[998] = tmp;
+ }
+ }
+
+ selectRow(id) {
+ this.start('select');
+ this.state.selectedRowId = id;
+ }
+
+ removeRow(id) {
+ this.start('remove1');
+ const rows = this.state.rows;
+ rows.splice(rows.findIndex(row => row.id === id), 1);
+ }
+}
+
+
+// dev=false for benchmarking. we don't want to benchmark dev code!
+mount(Root, document.body, { templates: TEMPLATES, dev: false });
diff --git a/tools/playground/samples/benchmark/benchmark.xml b/tools/playground/samples/benchmark/benchmark.xml
new file mode 100644
index 00000000..58c19c42
--- /dev/null
+++ b/tools/playground/samples/benchmark/benchmark.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [x]
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/playground/samples/components/components.css b/tools/playground/samples/components/components.css
new file mode 100644
index 00000000..70eee24d
--- /dev/null
+++ b/tools/playground/samples/components/components.css
@@ -0,0 +1,10 @@
+.greeter {
+ font-size: 20px;
+ width: 300px;
+ height: 100px;
+ margin: 5px;
+ text-align: center;
+ line-height: 100px;
+ background-color: #eeeeee;
+ user-select: none;
+}
diff --git a/tools/playground/samples/components/components.js b/tools/playground/samples/components/components.js
new file mode 100644
index 00000000..0a1f3478
--- /dev/null
+++ b/tools/playground/samples/components/components.js
@@ -0,0 +1,26 @@
+// In this example, we show how components can be defined and created.
+import { Component, useState, mount } from "@odoo/owl";
+
+class Greeter extends Component {
+ static template = "Greeter";
+
+ setup() {
+ this.state = useState({ word: 'Hello' });
+ }
+
+ toggle() {
+ this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
+ }
+}
+
+// Main root component
+class Root extends Component {
+ static components = { Greeter };
+ static template = "Root"
+
+ setup() {
+ this.state = useState({ name: 'World'});
+ }
+}
+
+mount(Root, document.body, { templates: TEMPLATES, dev: true });
diff --git a/tools/playground/samples/components/components.xml b/tools/playground/samples/components/components.xml
new file mode 100644
index 00000000..1caaf12a
--- /dev/null
+++ b/tools/playground/samples/components/components.xml
@@ -0,0 +1,9 @@
+
+
+ ,
+
+
+
+
+
+
diff --git a/tools/playground/samples/custom_hooks/custom_hooks.css b/tools/playground/samples/custom_hooks/custom_hooks.css
new file mode 100644
index 00000000..311149f8
--- /dev/null
+++ b/tools/playground/samples/custom_hooks/custom_hooks.css
@@ -0,0 +1,6 @@
+
+button {
+ width: 120px;
+ height: 35px;
+ font-size: 16px;
+}
diff --git a/tools/playground/samples/custom_hooks/custom_hooks.js b/tools/playground/samples/custom_hooks/custom_hooks.js
new file mode 100644
index 00000000..52a72fda
--- /dev/null
+++ b/tools/playground/samples/custom_hooks/custom_hooks.js
@@ -0,0 +1,40 @@
+// In this example, we show how hooks can be used or defined.
+import { Component, mount, useState, onWillDestroy } from "@odoo/owl";
+
+// We define here a custom behaviour: this hook tracks the state of the mouse
+// position
+function useMouse() {
+ const position = useState({x:0, y: 0});
+
+ function update(e) {
+ position.x = e.clientX;
+ position.y = e.clientY;
+ }
+ window.addEventListener('mousemove', update);
+ onWillDestroy(() => {
+ window.removeEventListener('mousemove', update);
+ });
+
+ return position;
+}
+
+
+// Main root component
+class Root extends Component {
+ static template = "Root";
+
+ setup() {
+ // simple state hook (reactive object)
+ this.counter = useState({ value: 0 });
+
+ // this hooks is bound to the 'mouse' property.
+ this.mouse = useMouse();
+ }
+
+ increment() {
+ this.counter.value++;
+ }
+}
+
+// Application setup
+mount(Root, document.body, { templates: TEMPLATES, dev: true });
diff --git a/tools/playground/samples/custom_hooks/custom_hooks.xml b/tools/playground/samples/custom_hooks/custom_hooks.xml
new file mode 100644
index 00000000..1522cfe7
--- /dev/null
+++ b/tools/playground/samples/custom_hooks/custom_hooks.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/tools/playground/samples/form/form.js b/tools/playground/samples/form/form.js
new file mode 100644
index 00000000..345c118b
--- /dev/null
+++ b/tools/playground/samples/form/form.js
@@ -0,0 +1,22 @@
+// This example illustrate how the t-model directive can be used to synchronize
+// data between html inputs (and select/textareas) and the state of a component.
+// Note that there are two controls with t-model="color": they are totally
+// synchronized.
+import { Component, useState, mount } from "@odoo/owl";
+
+class Form extends Component {
+ static template = "Form";
+
+ setup() {
+ this.state = useState({
+ text: "",
+ othertext: "",
+ number: 11,
+ color: "",
+ bool: false
+ });
+ }
+}
+
+// Application setup
+mount(Form, document.body, { templates: TEMPLATES, dev: true });
diff --git a/tools/playground/samples/form/form.xml b/tools/playground/samples/form/form.xml
new file mode 100644
index 00000000..1693eb9e
--- /dev/null
+++ b/tools/playground/samples/form/form.xml
@@ -0,0 +1,37 @@
+
+
+
Form
+
+ Text (immediate):
+
+
+ Other text (lazy):
+
+
+ Number:
+
+
+ Boolean:
+
+
+ Color, with a select:
+
+ Select a color
+ Red
+ Blue
+
+
+
+ Color, with radio buttons:
+ Red
+ Blue
+
+
+
State
+
Text:
+
Other Text:
+
Number:
+
Boolean: True False
+
Color:
+
+
diff --git a/tools/playground/samples/jsconfig.json b/tools/playground/samples/jsconfig.json
new file mode 100644
index 00000000..2d56cbb6
--- /dev/null
+++ b/tools/playground/samples/jsconfig.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "paths": {
+ "owl": ["../../../src/index.ts"],
+ },
+ }
+}
\ No newline at end of file
diff --git a/tools/playground/samples/lifecycle_demo/lifecycle_demo.css b/tools/playground/samples/lifecycle_demo/lifecycle_demo.css
new file mode 100644
index 00000000..589db82b
--- /dev/null
+++ b/tools/playground/samples/lifecycle_demo/lifecycle_demo.css
@@ -0,0 +1,11 @@
+button {
+ font-size: 18px;
+ margin: 5px;
+}
+
+.demo {
+ margin: 10px;
+ padding: 10px;
+ background-color: #dddddd;
+ width: 250px;
+}
diff --git a/tools/playground/samples/lifecycle_demo/lifecycle_demo.js b/tools/playground/samples/lifecycle_demo/lifecycle_demo.js
new file mode 100644
index 00000000..ee2eec87
--- /dev/null
+++ b/tools/playground/samples/lifecycle_demo/lifecycle_demo.js
@@ -0,0 +1,68 @@
+// This example shows all the possible lifecycle hooks
+//
+// The root component controls a sub component (DemoComponent). It logs all its lifecycle
+// methods in the console. Try modifying its state by clicking on it, or by
+// clicking on the two main buttons, and look into the console to see what
+// happens.
+import {
+ Component,
+ useState,
+ mount,
+ useComponent,
+ onWillStart,
+ onMounted,
+ onWillUnmount,
+ onWillUpdateProps,
+ onPatched,
+ onWillPatch,
+ onWillRender,
+ onRendered,
+ onWillDestroy,
+} from "@odoo/owl";
+
+function useLogLifecycle() {
+ const component = useComponent();
+ const name = component.constructor.name;
+ onWillStart(() => console.log(`${name}:willStart`));
+ onMounted(() => console.log(`${name}:mounted`));
+ onWillUpdateProps(() => console.log(`${name}:willUpdateProps`));
+ onWillRender(() => console.log(`${name}:willRender`));
+ onRendered(() => console.log(`${name}:rendered`));
+ onWillPatch(() => console.log(`${name}:willPatch`));
+ onPatched(() => console.log(`${name}:patched`));
+ onWillUnmount(() => console.log(`${name}:willUnmount`));
+ onWillDestroy(() => console.log(`${name}:willDestroy`));
+}
+
+class DemoComponent extends Component {
+ static template = "DemoComponent";
+
+ setup() {
+ useLogLifecycle();
+ this.state = useState({ n: 0 });
+ }
+ increment() {
+ this.state.n++;
+ }
+}
+
+class Root extends Component {
+ static template = "Root";
+ static components = { DemoComponent };
+
+ setup() {
+ useLogLifecycle();
+ this.state = useState({ n: 0, flag: true });
+ }
+
+ increment() {
+ this.state.n++;
+ }
+
+ toggleSubComponent() {
+ this.state.flag = !this.state.flag;
+ }
+}
+
+mount(Root, document.body, { templates: TEMPLATES, dev: true });
+
\ No newline at end of file
diff --git a/tools/playground/samples/lifecycle_demo/lifecycle_demo.xml b/tools/playground/samples/lifecycle_demo/lifecycle_demo.xml
new file mode 100644
index 00000000..33b0c843
--- /dev/null
+++ b/tools/playground/samples/lifecycle_demo/lifecycle_demo.xml
@@ -0,0 +1,15 @@
+
+
+
Demo Sub Component
+
(click on me to update me)
+
Props: , State: .
+
+
+
+
Increment Parent State
+
Toggle SubComponent
+
+
+
+
+
diff --git a/tools/playground/samples/responsive_app/responsive_app.css b/tools/playground/samples/responsive_app/responsive_app.css
new file mode 100644
index 00000000..e43e8e5e
--- /dev/null
+++ b/tools/playground/samples/responsive_app/responsive_app.css
@@ -0,0 +1,56 @@
+body {
+ margin: 0;
+}
+
+.app {
+ height: 100%;
+ flex-direction: column;
+}
+
+.app.desktop {
+ display: flex;
+}
+
+.navbar {
+ flex: 0 0 30px;
+ height: 30px;
+ background-color: cadetblue;
+ color: white;
+ line-height: 30px;
+}
+
+.controlpanel {
+ flex: 0 0 100px;
+ height: 100px;
+ background-color: #dddddd;
+ padding: 8px;
+}
+
+.content-wrapper {
+ flex: 1 1 auto;
+ position: relative;
+}
+
+.content {
+ display: flex;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+
+.formview {
+ overflow-y: auto;
+ flex: 1 1 60%;
+ min-height: 200px;
+ padding: 8px;
+}
+
+.chatter {
+ overflow-y: auto;
+ flex: 1 1 40%;
+ background-color: #eeeeee;
+ color: #333333;
+ padding: 8px;
+}
diff --git a/tools/playground/samples/responsive_app/responsive_app.js b/tools/playground/samples/responsive_app/responsive_app.js
new file mode 100644
index 00000000..7a2134cc
--- /dev/null
+++ b/tools/playground/samples/responsive_app/responsive_app.js
@@ -0,0 +1,118 @@
+// In this example, we show how one can design an application that is responsive:
+// its UI is different in mobile mode or in desktop mode.
+//
+// The main idea is to have a "isMobile" key in the environment, then listen
+// to resize events and update the env if needed. Then, the whole interface
+// will be updated, creating and destroying components as needed.
+//
+// To see this in action, try resizing the window. The application will switch
+// to mobile mode whenever it has less than 768px.
+import { Component, useState, mount, reactive, useEnv } from "@odoo/owl";
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+function debounce(func, wait, immediate) {
+ let timeout;
+ return function () {
+ const context = this;
+ const args = arguments;
+ function later() {
+ timeout = null;
+ if (!immediate) {
+ func.apply(context, args);
+ }
+ }
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ func.apply(context, args);
+ }
+ };
+}
+
+//------------------------------------------------------------------------------
+// Responsive hook
+//------------------------------------------------------------------------------
+
+function createUI() {
+ const getIsMobile = () => window.innerWidth <= 768;
+
+ const ui = reactive({ isMobile: getIsMobile() });
+
+ const updateEnv = debounce(() => {
+ const isMobile = getIsMobile();
+ if (ui.isMobile !== isMobile) {
+ ui.isMobile = isMobile;
+ }
+ }, 15);
+ window.addEventListener("resize", updateEnv);
+ return ui;
+}
+
+function useUI() {
+ const env = useEnv();
+ return useState(env.ui);
+}
+
+
+//------------------------------------------------------------------------------
+// Components
+//------------------------------------------------------------------------------
+class Navbar extends Component {
+ static template = "Navbar";
+}
+
+class MobileSearchView extends Component {
+ static template = "MobileSearchView";
+}
+
+class ControlPanel extends Component {
+ static template = "ControlPanel";
+ static components = { MobileSearchView };
+ setup() {
+ this.ui = useUI();
+ }
+}
+
+class AdvancedComponent extends Component {
+ static template = "AdvancedComponent";
+}
+
+class FormView extends Component {
+ static template = "FormView";
+ static components = { AdvancedComponent };
+ setup() {
+ this.ui = useUI();
+ }
+}
+
+class Chatter extends Component {
+ static template = "Chatter";
+
+ setup() {
+ this.messages = Array.from(Array(100).keys());
+ }
+}
+
+class Root extends Component {
+ static template = "Root";
+ static components = { Navbar, ControlPanel, FormView, Chatter };
+
+ setup() {
+ this.ui = useUI();
+ }
+}
+
+
+
+//------------------------------------------------------------------------------
+// Application Startup
+//------------------------------------------------------------------------------
+const env = {
+ ui: createUI()
+};
+
+mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
diff --git a/tools/playground/samples/responsive_app/responsive_app.xml b/tools/playground/samples/responsive_app/responsive_app.xml
new file mode 100644
index 00000000..ccb2da9b
--- /dev/null
+++ b/tools/playground/samples/responsive_app/responsive_app.xml
@@ -0,0 +1,42 @@
+
+ Navbar
+
+
+
Control Panel
+
+
+
+
+
+
+
+ Mobile searchview
+
+
+ This component is only created in desktop mode.
+ Button!
+
+
+
+
+
+
+
+
diff --git a/tools/playground/samples/single_file_component/single_file_component.js b/tools/playground/samples/single_file_component/single_file_component.js
new file mode 100644
index 00000000..20ba9e5e
--- /dev/null
+++ b/tools/playground/samples/single_file_component/single_file_component.js
@@ -0,0 +1,28 @@
+// This example illustrates how one can write Owl components with
+// inline templates.
+
+import { Component, useState, xml, mount } from "@odoo/owl";
+
+// Counter component
+class Counter extends Component {
+ static template = xml`
+
+ Click! [ ]
+ `;
+
+ state = useState({ value: 0 })
+}
+
+// Root
+class Root extends Component {
+ static template = xml`
+
+
+
+
`;
+
+ static components = { Counter };
+}
+
+// Application setup
+mount(Root, document.body, { templates: TEMPLATES, dev: true});
diff --git a/tools/playground/samples/slots/slots.css b/tools/playground/samples/slots/slots.css
new file mode 100644
index 00000000..786e938c
--- /dev/null
+++ b/tools/playground/samples/slots/slots.css
@@ -0,0 +1,43 @@
+.main {
+ display: flex;
+}
+
+.card {
+ display: flex;
+ flex-direction: column;
+ background-color: #eeeeee;
+ width: 200px;
+ height: 100px;
+ margin: 10px;
+ border: 1px solid gray;
+}
+
+.card.full {
+ height: 100px;
+}
+
+.card.small {
+ height: 25px;
+}
+
+.card-title {
+ flex: 0 0 25px;
+ font-weight: bold;
+ background-color: darkcyan;
+ color: white;
+ padding: 2px;
+}
+
+.card-title button {
+ float: right;
+}
+
+.card-content {
+ flex: 1 1 auto;
+ padding: 5px;
+ border-top: 1px solid white;
+}
+
+.card-footer {
+ border-top: 1px solid white;
+}
diff --git a/tools/playground/samples/slots/slots.js b/tools/playground/samples/slots/slots.js
new file mode 100644
index 00000000..58eac769
--- /dev/null
+++ b/tools/playground/samples/slots/slots.js
@@ -0,0 +1,49 @@
+// We show here how slots can be used to create generic components.
+// In this example, the Card component is basically only a container. It is not
+// aware of its content. It just knows where it should be (with t-slot).
+// The parent component define the content with t-set-slot.
+//
+// Note that the t-on-click event, defined in the Root template, is executed in
+// the context of the Root component, even though it is inside the Card component
+import { Component, useState, mount } from "@odoo/owl";
+
+class Card extends Component {
+ static template = "Card";
+
+ setup() {
+ this.state = useState({ showContent: true });
+ }
+
+ toggleDisplay() {
+ this.state.showContent = !this.state.showContent;
+ }
+}
+
+class Counter extends Component {
+ static template = "Counter";
+
+ setup() {
+ this.state = useState({val: 1});
+ }
+
+ inc() {
+ this.state.val++;
+ }
+}
+
+// Main root component
+class Root extends Component {
+ static template = "Root"
+ static components = { Card, Counter };
+
+ setup() {
+ this.state = useState({a: 1, b: 3});
+ }
+
+ inc(key, delta) {
+ this.state[key] += delta;
+ }
+}
+
+// Application setup
+mount(Root, document.body, { templates: TEMPLATES, dev: true});
diff --git a/tools/playground/samples/slots/slots.xml b/tools/playground/samples/slots/slots.xml
new file mode 100644
index 00000000..34227137
--- /dev/null
+++ b/tools/playground/samples/slots/slots.xml
@@ -0,0 +1,33 @@
+
+
+
+ Toggle
+
+
+
+
+
+
+
+
+
+
+ Inc
+
+
+
+
+ Content of card 1... [ ]
+ Increment A
+
+
+
+ Card 2... [ ]
+
+
+ Decrement B
+
+
+
diff --git a/tools/playground/samples/todo_app/todo_app.css b/tools/playground/samples/todo_app/todo_app.css
new file mode 100644
index 00000000..bdfd38a8
--- /dev/null
+++ b/tools/playground/samples/todo_app/todo_app.css
@@ -0,0 +1,379 @@
+html,body {
+ margin: 0;
+ padding: 0;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #f5f5f5;
+ color: #4d4d4d;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-weight: 300;
+}
+
+:focus {
+ outline: 0;
+}
+
+.hidden {
+ display: none;
+}
+
+.todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+.todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::-moz-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp h1 {
+ position: absolute;
+ top: -155px;
+ width: 100%;
+ font-size: 100px;
+ font-weight: 100;
+ text-align: center;
+ color: rgba(175, 47, 47, 0.15);
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
+
+.new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ border: 0;
+ color: inherit;
+ padding: 6px;
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.new-todo {
+ padding: 16px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
+}
+
+.main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+}
+
+.toggle-all {
+ width: 1px;
+ height: 1px;
+ border: none; /* Mobile Safari */
+ opacity: 0;
+ position: absolute;
+ right: 100%;
+ bottom: 100%;
+}
+
+.toggle-all + label {
+ width: 60px;
+ height: 34px;
+ font-size: 0;
+ position: absolute;
+ top: -52px;
+ left: -13px;
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+
+.toggle-all + label:before {
+ content: "❯";
+ font-size: 22px;
+ color: #e6e6e6;
+ padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked + label:before {
+ color: #737373;
+}
+
+.todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+ border-bottom: none;
+}
+
+.todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+.todo-list li.editing .edit {
+ display: block;
+ width: calc(100% - 43px);
+ padding: 12px 16px;
+ margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+ display: none;
+}
+
+.todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ /* auto, since non-WebKit browsers doesn't support input styling */
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.todo-list li .toggle {
+ opacity: 0;
+}
+
+.todo-list li .toggle + label {
+ /*
+ Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
+ IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
+ */
+ background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center left;
+}
+
+.todo-list li .toggle:checked + label {
+ background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
+}
+
+.todo-list li label {
+ word-break: break-all;
+ padding: 15px 15px 15px 60px;
+ display: block;
+ line-height: 1.2;
+ transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+ color: #d9d9d9;
+ text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #cc9a9a;
+ margin-bottom: 11px;
+ transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover {
+ color: #af5b5e;
+}
+
+.todo-list li .destroy:after {
+ content: "×";
+}
+
+.todo-list li:hover .destroy {
+ display: block;
+}
+
+.todo-list li .edit {
+ display: none;
+}
+
+.todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
+
+.footer {
+ color: #777;
+ padding: 10px 15px;
+ height: 20px;
+ text-align: center;
+ border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+ content: "";
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
+ 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
+ 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+ float: left;
+ text-align: left;
+}
+
+.todo-count strong {
+ font-weight: 300;
+}
+
+.filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+.filters li {
+ display: inline;
+}
+
+.filters li a {
+ color: inherit;
+ margin: 3px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+}
+
+.filters li a:hover {
+ border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+ border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed,
+html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.clear-completed:hover {
+ text-decoration: underline;
+}
+
+.info {
+ margin: 65px auto 0;
+ color: #bfbfbf;
+ font-size: 10px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+.info p {
+ line-height: 1;
+}
+
+.info a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 400;
+}
+
+.info a:hover {
+ text-decoration: underline;
+}
+
+/*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+ .toggle-all,
+ .todo-list li .toggle {
+ background: none;
+ }
+
+ .todo-list li .toggle {
+ height: 40px;
+ }
+}
+
+@media (max-width: 430px) {
+ .footer {
+ height: 50px;
+ }
+
+ .filters {
+ bottom: 10px;
+ }
+}
+
+.filters a {
+ cursor: pointer;
+}
diff --git a/tools/playground/samples/todo_app/todo_app.js b/tools/playground/samples/todo_app/todo_app.js
new file mode 100644
index 00000000..bc602d09
--- /dev/null
+++ b/tools/playground/samples/todo_app/todo_app.js
@@ -0,0 +1,182 @@
+// This example is an implementation of the TodoList application, from the
+// www.todomvc.com project. This is a non trivial application with some
+// interesting user interactions. It uses the local storage for persistence.
+//
+// In this implementation, we use the owl reactivity mechanism.
+import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl";
+
+//------------------------------------------------------------------------------
+// Constants, helpers
+//------------------------------------------------------------------------------
+const ENTER_KEY = 13;
+const ESC_KEY = 27;
+
+function useAutofocus(name) {
+ let ref = useRef(name);
+ useEffect(el => el && el.focus(), () => [ref.el]);
+}
+
+function useStore() {
+ const env = useEnv();
+ return useState(env.store);
+}
+
+//------------------------------------------------------------------------------
+// Task store
+//------------------------------------------------------------------------------
+class TaskList {
+ constructor(tasks) {
+ this.tasks = tasks || [];
+ const taskIds = this.tasks.map((t) => t.id);
+ this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
+ }
+
+ addTask(text) {
+ text = text.trim();
+ if (text) {
+ const task = {
+ id: this.nextId++,
+ text: text,
+ isCompleted: false,
+ };
+ this.tasks.push(task);
+ }
+ }
+
+ toggleTask(task) {
+ task.isCompleted = !task.isCompleted;
+ }
+
+ toggleTask(id) {
+ const task = this.tasks.find(t => t.id === id);
+ task.isCompleted = !task.isCompleted;
+ }
+
+ toggleAll(value) {
+ for (let task of this.tasks) {
+ task.isCompleted = value;
+ }
+ }
+
+ clearCompleted() {
+ const tasks = this.tasks.filter(t => t.isCompleted);
+ for (let task of tasks) {
+ this.deleteTask(task);
+ }
+ }
+
+ deleteTask(id) {
+ const index = this.tasks.findIndex((t) => t.id === id);
+ this.tasks.splice(index, 1);
+ }
+
+ updateTask(id, text) {
+ const value = text.trim();
+ if (!value) {
+ this.deleteTask(id);
+ } else {
+ const task = this.tasks.find(t => t.id === id);
+ task.text = value;
+ }
+ }
+}
+
+function createTaskStore() {
+ const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
+ const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
+ const taskStore = reactive(new TaskList(initialTasks), saveTasks);
+ saveTasks();
+ return taskStore;
+}
+
+//------------------------------------------------------------------------------
+// Todo
+//------------------------------------------------------------------------------
+class Todo extends Component {
+ static template = "Todo";
+
+ setup() {
+ useAutofocus("input");
+ this.store = useStore();
+ this.state = useState({
+ isEditing: false
+ });
+ }
+
+ handleKeyup(ev) {
+ if (ev.keyCode === ENTER_KEY) {
+ this.updateText(ev.target.value);
+ }
+ if (ev.keyCode === ESC_KEY) {
+ ev.target.value = this.props.text;
+ this.state.isEditing = false;
+ }
+ }
+
+ handleBlur(ev) {
+ this.updateText(ev.target.value);
+ }
+
+ updateText(text) {
+ this.store.updateTask(this.props.id, text);
+ this.state.isEditing = false;
+ }
+}
+
+//------------------------------------------------------------------------------
+// TodoList
+//------------------------------------------------------------------------------
+class TodoList extends Component {
+ static template = "TodoList";
+ static components = { Todo };
+
+ setup() {
+ this.store = useStore();
+ this.state = useState({ filter: "all" });
+ }
+
+ get displayedTasks() {
+ const tasks = this.store.tasks;
+ switch (this.state.filter) {
+ case "active":
+ return tasks.filter((t) => !t.isCompleted);
+ case "completed":
+ return tasks.filter((t) => t.isCompleted);
+ case "all":
+ return tasks;
+ }
+ }
+
+ get allChecked() {
+ return this.store.tasks.every(todo => todo.isCompleted);
+ }
+
+ get remaining() {
+ return this.store.tasks.filter(todo => !todo.isCompleted).length;
+ }
+
+ get remainingText() {
+ const items = this.remaining < 2 ? "item" : "items";
+ return ` ${items} left`;
+ }
+
+ addTodo(ev) {
+ if (ev.keyCode === ENTER_KEY) {
+ const text = ev.target.value;
+ if (text.trim()) {
+ this.store.addTask(text);
+ }
+ ev.target.value = "";
+ }
+ }
+
+ setFilter(filter) {
+ this.state.filter = filter;
+ }
+}
+
+//------------------------------------------------------------------------------
+// App Initialization
+//------------------------------------------------------------------------------
+const env = { store: createTaskStore() };
+mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
diff --git a/tools/playground/samples/todo_app/todo_app.xml b/tools/playground/samples/todo_app/todo_app.xml
new file mode 100644
index 00000000..1ed4d9f7
--- /dev/null
+++ b/tools/playground/samples/todo_app/todo_app.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/playground/samples/window_manager/window_manager.css b/tools/playground/samples/window_manager/window_manager.css
new file mode 100644
index 00000000..92ee4226
--- /dev/null
+++ b/tools/playground/samples/window_manager/window_manager.css
@@ -0,0 +1,70 @@
+body {
+ margin: 0;
+}
+
+.app {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ grid-template-rows: auto 50px;
+}
+
+.window-manager {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background-color: #eeeeee;
+ overflow: hidden;
+}
+
+.menubar {
+ background-color: #875a7b;
+ color: white;
+}
+
+.menubar button {
+ height: 40px;
+ font-size: 18px;
+ margin: 5px;
+}
+
+.window {
+ display: grid;
+ grid-template-rows: 30px auto;
+ border: 1px solid gray;
+ background-color: white;
+ position: absolute;
+ box-shadow: 1px 1px 2px 1px grey;
+}
+
+.window.dragging {
+ opacity: 0.75;
+}
+
+.window .header {
+ background-color: #875a7b;
+ display: grid;
+ grid-template-columns: auto 24px;
+ color: white;
+ line-height: 30px;
+ padding-left: 5px;
+ cursor: default;
+ user-select: none;
+}
+
+.window .header .close {
+ cursor: pointer;
+ font-size: 22px;
+ padding-left: 4px;
+ padding-right: 4px;
+ font-weight: bold;
+}
+
+.counter {
+ font-size: 20px;
+}
+.counter button {
+ width: 80px;
+ height:40px;
+ font-size: 20px;
+}
diff --git a/tools/playground/samples/window_manager/window_manager.js b/tools/playground/samples/window_manager/window_manager.js
new file mode 100644
index 00000000..9783beea
--- /dev/null
+++ b/tools/playground/samples/window_manager/window_manager.js
@@ -0,0 +1,192 @@
+// This example is slightly more complex than usual. We demonstrate
+// here a way to manage sub windows in Owl, declaratively. This is still just a
+// demonstration. Managing windows can be as complex as we want. For example,
+// we could implement the following features:
+// - resizing windows
+// - minimizing windows
+// - configuration options for windows to make a window non resizeable
+// - minimal width/height
+// - better heuristic for initial window position
+// - ...
+import { Component, useState, mount, useRef, reactive, useEnv, onMounted } from "@odoo/owl";
+
+// -----------------------------------------------------------------------------
+// Window manager code
+// -----------------------------------------------------------------------------
+
+class WindowManager {
+ // contains all components with metadata
+ static Windows = {};
+ windows = {}; // mapping id => info
+ nextId = 1;
+
+ add(type) {
+ const Comp = WindowManager.Windows[type];
+ const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth));
+ const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight));
+ const id = this.nextId++;
+ this.windows[id] = {
+ id,
+ title: Comp.defaultTitle,
+ width: Comp.defaultWidth,
+ height: Comp.defaultHeight,
+ left,
+ top,
+ Component: Comp,
+ };
+ }
+
+ close(id) {
+ delete this.windows[id];
+ }
+
+ updatePosition(id, left, top) {
+ const w = this.windows[id];
+ w.left = left;
+ w.top = top;
+ }
+
+ getWindows() {
+ return Object.values(this.windows);
+ }
+}
+
+function createWindowService() {
+ return reactive(new WindowManager());
+}
+
+function useWindowService() {
+ const env = useEnv();
+ return useState(env.windowService);
+}
+
+// -----------------------------------------------------------------------------
+// Generic Window Component
+// -----------------------------------------------------------------------------
+
+class Window extends Component {
+ static template = "Window";
+ static nextZIndex = 1;
+ zIndex = 0;
+
+ setup() {
+ this.windowService = useWindowService();
+ this.root = useRef('root');
+ onMounted(this.updateZIndex);
+ }
+
+ get style() {
+ let { width, height, top, left } = this.props.info;
+ return `width: ${width}px;height: ${height}px;top:${top}px;left:${left}px;z-index:${this.zIndex}`;
+ }
+
+ close() {
+ this.windowService.close(this.props.info.id);
+ }
+
+ startDragAndDrop(ev) {
+ this.updateZIndex();
+ const self = this;
+ const root = this.root;
+
+ const el = root.el;
+ el.classList.add('dragging');
+
+ const current = this.props.info;
+ const offsetX = current.left - ev.pageX;
+ const offsetY = current.top - ev.pageY;
+ let left, top;
+
+ window.addEventListener("mousemove", moveWindow);
+ window.addEventListener("mouseup", stopDnD, { once: true });
+
+ function moveWindow(ev) {
+ left = Math.max(offsetX + ev.pageX, 0);
+ top = Math.max(offsetY + ev.pageY, 0);
+ el.style.left = `${left}px`;
+ el.style.top = `${top}px`;
+ }
+ function stopDnD() {
+ window.removeEventListener("mousemove", moveWindow);
+ el.classList.remove('dragging');
+
+ if (top !== undefined && left !== undefined) {
+ self.windowService.updatePosition(current.id, left, top);
+ }
+ }
+ }
+
+ updateZIndex() {
+ this.zIndex = Window.nextZIndex++;
+ this.root.el.style['z-index'] = this.zIndex;
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Two concrete Window type implementations
+// -----------------------------------------------------------------------------
+
+class HelloWorld extends Component {
+ static template = "HelloWorld";
+ static defaultTitle = "Hello Owl!";
+ static defaultWidth = 200;
+ static defaultHeight = 100;
+}
+
+
+class Counter extends Component {
+ static template = "Counter";
+ static defaultTitle = "Click Counter";
+ static defaultWidth = 300;
+ static defaultHeight = 120;
+
+ state = useState({ value: 0 });
+
+ inc() {
+ this.state.value++;
+ }
+}
+
+// register window components
+WindowManager.Windows.Hello = HelloWorld;
+WindowManager.Windows.Counter = Counter;
+
+// -----------------------------------------------------------------------------
+// Window Container
+// -----------------------------------------------------------------------------
+
+class WindowContainer extends Component {
+ static template = "WindowContainer";
+ static components = { Window };
+
+ setup() {
+ this.windowService = useWindowService();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Root Component
+// -----------------------------------------------------------------------------
+
+class Root extends Component {
+ static template = "Root";
+ static components = { WindowContainer };
+
+ setup() {
+ this.windowService = useWindowService();
+ }
+
+ addWindow(type) {
+ this.windowService.add(type);
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Setup
+// -----------------------------------------------------------------------------
+
+const env = {
+ windowService: createWindowService(),
+};
+
+mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
diff --git a/tools/playground/samples/window_manager/window_manager.xml b/tools/playground/samples/window_manager/window_manager.xml
new file mode 100644
index 00000000..e5a1e242
--- /dev/null
+++ b/tools/playground/samples/window_manager/window_manager.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Some content here...
+
+
+
+ Click
+
+
+
diff --git a/tools/playground/standalone_app/app.py b/tools/playground/standalone_app/app.py
new file mode 100644
index 00000000..42505fcd
--- /dev/null
+++ b/tools/playground/standalone_app/app.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+
+import threading
+import time
+
+from http.server import SimpleHTTPRequestHandler, HTTPServer
+
+httpd = None
+def start_server():
+ global httpd
+ SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
+ httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
+ httpd.serve_forever()
+
+url = 'http://127.0.0.1:3600'
+
+if __name__ == "__main__":
+ print("Owl Application")
+ print("---------------")
+ print("Server running on: {}".format(url))
+ threading.Thread(target=start_server, daemon=True).start()
+
+ while True:
+ try:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ httpd.server_close()
+ quit(0)
diff --git a/tools/playground/standalone_app/index.html b/tools/playground/standalone_app/index.html
new file mode 100644
index 00000000..88e25ec9
--- /dev/null
+++ b/tools/playground/standalone_app/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+ OWL App
+
+
+
+
+
+
+
+
+This app requires a web server, simply opening the html file using your browser will not work.
+You can run a simple web server with one of the following:
+- if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
+- if you have node/npm installed, run 'npx serve' in the app folder
+- if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
+
+
+
+
\ No newline at end of file
diff --git a/tools/playground/templates.xml b/tools/playground/templates.xml
index 602d5e81..2054da23 100644
--- a/tools/playground/templates.xml
+++ b/tools/playground/templates.xml
@@ -1,5 +1,5 @@
-
+
-
+
@@ -37,19 +37,19 @@
css="state.css"
xml="state.xml"
resizeable="true"
- onUpdatePanelHeight="updatePanelHeight"
+ updatePanelHeight.bind="updatePanelHeight"
updateCode="updateCode"/>
-
+
🦉 Odoo Web Library 🦉
v
-
Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
- For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
+
+ Note: these examples make use of recent features of Javascript, and require a recent browser to work without a transpilation step! Among other things, it uses class fields and import maps. If you encounter issues, make sure your browser is up to date.
diff --git a/tools/playground/utils.js b/tools/playground/utils.js
index 96f8dd4e..88b20cb8 100644
--- a/tools/playground/utils.js
+++ b/tools/playground/utils.js
@@ -18,12 +18,6 @@ export function debounce(func, wait, immediate) {
};
}
-export function useBus(bus, evName, cb) {
- bus.addEventListener(evName, cb);
- owl.onWillUnmount(() => {
- bus.removeEventListener(evName, cb);
- });
-}
const loadedScripts = {};
diff --git a/tools/server.py b/tools/server.py
index 8aba2c2a..5b9a51de 100644
--- a/tools/server.py
+++ b/tools/server.py
@@ -15,8 +15,8 @@ URL = 'http://{0}:{1}/tools/playground'.format(HOST, PORT)
# we want to update the playground.
class OWLHandler(SimpleHTTPRequestHandler):
def do_GET(self):
- if self.path == '/tools/owl.js':
- self.path = '/dist/owl.iife.js'
+ if self.path == '/tools/owl.js' or self.path == '/tools/playground/owl.js':
+ self.path = '/dist/owl.es.js'
return SimpleHTTPRequestHandler.do_GET(self)
def end_headers(self):