2021-11-17 18:41:01 +01:00
const COMPONENTS = /*js*/ `
// In this example, we show how components can be defined and created.
2020-10-27 14:41:09 +01:00
const { Component, useState, mount } = owl;
2019-05-03 10:33:22 +02:00
2019-09-24 14:06:53 +02:00
class Greeter extends Component {
2021-12-21 15:09:02 +01:00
static template = "Greeter";
2020-10-30 09:33:49 +01:00
setup() {
2019-11-28 10:18:46 +01:00
this.state = useState({ word: 'Hello' });
}
2019-05-03 10:33:22 +02:00
2019-06-28 14:28:35 +02:00
toggle() {
2019-10-01 21:37:36 +02:00
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
2019-05-03 11:59:30 +02:00
}
2019-05-03 10:33:22 +02:00
}
2019-06-20 09:42:33 +02:00
// Main root component
2021-12-21 15:09:02 +01:00
class Root extends Component {
static components = { Greeter };
static template = "Root"
setup() {
this.state = useState({ name: 'World'});
}
2019-05-03 10:33:22 +02:00
}
2022-02-15 09:03:25 +01:00
// dev=false for benchmarking. we don't want to benchmark dev code!
mount(Root, document.body, { templates: TEMPLATES, dev: false }); ` ;
2019-05-03 10:33:22 +02:00
2021-11-17 18:41:01 +01:00
const COMPONENTS _XML = /*xml*/ `
<templates>
2019-06-28 14:28:35 +02:00
<div t-name="Greeter" class="greeter" t-on-click="toggle">
<t t-esc="state.word"/>, <t t-esc="props.name"/>
</div>
2019-05-27 13:33:26 +02:00
2021-12-21 15:09:02 +01:00
<t t-name="Root">
2019-06-28 14:28:35 +02:00
<Greeter name="state.name"/>
2021-12-21 15:09:02 +01:00
</t>
2019-06-28 14:28:35 +02:00
</templates>
` ;
2019-05-03 10:33:22 +02:00
2021-11-17 18:41:01 +01:00
const COMPONENTS _CSS = /*css*/ `
.greeter {
2019-05-27 13:33:26 +02:00
font-size: 20px;
2019-06-28 14:28:35 +02:00
width: 300px;
height: 100px;
2019-05-27 13:33:26 +02:00
margin: 5px;
2019-06-28 14:28:35 +02:00
text-align: center;
line-height: 100px;
background-color: #eeeeee;
user-select: none;
2019-05-03 10:33:22 +02:00
} ` ;
2021-11-17 18:41:01 +01:00
const LIFECYCLE _DEMO = /*js*/ `
// This example shows all the possible lifecycle hooks
2019-06-14 16:01:24 +02:00
//
2019-06-20 09:42:33 +02:00
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
2019-06-14 16:01:24 +02:00
// 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.
2021-12-21 15:09:02 +01:00
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 \` ));
}
2019-06-14 16:01:24 +02:00
2019-09-24 14:06:53 +02:00
class DemoComponent extends Component {
2021-12-21 15:09:02 +01:00
static template = "DemoComponent";
2020-10-30 09:33:49 +01:00
setup() {
2021-12-21 15:09:02 +01:00
useLogLifecycle();
2019-09-24 14:06:53 +02:00
this.state = useState({ n: 0 });
2019-05-03 11:59:30 +02:00
}
increment() {
this.state.n++;
}
2019-05-03 10:33:22 +02:00
}
2021-12-21 15:09:02 +01:00
class Root extends Component {
static template = "Root";
static components = { DemoComponent };
2020-10-30 09:33:49 +01:00
setup() {
2021-12-21 15:09:02 +01:00
useLogLifecycle();
2019-11-28 10:18:46 +01:00
this.state = useState({ n: 0, flag: true });
}
2019-06-14 16:01:24 +02:00
2019-05-03 11:59:30 +02:00
increment() {
this.state.n++;
}
2019-06-14 16:01:24 +02:00
2019-06-20 09:42:33 +02:00
toggleSubComponent() {
2019-05-03 11:59:30 +02:00
this.state.flag = !this.state.flag;
}
2019-05-03 10:33:22 +02:00
}
2021-12-21 15:09:02 +01:00
mount(Root, document.body, { templates: TEMPLATES, dev: true });
2019-05-03 10:33:22 +02:00
` ;
2021-11-17 18:41:01 +01:00
const LIFECYCLE _DEMO _XML = /*xml*/ `
<templates>
2019-06-20 09:42:33 +02:00
<div t-name="DemoComponent" t-on-click="increment" class="demo">
<div>Demo Sub Component</div>
2019-06-14 16:01:24 +02:00
<div>(click on me to update me)</div>
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
</div>
2021-12-21 15:09:02 +01:00
<div t-name="Root">
2019-06-14 16:01:24 +02:00
<button t-on-click="increment">Increment Parent State</button>
2019-06-20 09:42:33 +02:00
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
2019-06-14 16:01:24 +02:00
<div t-if="state.flag">
2019-06-20 09:42:33 +02:00
<DemoComponent n="state.n"/>
2019-05-14 15:58:30 +02:00
</div>
2019-06-14 16:01:24 +02:00
</div>
2019-05-03 10:33:22 +02:00
</templates> ` ;
2021-11-17 18:41:01 +01:00
const LIFECYCLE _CSS = /*css*/ `
button {
2019-06-14 16:01:24 +02:00
font-size: 18px;
margin: 5px;
}
.demo {
margin: 10px;
padding: 10px;
background-color: #dddddd;
width: 250px;
} ` ;
2021-11-17 18:41:01 +01:00
const HOOKS _DEMO = /*js*/ `
// In this example, we show how hooks can be used or defined.
2021-12-21 15:09:02 +01:00
const { Component, mount, useState, onWillDestroy } = owl;
2019-09-24 14:06:53 +02:00
// 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) {
2021-12-21 15:09:02 +01:00
position.x = e.clientX;
position.y = e.clientY;
2019-09-24 14:06:53 +02:00
}
2021-12-21 15:09:02 +01:00
window.addEventListener('mousemove', update);
onWillDestroy(() => {
2019-09-24 14:06:53 +02:00
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
2021-12-21 15:09:02 +01:00
class Root extends Component {
static template = "Root";
2020-10-30 09:33:49 +01:00
setup() {
2019-11-28 10:18:46 +01:00
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
2019-09-24 14:06:53 +02:00
2019-11-28 10:18:46 +01:00
// this hooks is bound to the 'mouse' property.
this.mouse = useMouse();
}
2019-09-24 14:06:53 +02:00
increment() {
this.counter.value++;
}
}
// Application setup
2021-12-21 15:09:02 +01:00
mount(Root, document.body, { templates: TEMPLATES, dev: true });
2019-09-24 14:06:53 +02:00
` ;
2021-11-17 18:41:01 +01:00
const HOOKS _DEMO _XML = /*xml*/ `
<templates>
2021-12-21 15:09:02 +01:00
<div t-name="Root">
2019-09-24 14:06:53 +02:00
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
` ;
2021-11-17 18:41:01 +01:00
const HOOKS _CSS = /*css*/ ` button {
2019-09-24 14:06:53 +02:00
width: 120px;
height: 35px;
font-size: 16px;
} ` ;
2021-11-17 18:41:01 +01:00
const TODO _APP _REACTIVITY = /*js*/ `
2021-12-21 15:09:02 +01:00
2021-11-17 18:41:01 +01:00
// This example is an implementation of the TodoList application, from the
2019-06-14 16:01:24 +02:00
// www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence.
//
2021-11-17 18:41:01 +01:00
// In this implementation, we use the owl reactivity mechanism.
2021-12-21 15:09:02 +01:00
const { Component, useState, mount, useRef, onPatched, onMounted, reactive, useEnv, useEffect } = owl;
2019-06-14 16:01:24 +02:00
2019-10-10 17:17:42 +02:00
//------------------------------------------------------------------------------
// Constants, helpers
//------------------------------------------------------------------------------
2019-06-14 16:01:24 +02:00
const ENTER_KEY = 13;
2019-05-03 10:33:22 +02:00
const ESC_KEY = 27;
2019-08-27 13:58:52 +02:00
const LOCALSTORAGE_KEY = "todomvc";
2019-05-03 10:33:22 +02:00
2019-10-10 17:17:42 +02:00
function useAutofocus(name) {
2021-12-21 15:09:02 +01:00
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;
}
2019-10-10 17:17:42 +02:00
}
}
2021-12-21 15:09:02 +01:00
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;
}
2019-05-03 10:33:22 +02:00
//------------------------------------------------------------------------------
2021-12-21 15:09:02 +01:00
// Todo
2019-05-03 10:33:22 +02:00
//------------------------------------------------------------------------------
2021-12-21 15:09:02 +01:00
class Todo extends Component {
static template = "Todo";
2020-10-30 09:33:49 +01:00
setup() {
2019-10-10 17:17:42 +02:00
useAutofocus("input");
2021-12-21 15:09:02 +01:00
this.store = useStore();
this.state = useState({
isEditing: false
});
2019-05-03 11:59:30 +02:00
}
2019-05-03 10:33:22 +02:00
2019-05-03 11:59:30 +02:00
handleKeyup(ev) {
if (ev.keyCode === ENTER_KEY) {
2021-12-21 15:09:02 +01:00
this.updateText(ev.target.value);
2019-05-03 11:59:30 +02:00
}
if (ev.keyCode === ESC_KEY) {
2021-12-21 15:09:02 +01:00
ev.target.value = this.props.text;
2019-05-03 11:59:30 +02:00
this.state.isEditing = false;
}
2019-05-03 10:33:22 +02:00
}
2019-05-03 11:59:30 +02:00
handleBlur(ev) {
2021-12-21 15:09:02 +01:00
this.updateText(ev.target.value);
2019-05-03 10:33:22 +02:00
}
2021-12-21 15:09:02 +01:00
updateText(text) {
this.store.updateTask(this.props.id, text);
2019-10-10 17:17:42 +02:00
this.state.isEditing = false;
2019-05-03 10:33:22 +02:00
}
}
//------------------------------------------------------------------------------
2021-12-21 15:09:02 +01:00
// TodoList
2019-05-03 10:33:22 +02:00
//------------------------------------------------------------------------------
2021-12-21 15:09:02 +01:00
class TodoList extends Component {
static template = "TodoList";
static components = { Todo };
2020-10-30 09:33:49 +01:00
setup() {
2021-12-21 15:09:02 +01:00
this.store = useStore();
2019-11-28 10:18:46 +01:00
this.state = useState({ filter: "all" });
2021-11-17 18:41:01 +01:00
}
2021-12-21 15:09:02 +01:00
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;
}
2019-05-03 10:33:22 +02:00
}
2021-12-21 15:09:02 +01:00
2019-05-03 11:59:30 +02:00
get allChecked() {
2021-12-21 15:09:02 +01:00
return this.store.tasks.every(todo => todo.isCompleted);
2019-05-03 11:59:30 +02:00
}
2019-05-03 10:33:22 +02:00
2019-05-03 11:59:30 +02:00
get remaining() {
2021-12-21 15:09:02 +01:00
return this.store.tasks.filter(todo => !todo.isCompleted).length;
2019-05-03 11:59:30 +02:00
}
2019-05-03 10:33:22 +02:00
2019-05-03 11:59:30 +02:00
get remainingText() {
const items = this.remaining < 2 ? "item" : "items";
return \` \$ {items} left \` ;
}
2019-05-03 10:33:22 +02:00
2019-05-03 11:59:30 +02:00
addTodo(ev) {
if (ev.keyCode === ENTER_KEY) {
2021-12-21 15:09:02 +01:00
const text = ev.target.value;
if (text.trim()) {
this.store.addTask(text);
2019-05-03 11:59:30 +02:00
}
ev.target.value = "";
}
2019-05-03 10:33:22 +02:00
}
2019-05-03 11:59:30 +02:00
setFilter(filter) {
this.state.filter = filter;
}
2019-05-03 10:33:22 +02:00
}
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
2021-12-21 15:09:02 +01:00
const env = { store: createTaskStore() };
mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
2019-05-03 10:33:22 +02:00
` ;
2021-11-17 18:41:01 +01:00
const TODO _APP _REACTIVITY _XML = /*xml*/ `
<templates>
2021-12-21 15:09:02 +01:00
<section t-name="TodoList" class="todoapp">
2019-05-03 10:33:22 +02:00
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header>
2021-12-21 15:09:02 +01:00
<section class="main" t-if="store.tasks.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="() => store.toggleAll(!allChecked)"/>
2019-05-03 10:33:22 +02:00
<label for="toggle-all"></label>
<ul class="todo-list">
2021-12-21 15:09:02 +01:00
<t t-foreach="displayedTasks" t-as="todo" t-key="todo.id">
<Todo id="todo.id" isCompleted="todo.isCompleted" text="todo.text"/>
2019-05-03 10:33:22 +02:00
</t>
</ul>
</section>
2021-12-21 15:09:02 +01:00
<footer class="footer" t-if="store.tasks.length">
2019-05-03 10:33:22 +02:00
<span class="todo-count">
<strong>
<t t-esc="remaining"/>
</strong>
<t t-esc="remainingText"/>
</span>
<ul class="filters">
<li>
2021-12-21 15:09:02 +01:00
<a t-on-click="() => this.setFilter('all')" t-att-class="{selected: state.filter === 'all'}">All</a>
2019-05-03 10:33:22 +02:00
</li>
<li>
2021-12-21 15:09:02 +01:00
<a t-on-click="() => this.setFilter('active')" t-att-class="{selected: state.filter === 'active'}">Active</a>
2019-05-03 10:33:22 +02:00
</li>
<li>
2021-12-21 15:09:02 +01:00
<a t-on-click="() => this.setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
2019-05-03 10:33:22 +02:00
</li>
</ul>
2021-12-21 15:09:02 +01:00
<button class="clear-completed" t-if="store.tasks.length gt remaining" t-on-click="() => store.clearCompleted()">
2019-05-03 10:33:22 +02:00
Clear completed
</button>
</footer>
</section>
2021-12-21 15:09:02 +01:00
<li t-name="Todo" class="todo" t-att-class="{completed: props.isCompleted, editing: state.isEditing}">
2019-05-03 10:33:22 +02:00
<div class="view">
2021-12-21 15:09:02 +01:00
<input class="toggle" type="checkbox" t-on-change="() => store.toggleTask(props.id)" t-att-checked="props.completed"/>
2021-11-17 18:41:01 +01:00
<label t-on-dblclick="() => state.isEditing = true">
2021-12-21 15:09:02 +01:00
<t t-esc="props.text"/>
2019-05-03 10:33:22 +02:00
</label>
2021-12-21 15:09:02 +01:00
<button class="destroy" t-on-click="() => store.deleteTask(props.id)"></button>
2019-05-03 10:33:22 +02:00
</div>
2021-12-21 15:09:02 +01:00
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.text" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
2019-05-03 10:33:22 +02:00
</li>
</templates> ` ;
2021-11-17 18:41:01 +01:00
const TODO _APP _REACTIVITY _CSS = /*css*/ `
html,body {
2019-05-03 10:33:22 +02:00
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;
}
` ;
2021-11-17 18:41:01 +01:00
const RESPONSIVE = /*js*/ `
2021-12-21 15:09:02 +01:00
// 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.
2019-06-14 16:01:24 +02:00
//
// 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
2019-06-20 09:42:33 +02:00
// will be updated, creating and destroying components as needed.
2019-06-14 16:01:24 +02:00
//
// To see this in action, try resizing the window. The application will switch
// to mobile mode whenever it has less than 768px.
2021-12-21 15:09:02 +01:00
const { Component, useState, mount, reactive, useEnv } = owl;
2021-11-17 18:41:01 +01:00
2019-06-14 16:01:24 +02:00
//------------------------------------------------------------------------------
2021-12-21 15:09:02 +01:00
// Helpers
2019-06-14 16:01:24 +02:00
//------------------------------------------------------------------------------
2019-05-07 16:26:20 +02:00
2021-12-21 15:09:02 +01:00
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);
}
2019-11-28 10:18:46 +01:00
}
2021-12-21 15:09:02 +01:00
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
2021-11-17 18:41:01 +01:00
}
2021-12-21 15:09:02 +01:00
};
2021-11-17 18:41:01 +01:00
}
2019-05-03 10:33:22 +02:00
2019-06-14 16:01:24 +02:00
//------------------------------------------------------------------------------
2021-12-21 15:09:02 +01:00
// Responsive hook
2019-06-14 16:01:24 +02:00
//------------------------------------------------------------------------------
2021-11-17 18:41:01 +01:00
2021-12-21 15:09:02 +01:00
function createUI() {
const getIsMobile = () => window.innerWidth <= 768;
2021-11-17 18:41:01 +01:00
2021-12-21 15:09:02 +01:00
const ui = reactive({ isMobile: getIsMobile() });
2021-11-17 18:41:01 +01:00
const updateEnv = debounce(() => {
2021-12-21 15:09:02 +01:00
const isMobile = getIsMobile();
if (ui.isMobile !== isMobile) {
ui.isMobile = isMobile;
2019-06-22 14:44:09 +02:00
}
}, 15);
window.addEventListener("resize", updateEnv);
2021-12-21 15:09:02 +01:00
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();
}
2019-05-03 10:33:22 +02:00
}
2021-12-21 15:09:02 +01:00
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();
}
2021-11-17 18:41:01 +01:00
}
2021-12-21 15:09:02 +01:00
2019-06-22 14:44:09 +02:00
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
2021-12-21 15:09:02 +01:00
const env = {
ui: createUI()
};
2019-06-14 16:01:24 +02:00
2021-12-21 15:09:02 +01:00
mount(Root, document.body, { templates: TEMPLATES, env });
2019-05-14 15:58:30 +02:00
` ;
2019-05-03 10:33:22 +02:00
2021-11-17 18:41:01 +01:00
const RESPONSIVE _XML = /*xml*/ `
<templates>
2019-06-14 16:01:24 +02:00
<div t-name="Navbar" class="navbar">Navbar</div>
2019-05-07 16:26:20 +02:00
2019-06-14 16:01:24 +02:00
<div t-name="ControlPanel" class="controlpanel">
<h2>Control Panel</h2>
2021-12-21 15:09:02 +01:00
<MobileSearchView t-if="ui.isMobile" />
2019-06-14 16:01:24 +02:00
</div>
2019-05-07 16:26:20 +02:00
2019-06-14 16:01:24 +02:00
<div t-name="FormView" class="formview">
<h2>Form View</h2>
2021-12-21 15:09:02 +01:00
<AdvancedComponent t-if="!ui.isMobile" />
2019-06-14 16:01:24 +02:00
</div>
2019-05-07 16:26:20 +02:00
2019-06-14 16:01:24 +02:00
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
2021-11-17 18:41:01 +01:00
<t t-foreach="messages" t-as="item" t-key="item"><div>Message <t t-esc="item"/></div></t>
2019-06-14 16:01:24 +02:00
</div>
2019-05-07 16:26:20 +02:00
2019-06-14 16:01:24 +02:00
<div t-name="MobileSearchView">Mobile searchview</div>
2019-05-07 16:26:20 +02:00
2019-06-20 09:42:33 +02:00
<div t-name="AdvancedComponent">
This component is only created in desktop mode.
2019-06-14 16:01:24 +02:00
<button>Button!</button>
</div>
2019-05-07 16:26:20 +02:00
2020-01-08 15:05:19 +01:00
<t t-name="maincontent">
<FormView />
<Chatter />
</t>
2021-12-21 15:09:02 +01:00
<div t-name="Root" class="app" t-att-class="{mobile: ui.isMobile, desktop: !ui.isMobile}">
2019-06-18 09:24:52 +02:00
<Navbar/>
<ControlPanel/>
2021-12-21 15:09:02 +01:00
<div class="content-wrapper" t-if="!ui.isMobile">
2019-06-14 16:01:24 +02:00
<div class="content">
2020-01-08 15:05:19 +01:00
<t t-call="maincontent"/>
2019-06-14 16:01:24 +02:00
</div>
2019-05-03 10:33:22 +02:00
</div>
2019-12-11 11:01:54 +01:00
<t t-else="">
2020-01-08 15:05:19 +01:00
<t t-call="maincontent"/>
2019-06-14 16:01:24 +02:00
</t>
</div>
2019-05-03 10:33:22 +02:00
</templates>
` ;
2021-11-17 18:41:01 +01:00
const RESPONSIVE _CSS = /*css*/ `
body {
2019-05-07 16:26:20 +02:00
margin: 0;
2019-05-03 10:33:22 +02:00
}
2019-05-07 16:26:20 +02:00
.app {
height: 100%;
flex-direction: column;
2019-05-03 10:33:22 +02:00
}
2019-05-07 16:26:20 +02:00
.app.desktop {
display: flex;
2019-05-03 10:33:22 +02:00
}
2019-06-14 16:01:24 +02:00
2019-05-07 16:26:20 +02:00
.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;
}
` ;
2019-05-03 10:33:22 +02:00
2021-11-17 18:41:01 +01:00
const SLOTS = /*js*/ `
2021-12-21 15:09:02 +01:00
2021-11-17 18:41:01 +01:00
// We show here how slots can be used to create generic components.
2019-06-14 16:01:24 +02:00
// 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).
2020-04-06 15:29:54 +02:00
// The parent component define the content with t-set-slot.
2019-06-14 16:01:24 +02:00
//
2021-12-21 15:09:02 +01:00
// 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
2020-10-27 14:41:09 +01:00
const { Component, useState, mount } = owl;
2019-06-11 15:02:10 +02:00
2019-09-24 14:06:53 +02:00
class Card extends Component {
2021-12-21 15:09:02 +01:00
static template = "Card";
setup() {
this.state = useState({ showContent: true });
}
2019-06-11 15:02:10 +02:00
2021-12-21 15:09:02 +01:00
toggleDisplay() {
this.state.showContent = !this.state.showContent;
}
2019-06-11 15:02:10 +02:00
}
2019-09-24 14:06:53 +02:00
class Counter extends Component {
2021-12-21 15:09:02 +01:00
static template = "Counter";
setup() {
this.state = useState({val: 1});
}
2019-06-11 15:02:10 +02:00
2021-12-21 15:09:02 +01:00
inc() {
this.state.val++;
}
2019-06-11 15:02:10 +02:00
}
2019-06-20 09:42:33 +02:00
// Main root component
2021-12-21 15:09:02 +01:00
class Root extends Component {
static template = "Root"
static components = { Card, Counter };
setup() {
this.state = useState({a: 1, b: 3});
}
2019-06-11 15:02:10 +02:00
2021-12-21 15:09:02 +01:00
inc(key, delta) {
this.state[key] += delta;
}
2019-06-11 15:02:10 +02:00
}
// Application setup
2021-12-21 15:09:02 +01:00
mount(Root, document.body, { templates: TEMPLATES, dev: true});
2020-10-27 14:41:09 +01:00
` ;
2019-06-11 15:02:10 +02:00
2021-11-17 18:41:01 +01:00
const SLOTS _XML = /*xml*/ `
<templates>
2019-06-14 16:01:24 +02:00
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
2019-06-11 15:02:10 +02:00
<div class="card-title">
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
</div>
2019-06-14 16:01:24 +02:00
<t t-if="state.showContent">
2019-06-11 15:02:10 +02:00
<div class="card-content" >
<t t-slot="content"/>
</div>
<div class="card-footer">
<t t-slot="footer"/>
</div>
</t>
</div>
<div t-name="Counter">
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
</div>
2021-12-21 15:09:02 +01:00
<div t-name="Root" class="main">
2019-06-18 09:24:52 +02:00
<Card title="'Title card A'">
2020-04-06 15:29:54 +02:00
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
2021-12-21 15:09:02 +01:00
<t t-set-slot="footer"><button t-on-click="() => this.inc('a', 1)">Increment A</button></t>
2019-06-18 09:24:52 +02:00
</Card>
<Card title="'Title card B'">
2020-04-06 15:29:54 +02:00
<t t-set-slot="content">
2019-06-11 15:02:10 +02:00
<div>Card 2... [<t t-esc="state.b"/>]</div>
2019-06-18 09:24:52 +02:00
<Counter />
2019-06-23 09:03:18 +02:00
</t>
2021-12-21 15:09:02 +01:00
<t t-set-slot="footer"><button t-on-click="() => this.inc('b', -1)">Decrement B</button></t>
2019-06-18 09:24:52 +02:00
</Card>
2019-06-11 15:02:10 +02:00
</div>
2021-12-21 15:09:02 +01:00
</templates>
` ;
2019-06-11 15:02:10 +02:00
2021-11-17 18:41:01 +01:00
const SLOTS _CSS = /*css*/ `
.main {
2019-06-11 15:02:10 +02:00
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;
} ` ;
2021-11-17 18:41:01 +01:00
const FORM = /*js*/ `
// This example illustrate how the t-model directive can be used to synchronize
2019-06-14 16:01:24 +02:00
// 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.
2020-10-27 14:41:09 +01:00
const { Component, useState, mount } = owl;
2019-05-03 10:33:22 +02:00
2019-09-24 14:06:53 +02:00
class Form extends Component {
2021-12-21 15:09:02 +01:00
static template = "Form";
setup() {
this.state = useState({
text: "",
othertext: "",
number: 11,
color: "",
bool: false
});
}
2019-06-13 16:27:12 +02:00
}
2019-06-14 16:01:24 +02:00
// Application setup
2021-12-21 15:09:02 +01:00
mount(Form, document.body, { templates: TEMPLATES, dev: true });
2019-06-13 16:27:12 +02:00
` ;
2021-11-17 18:41:01 +01:00
const FORM _XML = /*xml*/ `
<templates>
2019-06-14 16:01:24 +02:00
<div t-name="Form">
<h1>Form</h1>
<div>
2019-09-24 14:06:53 +02:00
Text (immediate): <input t-model="state.text"/>
2019-06-14 16:01:24 +02:00
</div>
<div>
2019-09-24 14:06:53 +02:00
Other text (lazy): <input t-model.lazy="state.othertext"/>
2019-06-14 16:01:24 +02:00
</div>
<div>
2019-09-24 14:06:53 +02:00
Number: <input t-model.number="state.number"/>
2019-06-14 16:01:24 +02:00
</div>
<div>
2019-09-24 14:06:53 +02:00
Boolean: <input type="checkbox" t-model="state.bool"/>
2019-06-13 16:27:12 +02:00
</div>
2019-06-14 16:01:24 +02:00
<div>
Color, with a select:
2019-09-24 14:06:53 +02:00
<select t-model="state.color">
2019-06-14 16:01:24 +02:00
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Color, with radio buttons:
2019-09-24 14:06:53 +02:00
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
2019-06-14 16:01:24 +02:00
</div>
<hr/>
<h1>State</h1>
<div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div>
2019-12-11 11:01:54 +01:00
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
2019-06-14 16:01:24 +02:00
<div>Color: <t t-esc="state.color"/></div>
</div>
2019-06-13 16:27:12 +02:00
</templates>
` ;
2021-11-17 18:41:01 +01:00
const WMS = /*js*/ `
// This example is slightly more complex than usual. We demonstrate
2019-08-27 13:47:01 +02:00
// 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
// - ...
2021-12-21 15:09:02 +01:00
const { Component, useState, mount, useRef, reactive, useEnv, onMounted } = owl;
// -----------------------------------------------------------------------------
// Window manager code
// -----------------------------------------------------------------------------
class WindowManager {
// contains all components with metadata
static Windows = {};
2022-01-20 10:04:56 +01:00
windows = {}; // mapping id => info
2021-12-21 15:09:02 +01:00
nextId = 1;
add(type) {
const Comp = WindowManager.Windows[type];
2022-01-20 10:04:56 +01:00
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,
2021-12-21 15:09:02 +01:00
title: Comp.defaultTitle,
width: Comp.defaultWidth,
height: Comp.defaultHeight,
left,
2022-01-20 10:04:56 +01:00
top,
2021-12-21 15:09:02 +01:00
Component: Comp,
2022-01-20 10:04:56 +01:00
};
2019-11-28 10:18:46 +01:00
}
2021-12-21 15:09:02 +01:00
close(id) {
2022-01-20 10:04:56 +01:00
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);
2019-08-27 13:47:01 +02:00
}
}
2021-12-21 15:09:02 +01:00
function createWindowService() {
return reactive(new WindowManager());
}
function useWindowService() {
const env = useEnv();
return useState(env.windowService);
}
// -----------------------------------------------------------------------------
// Generic Window Component
// -----------------------------------------------------------------------------
2019-08-27 13:47:01 +02:00
2019-09-24 14:06:53 +02:00
class Window extends Component {
2021-12-21 15:09:02 +01:00
static template = "Window";
2022-01-20 10:04:56 +01:00
static nextZIndex = 1;
zIndex = 0;
2021-12-21 15:09:02 +01:00
setup() {
this.windowService = useWindowService();
this.root = useRef('root');
onMounted(this.updateZIndex);
}
2019-09-26 21:56:01 +02:00
2019-08-27 13:47:01 +02:00
get style() {
2021-12-21 15:09:02 +01:00
let { width, height, top, left } = this.props.info;
2022-01-20 10:04:56 +01:00
return \` width: $ \{ width}px;height: $ \{ height}px;top: $ \{ top}px;left: $ \{ left}px;z-index: \$ {this.zIndex} \` ;
2019-08-27 13:47:01 +02:00
}
2019-09-26 21:56:01 +02:00
2019-08-27 13:47:01 +02:00
close() {
2021-12-21 15:09:02 +01:00
this.windowService.close(this.props.info.id);
2019-08-27 13:47:01 +02:00
}
2019-09-26 21:56:01 +02:00
2019-08-27 13:47:01 +02:00
startDragAndDrop(ev) {
this.updateZIndex();
2022-01-20 10:04:56 +01:00
const self = this;
2021-12-21 15:09:02 +01:00
const root = this.root;
2022-01-20 10:04:56 +01:00
2021-12-21 15:09:02 +01:00
const el = root.el;
el.classList.add('dragging');
2021-11-17 18:41:01 +01:00
const current = this.props.info;
const offsetX = current.left - ev.pageX;
const offsetY = current.top - ev.pageY;
2019-08-27 13:47:01 +02:00
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);
2021-12-21 15:09:02 +01:00
el.classList.remove('dragging');
2021-11-17 18:41:01 +01:00
if (top !== undefined && left !== undefined) {
2022-01-20 10:04:56 +01:00
self.windowService.updatePosition(current.id, left, top);
2021-11-17 18:41:01 +01:00
}
2019-08-27 13:47:01 +02:00
}
}
2019-09-26 21:56:01 +02:00
2019-08-27 13:47:01 +02:00
updateZIndex() {
2022-01-20 10:04:56 +01:00
this.zIndex = Window.nextZIndex++;
this.root.el.style['z-index'] = this.zIndex;
2019-08-27 13:47:01 +02:00
}
}
2021-12-21 15:09:02 +01:00
// -----------------------------------------------------------------------------
// Two concrete Window type implementations
// -----------------------------------------------------------------------------
2019-08-27 13:47:01 +02:00
2021-12-21 15:09:02 +01:00
class HelloWorld extends Component {
static template = "HelloWorld";
static defaultTitle = "Hello Owl!";
static defaultWidth = 200;
static defaultHeight = 100;
2019-08-27 13:47:01 +02:00
}
2021-12-21 15:09:02 +01:00
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++;
2019-08-27 13:47:01 +02:00
}
2021-12-21 15:09:02 +01:00
}
2019-08-27 13:47:01 +02:00
2021-12-21 15:09:02 +01:00
// register window components
WindowManager.Windows.Hello = HelloWorld;
WindowManager.Windows.Counter = Counter;
2021-11-17 18:41:01 +01:00
2021-12-21 15:09:02 +01:00
// -----------------------------------------------------------------------------
// Window Container
// -----------------------------------------------------------------------------
2021-11-17 18:41:01 +01:00
2021-12-21 15:09:02 +01:00
class WindowContainer extends Component {
static template = "WindowContainer";
static components = { Window };
setup() {
this.windowService = useWindowService();
}
}
2021-11-17 18:41:01 +01:00
2021-12-21 15:09:02 +01:00
// -----------------------------------------------------------------------------
// Root Component
// -----------------------------------------------------------------------------
2021-11-17 18:41:01 +01:00
2021-12-21 15:09:02 +01:00
class Root extends Component {
static template = "Root";
static components = { WindowContainer };
setup() {
this.windowService = useWindowService();
}
addWindow(type) {
this.windowService.add(type);
}
2021-11-17 18:41:01 +01:00
}
2021-12-21 15:09:02 +01:00
// -----------------------------------------------------------------------------
// Setup
// -----------------------------------------------------------------------------
2021-11-17 18:41:01 +01:00
const env = {
2021-12-21 15:09:02 +01:00
windowService: createWindowService(),
2021-11-17 18:41:01 +01:00
};
2021-12-21 15:09:02 +01:00
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
2019-08-27 13:47:01 +02:00
` ;
2021-11-17 18:41:01 +01:00
const WMS _XML = /*xml*/ `
<templates>
2021-12-21 15:09:02 +01:00
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex" t-ref="root">
2019-08-27 13:47:01 +02:00
<div class="header">
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
2019-11-22 12:07:16 +01:00
<span class="close" t-on-click.stop="close">× </span>
2019-08-27 13:47:01 +02:00
</div>
<t t-slot="default"/>
</div>
2021-12-21 15:09:02 +01:00
<div t-name="WindowContainer" class="window-manager">
2022-01-20 10:04:56 +01:00
<Window t-foreach="windowService.getWindows()" t-as="w" t-key="w.id" info="w">
2021-12-21 15:09:02 +01:00
<t t-component="w.Component"/>
2019-08-27 13:47:01 +02:00
</Window>
</div>
2021-12-21 15:09:02 +01:00
<div t-name="Root" class="app">
<WindowContainer/>
2019-08-27 13:47:01 +02:00
<div class="menubar">
2021-12-21 15:09:02 +01:00
<button t-on-click="() => this.addWindow('Hello')">Say Hello</button>
<button t-on-click="() => this.addWindow('Counter')">Counter</button>
2019-08-27 13:47:01 +02:00
</div>
</div>
<div t-name="HelloWorld">
2021-12-21 15:09:02 +01:00
Some content here...
2019-08-27 13:47:01 +02:00
</div>
<div t-name="Counter" class="counter">
2021-12-21 15:09:02 +01:00
<button t-on-click="inc">Click</button>
2019-08-27 13:47:01 +02:00
<span><t t-esc="state.value"/></span>
</div>
</templates>
` ;
2021-11-17 18:41:01 +01:00
const WMS _CSS = /*css*/ `
body {
2019-08-27 13:47:01 +02:00
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;
} ` ;
2021-11-17 18:41:01 +01:00
const SFC = /*js*/ `
2021-12-21 15:09:02 +01:00
// This example illustrates how one can write Owl components with
// inline templates.
2020-01-18 10:00:18 +01:00
2021-11-17 18:41:01 +01:00
const { Component, useState, xml, css, mount } = owl;
2020-01-18 10:00:18 +01:00
// Counter component
class Counter extends Component {
2021-12-21 15:09:02 +01:00
static template = xml \`
<button t-on-click="() => state.value++">
Click! [<t t-esc="state.value"/>]
</button> \` ;
2020-01-18 10:00:18 +01:00
state = useState({ value: 0})
}
2021-12-21 15:09:02 +01:00
// Root
class Root extends Component {
static template = xml \`
<div>
<Counter/>
<Counter/>
</div> \` ;
static components = { Counter };
}
2020-01-18 10:00:18 +01:00
// Application setup
2021-12-21 15:09:02 +01:00
mount(Root, document.body, { templates: TEMPLATES, dev: true});
2020-01-18 10:00:18 +01:00
` ;
2022-02-14 13:04:13 +01:00
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 \`
<div class='col-sm-6 smallpad'>
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
<t t-esc="props.text"/>
</button>
</div> \` ;
}
class Row extends Component {
static template = xml \`
<tr t-att-class="props.isSelected ? 'danger' : ''">
<td class="col-md-1" t-esc="props.row.id" />
<td class="col-md-4">
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
</td>
<td class="col-md-1">
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
</a>
</td>
<td class='col-md-6'/>
</tr> \`
}
class Root extends Component {
static template = xml \`
<div class='container'>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Owl Keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<tbody>
<t t-foreach="state.rows" t-as="row" t-key="row.id">
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
</t>
</tbody>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
</div> \` ;
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);
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
` ;
const BENCHMARK _CSS = `
tr.danger {
font-weight: bold;
}
.remove:hover {
font-weight: bold;
}
.remove {
cursor: pointer;
}
` ;
2019-05-03 10:33:22 +02:00
export const SAMPLES = [
{
2019-06-14 16:01:24 +02:00
description : "Components" ,
code : COMPONENTS ,
xml : COMPONENTS _XML ,
css : COMPONENTS _CSS
2019-05-03 10:33:22 +02:00
} ,
2019-06-13 16:27:12 +02:00
{
2019-06-14 16:01:24 +02:00
description : "Form Input Bindings" ,
2019-06-13 16:27:12 +02:00
code : FORM ,
2019-06-14 16:01:24 +02:00
xml : FORM _XML
2019-05-03 10:33:22 +02:00
} ,
2020-01-18 10:00:18 +01:00
{
2021-12-21 15:09:02 +01:00
description : "Inline templates" ,
2020-01-18 10:00:18 +01:00
code : SFC
} ,
2019-05-03 10:33:22 +02:00
{
description : "Lifecycle demo" ,
code : LIFECYCLE _DEMO ,
2019-06-14 16:01:24 +02:00
xml : LIFECYCLE _DEMO _XML ,
2019-06-28 10:32:03 +02:00
css : LIFECYCLE _CSS
2019-05-03 10:33:22 +02:00
} ,
2019-09-24 14:06:53 +02:00
{
2021-12-21 15:09:02 +01:00
description : "Customized hook" ,
2019-09-24 14:06:53 +02:00
code : HOOKS _DEMO ,
xml : HOOKS _DEMO _XML ,
css : HOOKS _CSS
} ,
2019-10-08 09:00:06 +02:00
{
2021-11-17 18:41:01 +01:00
description : "Todo List App (with reactivity)" ,
code : TODO _APP _REACTIVITY ,
css : TODO _APP _REACTIVITY _CSS ,
xml : TODO _APP _REACTIVITY _XML
2019-05-03 10:33:22 +02:00
} ,
{
description : "Responsive app" ,
code : RESPONSIVE ,
css : RESPONSIVE _CSS ,
xml : RESPONSIVE _XML
} ,
2019-06-11 15:02:10 +02:00
{
2019-06-14 16:01:24 +02:00
description : "Slots And Generic Components" ,
2019-06-11 15:02:10 +02:00
code : SLOTS ,
xml : SLOTS _XML ,
css : SLOTS _CSS
} ,
2019-08-27 13:47:01 +02:00
{
2021-11-17 18:41:01 +01:00
description : "Window Management System" ,
code : WMS ,
xml : WMS _XML ,
css : WMS _CSS ,
2019-11-15 13:24:07 +01:00
} ,
2022-02-14 13:04:13 +01:00
{
description : "Benchmark example" ,
code : BENCHMARK _JS ,
css : BENCHMARK _CSS ,
} ,
2019-05-03 10:33:22 +02:00
] ;