`;
-const LIFECYCLE_CSS = `button {
+const LIFECYCLE_CSS = /*css*/`
+button {
font-size: 18px;
margin: 5px;
}
@@ -269,9 +137,9 @@ const LIFECYCLE_CSS = `button {
width: 250px;
}`;
-const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
-const { hooks, mount } = owl;
-const {useState, onMounted, onWillUnmount} = hooks;
+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
@@ -279,13 +147,11 @@ function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
- position.x = e.clientX;
- position.y = e.clientY;
+ position.x = e.clientX;
+ position.y = e.clientY;
}
- onMounted(() => {
- window.addEventListener('mousemove', update);
- });
- onWillUnmount(() => {
+ window.addEventListener('mousemove', update);
+ onWillDestroy(() => {
window.removeEventListener('mousemove', update);
});
@@ -294,9 +160,10 @@ function useMouse() {
// Main root component
-class App extends owl.Component {
- constructor() {
- super(...arguments);
+class Root extends Component {
+ static template = "Root";
+
+ setup() {
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
@@ -310,89 +177,32 @@ class App extends owl.Component {
}
// Application setup
-mount(App, { target: document.body });
+mount(Root, document.body, { templates: TEMPLATES, dev: true });
`;
-const HOOKS_DEMO_XML = `
-
+const HOOKS_DEMO_XML = /*xml*/ `
+
+
Mouse: ,
`;
-const HOOKS_CSS = `button {
+const HOOKS_CSS = /*css*/ `button {
width: 120px;
height: 35px;
font-size: 16px;
}`;
-const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext'
-// hook to share information between them.
-const { Component, Context, mount } = owl;
-const { useContext } = owl.hooks;
+const TODO_APP_REACTIVITY = /*js*/ `
-class ToolbarButton extends Component {
- constructor() {
- super(...arguments);
- this.theme = useContext(this.env.themeContext);
- }
-
- get style () {
- const theme = this.theme;
- return \`background-color: \${theme.background}; color: \${theme.foreground}\`;
- }
-}
-
-class Toolbar extends Component {}
-Toolbar.components = { ToolbarButton };
-
-// Main root component
-class App extends Component {
- toggleTheme() {
- const { background, foreground } = this.env.themeContext.state;
- this.env.themeContext.state.background = foreground;
- this.env.themeContext.state.foreground = background;
- }
-}
-App.components = { Toolbar };
-
-// Application setup
-const themeContext = new Context({
- background: '#000',
- foreground: '#fff',
-});
-// Add the themeContext the environment to make it available to all components
-App.env.themeContext = themeContext;
-mount(App, { target: document.body });
-`;
-
-const CONTEXT_XML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
+// 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 Store class to manage the state. It
-// is very similar to the VueX store.
-const { Component, useState, mount } = owl;
-const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
+// In this implementation, we use the owl reactivity mechanism.
+const { Component, useState, mount, useRef, onPatched, onMounted, reactive, useEnv, useEffect } = owl;
//------------------------------------------------------------------------------
// Constants, helpers
@@ -402,124 +212,147 @@ const ESC_KEY = 27;
const LOCALSTORAGE_KEY = "todomvc";
function useAutofocus(name) {
- let ref = useRef(name);
- let isInDom = false;
- function updateFocus() {
- if (!isInDom && ref.el) {
- isInDom = true;
- const current = ref.el.value;
- ref.el.value = "";
- ref.el.focus();
- ref.el.value = current;
- } else if (isInDom && !ref.el) {
- isInDom = false;
- }
- }
- onPatched(updateFocus);
- onMounted(updateFocus);
+ let ref = useRef(name);
+ useEffect(el => el && el.focus(), () => [ref.el]);
+}
+
+function useStore() {
+ const env = useEnv();
+ return useState(env.store);
}
//------------------------------------------------------------------------------
-// Store
+// Task store
//------------------------------------------------------------------------------
-const initialState = { todos: [], nextId: 1};
+class TaskList {
+ constructor(tasks) {
+ this.tasks = tasks || [];
+ const taskIds = this.tasks.map((t) => t.id);
+ this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
+ }
-const actions = {
- addTodo({ state }, title) {
- const todo = {
- id: state.nextId++,
- title,
- completed: false
+ addTask(text) {
+ text = text.trim();
+ if (text) {
+ const task = {
+ id: this.nextId++,
+ text: text,
+ isCompleted: false,
+ };
+ this.tasks.push(task);
}
- state.todos.push(todo);
- },
- removeTodo({ state }, id) {
- const index = state.todos.findIndex(t => t.id === id);
- state.todos.splice(index, 1);
- },
- updateTodo({state, dispatch}, {id, title}) {
- const value = title.trim();
+ }
+
+ 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) {
- dispatch('removeTodo', id);
+ this.deleteTask(id);
} else {
- const todo = state.todos.find(t => t.id === id);
- todo.title = value;
+ const task = this.tasks.find(t => t.id === id);
+ task.text = value;
}
- },
- toggleTodo({ state }, id) {
- const todo = state.todos.find(t => t.id === id);
- todo.completed = !todo.completed;
- },
- clearCompleted({ state, dispatch }) {
- for (let todo of state.todos.slice()) {
- if (todo.completed) {
- dispatch("removeTodo", todo.id);
- }
- }
- },
- toggleAll({ state, dispatch }, completed) {
- for (let todo of state.todos) {
- todo.completed = completed;
- }
- },
-};
+ }
+}
+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;
+}
+
//------------------------------------------------------------------------------
-// TodoItem
+// Todo
//------------------------------------------------------------------------------
-class TodoItem extends Component {
- constructor() {
- super(...arguments);
+class Todo extends Component {
+ static template = "Todo";
+
+ setup() {
useAutofocus("input");
- this.state = useState({ isEditing: false });
- this.dispatch = useDispatch();
+ this.store = useStore();
+ this.state = useState({
+ isEditing: false
+ });
}
handleKeyup(ev) {
if (ev.keyCode === ENTER_KEY) {
- this.updateTitle(ev.target.value);
+ this.updateText(ev.target.value);
}
if (ev.keyCode === ESC_KEY) {
- ev.target.value = this.props.title;
+ ev.target.value = this.props.text;
this.state.isEditing = false;
}
}
handleBlur(ev) {
- this.updateTitle(ev.target.value);
+ this.updateText(ev.target.value);
}
- updateTitle(title) {
- this.dispatch("updateTodo", {title, id: this.props.id});
+ updateText(text) {
+ this.store.updateTask(this.props.id, text);
this.state.isEditing = false;
}
}
//------------------------------------------------------------------------------
-// TodoApp
+// TodoList
//------------------------------------------------------------------------------
-class TodoApp extends Component {
- constructor() {
- super(...arguments);
+class TodoList extends Component {
+ static template = "TodoList";
+ static components = { Todo };
+
+ setup() {
+ this.store = useStore();
this.state = useState({ filter: "all" });
- this.todos = useStore(state => state.todos);
- this.dispatch = useDispatch();
}
- get visibleTodos() {
- switch (this.state.filter) {
- case "active": return this.todos.filter(t => !t.completed);
- case "completed": return this.todos.filter(t => t.completed);
- case "all": return this.todos;
- }
+ 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.todos.every(todo => todo.completed);
+ return this.store.tasks.every(todo => todo.isCompleted);
}
get remaining() {
- return this.todos.filter(todo => !todo.completed).length;
+ return this.store.tasks.filter(todo => !todo.isCompleted).length;
}
get remainingText() {
@@ -529,9 +362,9 @@ class TodoApp extends Component {
addTodo(ev) {
if (ev.keyCode === ENTER_KEY) {
- const title = ev.target.value;
- if (title.trim()) {
- this.dispatch("addTodo", title);
+ const text = ev.target.value;
+ if (text.trim()) {
+ this.store.addTask(text);
}
ev.target.value = "";
}
@@ -541,48 +374,31 @@ class TodoApp extends Component {
this.state.filter = filter;
}
}
-TodoApp.components = { TodoItem };
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
-
-function makeStore() {
- function saveState(state) {
- const str = JSON.stringify(state);
- window.localStorage.setItem(LOCALSTORAGE_KEY, str);
- }
- function loadState() {
- const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
- return localState ? JSON.parse(localState) : initialState;
- }
-
- const state = loadState();
- const store = new owl.Store({ state, actions });
- store.on("update", null, () => saveState(store.state));
- return store;
-}
-
-TodoApp.env.store = makeStore();
-mount(TodoApp, { target: document.body });
+const env = { store: createTaskStore() };
+mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
`;
-const TODO_APP_STORE_XML = `
-
+const TODO_APP_REACTIVITY_XML = /*xml*/ `
+
+
todos
-
-
+
+
-
-
+
+
-
-
+
-
-
-
+
`;
-const TODO_APP_STORE_CSS = `html,
-body {
+const TODO_APP_REACTIVITY_CSS = /*css*/`
+html,body {
margin: 0;
padding: 0;
}
@@ -1000,8 +816,9 @@ html .clear-completed:active {
}
`;
-const RESPONSIVE = `// In this example, we show how we can modify keys in the global environment to
-// make a responsive application.
+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
@@ -1009,71 +826,134 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
//
// 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 {}
+class Navbar extends owl.Component {
+ static template = "Navbar";
+}
-class MobileSearchView extends owl.Component {}
+class MobileSearchView extends Component {
+ static template = "MobileSearchView";
+}
-class ControlPanel extends owl.Component {}
-ControlPanel.components = { MobileSearchView };
+class ControlPanel extends Component {
+ static template = "ControlPanel";
+ static components = { MobileSearchView };
+ setup() {
+ this.ui = useUI();
+ }
+}
-class AdvancedComponent extends owl.Component {}
+class AdvancedComponent extends Component {
+ static template = "AdvancedComponent";
+}
-class FormView extends owl.Component {}
-FormView.components = { AdvancedComponent };
+class FormView extends Component {
+ static template = "FormView";
+ static components = { AdvancedComponent };
+ setup() {
+ this.ui = useUI();
+ }
+}
-class Chatter extends owl.Component {
- constructor() {
- super(...arguments);
+class Chatter extends Component {
+ static template = "Chatter";
+
+ setup() {
this.messages = Array.from(Array(100).keys());
}
}
-class App extends owl.Component {}
-App.components = { Navbar, ControlPanel, FormView, Chatter };
+class Root extends Component {
+ static template = "Root";
+ static components = { Navbar, ControlPanel, FormView, Chatter };
-//------------------------------------------------------------------------------
-// Responsive plugin
-//------------------------------------------------------------------------------
-function setupResponsivePlugin(env) {
- const isMobile = () => window.innerWidth <= 768;
- env.isMobile = isMobile();
- const updateEnv = owl.utils.debounce(() => {
- if (env.isMobile !== isMobile()) {
- env.isMobile = !env.isMobile;
- env.qweb.forceUpdate();
- }
- }, 15);
- window.addEventListener("resize", updateEnv);
+ setup() {
+ this.ui = useUI();
+ }
}
+
+
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
-setupResponsivePlugin(App.env);
+const env = {
+ ui: createUI()
+};
-owl.mount(App, { target: document.body });
+mount(Root, document.body, { templates: TEMPLATES, env });
`;
-const RESPONSIVE_XML = `
+const RESPONSIVE_XML = /*xml*/`
+
Navbar
Control Panel
-
+
Form View
-
+
Chatter
-
Message
+
Message
Mobile searchview
@@ -1087,10 +967,10 @@ const RESPONSIVE_XML = `
-
+
-
+
@@ -1102,7 +982,8 @@ const RESPONSIVE_XML = `
`;
-const RESPONSIVE_CSS = `body {
+const RESPONSIVE_CSS = /*css*/`
+body {
margin: 0;
}
@@ -1160,55 +1041,61 @@ const RESPONSIVE_CSS = `body {
}
`;
-const SLOTS = `// We show here how slots can be used to create generic components.
+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 App template, is executed in
-// the context of the App component, even though it is inside the Card component
+// 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 {
- constructor() {
- super(...arguments);
- this.state = useState({ showContent: true });
- }
+ static template = "Card";
+
+ setup() {
+ this.state = useState({ showContent: true });
+ }
- toggleDisplay() {
- this.state.showContent = !this.state.showContent;
- }
+ toggleDisplay() {
+ this.state.showContent = !this.state.showContent;
+ }
}
class Counter extends Component {
- constructor() {
- super(...arguments);
- this.state = useState({val: 1});
- }
+ static template = "Counter";
+
+ setup() {
+ this.state = useState({val: 1});
+ }
- inc() {
- this.state.val++;
- }
+ inc() {
+ this.state.val++;
+ }
}
// Main root component
-class App extends Component {
- constructor() {
- super(...arguments);
- this.state = useState({a: 1, b: 3});
- }
+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;
- }
+ inc(key, delta) {
+ this.state[key] += delta;
+ }
}
-App.components = {Card, Counter};
// Application setup
-mount(App, { target: document.body });
+mount(Root, document.body, { templates: TEMPLATES, dev: true});
`;
-const SLOTS_XML = `
+const SLOTS_XML = /*xml*/`
+
@@ -1227,22 +1114,24 @@ const SLOTS_XML = `
-
+
Content of card 1... []
-
+
Card 2... []
-
+
-`;
+
+`;
-const SLOTS_CSS = `.main {
+const SLOTS_CSS = /*css*/ `
+.main {
display: flex;
}
@@ -1286,118 +1175,33 @@ const SLOTS_CSS = `.main {
border-top: 1px solid white;
}`;
-const ASYNC_COMPONENTS = `// This example will not work if your browser does not support ESNext class fields
-
-// In this example, we have 2 sub components, one of them being async (slow).
-// However, we don't want renderings of the other sub component to be delayed
-// because of the slow component. We use the AsyncRoot component for this
-// purpose. Try removing it to see the difference.
-const { Component, useState, mount } = owl;
-const { AsyncRoot } = owl.misc;
-
-class SlowComponent extends Component {
- willUpdateProps() {
- // simulate a component that needs to perform async stuff (e.g. an RPC)
- // with the updated props before re-rendering itself
- return new Promise(resolve => setTimeout(resolve, 1500));
- }
-}
-
-class NotificationList extends Component {}
-
-class App extends Component {
- constructor() {
- super(...arguments);
- this.state = useState({ value: 0, notifs: [] });
- }
-
- increment() {
- this.state.value++;
- const notif = "Value will be set to " + this.state.value;
- this.state.notifs.push(notif);
- setTimeout(() => {
- var index = this.state.notifs.indexOf(notif);
- this.state.notifs.splice(index, 1);
- }, 3000);
- }
-}
-App.components = {SlowComponent, NotificationList, AsyncRoot};
-
-mount(App, { target: document.body });
-`;
-
-const ASYNC_COMPONENTS_XML = `
-
-
-
-
-
-
-
-
- Current value:
-
-
-
-
-
-
-
-`;
-
-const ASYNC_COMPONENTS_CSS = `.app {
- width: 70%;
-}
-
-button {
- color: darkred;
- font-size: 30px;
- width: 220px;
-}
-
-.value {
- font-size: 26px;
- padding: 20px;
-}
-
-.notification-list {
- position: absolute;
- top: 0;
- right: 0;
-}
-
-.notification {
- width: 150px;
- margin: 4px 8px;
- padding: 16px;
- border: 1px solid: black;
- background-color: lightgray;
-}`;
-
-const FORM = `// This example illustrate how the t-model directive can be used to synchronize
+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 {
- constructor() {
- super(...arguments);
- this.state = useState({
- text: "",
- othertext: "",
- number: 11,
- color: "",
- bool: false
- });
- }
+ static template = "Form";
+
+ setup() {
+ this.state = useState({
+ text: "",
+ othertext: "",
+ number: 11,
+ color: "",
+ bool: false
+ });
+ }
}
// Application setup
-mount(Form, { target: document.body });
+mount(Form, document.body, { templates: TEMPLATES, dev: true });
`;
-const FORM_XML = `
+const FORM_XML = /*xml*/ `
+
Form
@@ -1436,111 +1240,8 @@ const FORM_XML = `
`;
-const PORTAL_COMPONENTS = `
-// This shows the expected use case of Portal
-// which is to implement something similar
-// to bootstrap modal
-const { Component, useState, mount } = owl;
-const { Portal } = owl.misc;
-
-class Modal extends Component {}
-Modal.components = { Portal };
-
-class Dialog extends Component {}
-Dialog.components = { Modal };
-
-class Interstellar extends Component {}
-
-// Main root component
-class App extends Component {
- state = useState({
- name: 'Portal used for Dialog (Modal)',
- dialog: false,
- text: 'Hello !',
- });
-}
-App.components = { Dialog , Interstellar };
-
-// Application setup
-mount(App, { target: document.body });
-`;
-
-const PORTAL_XML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
This is a subComponent
-
The events it triggers will go through the Portal and be teleported
- on the other side of the wormhole it has created
`;
-const WMS_CSS = `body {
+const WMS_CSS= /*css*/`
+body {
margin: 0;
}
@@ -1801,46 +1542,220 @@ const WMS_CSS = `body {
font-size: 20px;
}`;
-const SFC = `// This example illustrates how Owl enables single file components,
-// which include code, template and style.
-//
-// This is very useful in some situations, such as testing or quick prototyping.
-// Note that this example has no external xml or css file, everything is
-// contained in a single js file.
+const SFC =/*js*/`
+// This example illustrates how one can write Owl components with
+// inline templates.
-const { Component, useState, tags, mount } = owl;
-const { xml, css } = tags;
+const { Component, useState, xml, css, mount } = owl;
// Counter component
-const COUNTER_TEMPLATE = xml\`
- \`;
-
-const COUNTER_STYLE = css\`
- button {
- color: blue;
- }\`;
-
class Counter extends Component {
+ static template = xml\`
+ \`;
+
state = useState({ value: 0})
}
-Counter.template = COUNTER_TEMPLATE;
-Counter.style = COUNTER_STYLE;
-// App
-const APP_TEMPLATE = xml\`
-