From efd934d2b11949f2ec1ce441f17b149cf212039d Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Wed, 17 Nov 2021 18:41:01 +0100 Subject: [PATCH] [FIX] tools: adapt playground to owl 2 --- tools/playground/app.js | 245 +++------ tools/playground/export_snippet.js | 123 +++++ tools/playground/index.html | 2 + tools/playground/samples.js | 847 +++++++++++------------------ tools/playground/templates.xml | 13 +- tools/playground/utils.js | 49 ++ 6 files changed, 573 insertions(+), 706 deletions(-) create mode 100644 tools/playground/export_snippet.js create mode 100644 tools/playground/utils.js diff --git a/tools/playground/app.js b/tools/playground/app.js index 973eba65..9ebdf292 100644 --- a/tools/playground/app.js +++ b/tools/playground/app.js @@ -1,19 +1,11 @@ import { SAMPLES } from "./samples.js"; -const { mount, hooks } = owl; -const { useState, useRef, onMounted, onWillUnmount } = hooks; +import { debounce, loadJS } from "./utils.js"; +import { exportStandaloneApp } from "./export_snippet.js"; +const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl; + //------------------------------------------------------------------------------ // Constants, helpers, utils //------------------------------------------------------------------------------ -let owlJS; - -async function owlSourceCode() { - if (owlJS) { - return owlJS; - } - const result = await fetch("../owl.js"); - owlJS = await result.text(); - return owlJS; -} const MODES = { js: "ace/mode/javascript", @@ -24,77 +16,41 @@ const MODES = { const DEFAULT_XML = ` `; -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 an iframe, with all the js, css and xml properly injected. */ function makeCodeIframe(js, css, xml) { const sanitizedXML = xml.replace(//g, "").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 = "text/javascript"; + script.type = "module"; const content = ` - { - owl.config.mode = 'dev'; - let templates = \`${sanitizedXML}\`; - const qweb = new owl.QWeb({ templates }); - owl.Component.env = { qweb }; - } - ${js}`; + import * as utils from "./utils.js"; + (function (owl) { + const _configure = owl.App.prototype.configure; + owl.App.prototype.configure = function configureOverriden(config) { + config = Object.assign({ dev: true }, config); + this.addTemplates(\`${sanitizedXML}\`); + return _configure.call(this, config); + } + })(owl); + (async function() { + ${js} + })()`; script.innerHTML = content; doc.body.appendChild(script); }); @@ -108,58 +64,6 @@ function makeCodeIframe(js, css, xml) { return iframe; } -/** - * Make a zip file containing a functioning application - */ -async function makeApp(js, css, xml) { - await owl.utils.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. - */ -function app() { -${processedJS} -} - -/** - * Initialization code - * This code load templates, and make sure everything is properly connected. - */ -async function start() { - let templates; - try { - templates = await owl.utils.loadFile('app.xml'); - } catch(e) { - console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`); - return; - } - const env = { qweb: new owl.QWeb({templates})}; - owl.Component.env = env; - await owl.utils.whenReady(); - app(); -} - -start(); -`; - - zip.file("app.js", JS); - 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", owlSourceCode()); - return zip.generateAsync({ type: "blob" }); -} - //------------------------------------------------------------------------------ // SAMPLES //------------------------------------------------------------------------------ @@ -189,7 +93,7 @@ function deleteLocalSample() { function useSamples() { const samples = loadSamples(); - const component = owl.Component.current; + const component = owl.useComponent(); let interval; onMounted(() => { @@ -205,57 +109,57 @@ function useSamples() { }); return samples; } + //------------------------------------------------------------------------------ // Tabbed editor //------------------------------------------------------------------------------ class TabbedEditor extends owl.Component { - constructor(parent, props) { - super(parent, props); + setup() { + const props = this.props; this.state = useState({ currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css" }); - this.setTab = owl.utils.debounce(this.setTab, 250, true); + this.setTab = debounce(this.setTab.bind(this), 250, true); this.sessions = {}; this._setupSessions(props); this.editorNode = useRef("editor"); this._updateCode = this._updateCode.bind(this); - } - mounted() { - this.editor = this.editor || ace.edit(this.editorNode.el); + onMounted(() => { + this.editor = this.editor || ace.edit(this.editorNode.el); - this.editor.setValue(this.props[this.state.currentTab], -1); - this.editor.setFontSize("12px"); - this.editor.setTheme("ace/theme/monokai"); - this.editor.setSession(this.sessions[this.state.currentTab]); - const tabSize = this.state.currentTab === "xml" ? 2 : 4; - this.editor.session.setOption("tabSize", tabSize); - this.editor.on("blur", this._updateCode); - this.interval = setInterval(this._updateCode, 3000); - } + this.editor.setValue(this.props[this.state.currentTab], -1); + this.editor.setFontSize("12px"); + this.editor.setTheme("ace/theme/monokai"); + this.editor.setSession(this.sessions[this.state.currentTab]); + const tabSize = this.state.currentTab === "xml" ? 2 : 4; + this.editor.session.setOption("tabSize", tabSize); + this.editor.on("blur", this._updateCode); + this.interval = setInterval(this._updateCode, 3000); + }); - willUnmount() { - clearInterval(this.interval); - this.editor.off("blur", this._updateCode); - } - willUpdateProps(nextProps) { - this._setupSessions(nextProps); - } + onPatched(() => { + const session = this.sessions[this.state.currentTab]; + let content = this.props[this.state.currentTab]; + if (content === false) { + const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css"; + content = this.props[tab]; + this.state.currentTab = tab; + } + if (this.editor.getValue() !== content) { + session.setValue(content, -1); + this.editor.setSession(session); + this.editor.resize(); + } + }); - patched() { - const session = this.sessions[this.state.currentTab]; - let content = this.props[this.state.currentTab]; - if (content === false) { - const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css"; - content = this.props[tab]; - this.state.currentTab = tab; - } - if (this.editor.getValue() !== content) { - session.setValue(content, -1); - this.editor.setSession(session); - this.editor.resize(); - } + onWillUpdateProps((nextProps) => this._setupSessions(nextProps)); + + onWillUnmount(() => { + clearInterval(this.interval); + this.editor.off("blur", this._updateCode); + }); } setTab(tab) { @@ -298,20 +202,20 @@ class TabbedEditor extends owl.Component { const editorValue = this.editor.getValue(); const propsValue = this.props[this.state.currentTab]; if (editorValue !== propsValue) { - this.trigger("updateCode", { + this.props.updateCode({ type: this.state.currentTab, value: editorValue }); } } } +TabbedEditor.template = "TabbedEditor"; //------------------------------------------------------------------------------ // MAIN APP //------------------------------------------------------------------------------ class App extends owl.Component { - constructor(...args) { - super(...args); + setup() { this.version = owl.__info__.version; this.SAMPLES = useSamples(); this.isDirty = false; @@ -326,10 +230,11 @@ class App extends owl.Component { topPanelHeight: null }); - this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true); - this.runCode = owl.utils.debounce(this.runCode, 250, true); - this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true); + 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); } runCode() { @@ -379,9 +284,9 @@ class App extends owl.Component { } }); } - updateCode(ev) { - if (this.state[ev.detail.type] !== ev.detail.value) { - this.state[ev.detail.type] = ev.detail.value; + updateCode({type, value}) { + if (this.state[type] !== value) { + this.state[type] = value; this.isDirty = true; } } @@ -401,12 +306,13 @@ class App extends owl.Component { async downloadCode() { const { js, css, xml } = this.state; - const content = await makeApp(js, css, xml); - await owl.utils.loadJS("libs/FileSaver.min.js"); + const content = await exportStandaloneApp(js, css, xml); + await loadJS("libs/FileSaver.min.js"); saveAs(content, "app.zip"); } } App.components = { TabbedEditor }; +App.template = "App"; //------------------------------------------------------------------------------ // Application initialization @@ -416,12 +322,13 @@ async function start() { const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`; console.info(`This application is using Owl built with the following commit:`, commit); const [templates] = await Promise.all([ - owl.utils.loadFile("templates.xml"), - owl.utils.whenReady() + owl.loadFile("templates.xml"), + owl.whenReady() ]); - const qweb = new owl.QWeb({ templates }); - const env = { qweb }; - await mount(App, {target: document.body, env}); + const rootApp = new owl.App(App); + rootApp.addTemplates(templates); + + await rootApp.mount(document.body); } start(); diff --git a/tools/playground/export_snippet.js b/tools/playground/export_snippet.js new file mode 100644 index 00000000..7e0b8aef --- /dev/null +++ b/tools/playground/export_snippet.js @@ -0,0 +1,123 @@ +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 4fae2f13..6b9ba063 100644 --- a/tools/playground/index.html +++ b/tools/playground/index.html @@ -12,6 +12,8 @@ + + diff --git a/tools/playground/samples.js b/tools/playground/samples.js index 962229e6..2d654edf 100644 --- a/tools/playground/samples.js +++ b/tools/playground/samples.js @@ -1,4 +1,5 @@ -const COMPONENTS = `// In this example, we show how components can be defined and created. +const COMPONENTS = /*js*/ ` +// In this example, we show how components can be defined and created. const { Component, useState, mount } = owl; class Greeter extends Component { @@ -10,6 +11,7 @@ class Greeter extends Component { this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'; } } +Greeter.template = "Greeter"; // Main root component class App extends Component { @@ -18,12 +20,14 @@ class App extends Component { } } App.components = { Greeter }; +App.template = "App"; // Application setup -mount(App, { target: document.body }); +mount(App, document.body) `; -const COMPONENTS_XML = ` +const COMPONENTS_XML = /*xml*/` +
,
@@ -34,7 +38,8 @@ const COMPONENTS_XML = ` `; -const COMPONENTS_CSS = `.greeter { +const COMPONENTS_CSS = /*css*/` +.greeter { font-size: 20px; width: 300px; height: 100px; @@ -45,177 +50,32 @@ const COMPONENTS_CSS = `.greeter { user-select: none; }`; -const ANIMATION = `// The goal of this component is to see how the t-transition directive can be -// used to generate simple transition effects. -const { Component, useState, mount } = owl; - -class Counter extends Component { - setup() { - this.state = useState({ value: 0 }); - } - - increment() { - this.state.value++; - } -} - -class App extends Component { - setup() { - this.state = useState({ flag: false, componentFlag: false, numbers: [] }); - } - - toggle(key) { - this.state[key] = !this.state[key]; - } - - addNumber() { - const n = this.state.numbers.length + 1; - this.state.numbers.push(n); - } -} -App.components = { Counter }; - -mount(App, { target: document.body }); -`; - -const ANIMATION_XML = ` - - -
-

Transition on DOM element

- -
- -
-
Hello
-
-
- -

Transition on sub components

- -
- -
- -
-
- -

Transition on lists

-

Transitions can also be applied on lists

-
- -
- - - -
-
- -

Simple CSS animation

-

Remember, normal CSS still apply: for example, a simple flash animation with pure css

- -
-
-`; - -const ANIMATION_CSS = `button { - font-size: 18px; - height: 35px; -} - -.btn { - cursor: pointer; - padding: 5px; - margin: 5px; - background-color: #dddddd; -} - -.flash { - background-position: center; - transition: background .6s; -} - -.flash:active { - background-color: gray; - transition: background 0s; -} - -.square { - background-color: red; - width: 100px; - height: 70px; - color: white; - margin: 0 20px; - font-size: 24px; - line-height: 70px; - text-align: center; -} - -.fade-enter-active, .fade-leave-active { - transition: opacity .6s; -} - -.fade-enter, .fade-leave-to { - opacity: 0; -} - -.demo { - display: flex; - height: 80px; -} - -.clickcounter { - margin-left: 20px; - height: 50px; - background-color: blue; - color: white; -} - -.numberspan { - border: 1px solid green; - margin: 5px; - padding: 5px; -} -`; - -const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks +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 } = owl; +const { Component, useState, mount, onWillStart, onMounted, onWillUnmount, onWillUpdateProps, onPatched, onWillPatch } = owl; class DemoComponent extends Component { setup() { this.state = useState({ n: 0 }); console.log("setup"); - } - async willStart() { - console.log("willstart"); - } - mounted() { - console.log("mounted"); - } - async willUpdateProps(nextProps) { - console.log("willUpdateProps", nextProps); - } - willPatch() { - console.log("willPatch"); - } - patched() { - console.log("patched"); - } - willUnmount() { - console.log("willUnmount"); + + onWillStart(() => console.log("willstart")); + onMounted(() => console.log("mounted")); + onWillPatch(() => console.log("willPatch")); + onWillUpdateProps((nextProps) => console.log("willUpdateProps", nextProps)); + onPatched(() => console.log("patched")); + onWillUnmount(() => console.log("willUnmount")); } increment() { this.state.n++; } } +DemoComponent.template = "DemoComponent"; class App extends Component { setup() { @@ -231,11 +91,13 @@ class App extends Component { } } App.components = { DemoComponent }; +App.template = "App"; -mount(App, { target: document.body }); +mount(App, document.body); `; -const LIFECYCLE_DEMO_XML = ` +const LIFECYCLE_DEMO_XML = /*xml*/ ` +
Demo Sub Component
(click on me to update me)
@@ -251,7 +113,8 @@ const LIFECYCLE_DEMO_XML = `
`; -const LIFECYCLE_CSS = `button { +const LIFECYCLE_CSS = /*css*/` +button { font-size: 18px; margin: 5px; } @@ -263,9 +126,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 { mount, useState, onMounted, onWillUnmount } = owl; // We define here a custom behaviour: this hook tracks the state of the mouse // position @@ -301,12 +164,14 @@ class App extends owl.Component { this.counter.value++; } } +App.template = "App"; // Application setup -mount(App, { target: document.body }); +mount(App, document.body); `; -const HOOKS_DEMO_XML = ` +const HOOKS_DEMO_XML = /*xml*/ ` +
Mouse: ,
@@ -314,77 +179,19 @@ const HOOKS_DEMO_XML = ` `; -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; - -class ToolbarButton extends Component { - setup() { - 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 +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 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 } = owl; //------------------------------------------------------------------------------ // Constants, helpers @@ -411,59 +218,15 @@ function useAutofocus(name) { onMounted(updateFocus); } -//------------------------------------------------------------------------------ -// Store -//------------------------------------------------------------------------------ -const initialState = { todos: [], nextId: 1}; - -const actions = { - addTodo({ state }, title) { - const todo = { - id: state.nextId++, - title, - completed: false - } - 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(); - if (!value) { - dispatch('removeTodo', id); - } else { - const todo = state.todos.find(t => t.id === id); - todo.title = 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; - } - }, -}; - //------------------------------------------------------------------------------ // TodoItem //------------------------------------------------------------------------------ class TodoItem extends Component { setup() { useAutofocus("input"); - this.state = useState({ isEditing: false }); - this.dispatch = useDispatch(); + this.todo = this.env.todo; + this.todoState = useState(this.todo.state); + this.state = useState({}); } handleKeyup(ev) { @@ -481,19 +244,26 @@ class TodoItem extends Component { } updateTitle(title) { - this.dispatch("updateTodo", {title, id: this.props.id}); + this.todo.updateTodo({title, id: this.props.id}); this.state.isEditing = false; } } +TodoItem.template = "TodoItem"; //------------------------------------------------------------------------------ // TodoApp //------------------------------------------------------------------------------ class TodoApp extends Component { setup() { + this.todo = this.env.todo; + this.todoState = useState(this.todo.state); + this.state = useState({ filter: "all" }); - this.todos = useStore(state => state.todos); - this.dispatch = useDispatch(); + this.setFilter = this.setFilter.bind(this); + } + + get todos() { + return this.todoState.todos; } get visibleTodos() { @@ -521,7 +291,7 @@ class TodoApp extends Component { if (ev.keyCode === ENTER_KEY) { const title = ev.target.value; if (title.trim()) { - this.dispatch("addTodo", title); + this.todo.addTodo(title); } ev.target.value = ""; } @@ -532,43 +302,103 @@ class TodoApp extends Component { } } TodoApp.components = { TodoItem }; +TodoApp.template = "TodoApp"; //------------------------------------------------------------------------------ // App Initialization //------------------------------------------------------------------------------ -function makeStore() { +function makeGlobalState(initialState, key) { + const state = Object.assign(initialState, loadState()); + const reactiveState = reactive(state, () => saveState(reactiveState)); + // read everything to be notified afterwards + saveState(reactiveState); + 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 str = JSON.stringify(state); + try { + localStorage.setItem(key, str); + } catch (e) {}; } - const state = loadState(); - const store = new owl.Store({ state, actions }); - store.on("update", null, () => saveState(store.state)); - return store; + function loadState() { + const localState = localStorage.getItem(key); + return localState ? JSON.parse(localState) : {}; + } + + return { state: reactiveState }; } -TodoApp.env.store = makeStore(); -mount(TodoApp, { target: document.body }); +function toDoService() { + const { state } = makeGlobalState({ todos: [], nextId: 1}, LOCALSTORAGE_KEY); + + function addTodo(title) { + const todo = { + id: state.nextId++, + title, + completed: false + } + state.todos.push(todo); + } + + function removeTodo(id) { + const index = state.todos.findIndex(t => t.id === id); + state.todos.splice(index, 1); + } + + function updateTodo({id, title}) { + const value = title.trim(); + if (!value) { + removeTodo(id); + } else { + const todo = state.todos.find(t => t.id === id); + todo.title = value; + } + } + + function toggleTodo(id) { + const todo = state.todos.find(t => t.id === id); + todo.completed = !todo.completed; + } + + function clearCompleted() { + for (let todo of state.todos.slice()) { + if (todo.completed) { + removeTodo(todo.id); + } + } + } + + function toggleAll(completed) { + for (let todo of state.todos) { + todo.completed = completed; + } + } + + return { + state, addTodo, removeTodo, updateTodo, toggleTodo, clearCompleted, toggleAll + } +} + +const env = { + todo: toDoService(), +}; +mount(TodoApp, document.body, { env }); `; -const TODO_APP_STORE_XML = ` +const TODO_APP_REACTIVITY_XML = /*xml*/ ` +

todos

- +
    - - + +
@@ -581,16 +411,16 @@ const TODO_APP_STORE_XML = ` - @@ -598,18 +428,18 @@ const TODO_APP_STORE_XML = `
  • - -
  • `; -const TODO_APP_STORE_CSS = `html, -body { +const TODO_APP_REACTIVITY_CSS = /*css*/` +html,body { margin: 0; padding: 0; } @@ -990,7 +820,8 @@ html .clear-completed:active { } `; -const RESPONSIVE = `// In this example, we show how we can modify keys in the global environment to +const RESPONSIVE = /*js*/ ` +// In this example, we show how we can modify keys in the global environment to // make a responsive application. // // The main idea is to have a "isMobile" key in the environment, then listen @@ -1000,69 +831,97 @@ 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 { debounce, useBus } = utils; + //------------------------------------------------------------------------------ // Components //------------------------------------------------------------------------------ class Navbar extends owl.Component {} +Navbar.template = "Navbar"; class MobileSearchView extends owl.Component {} +MobileSearchView.template = "MobileSearchView"; class ControlPanel extends owl.Component {} ControlPanel.components = { MobileSearchView }; +ControlPanel.template = "ControlPanel"; class AdvancedComponent extends owl.Component {} +AdvancedComponent.template = "AdvancedComponent"; class FormView extends owl.Component {} FormView.components = { AdvancedComponent }; +FormView.template = "FormView"; class Chatter extends owl.Component { setup() { this.messages = Array.from(Array(100).keys()); } } +Chatter.template = "Chatter"; -class App extends owl.Component {} +class App extends owl.Component { + setup() { + useResponsive(); + } +} +App.template = "App"; App.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(); + +function responsiveService(env) { + const getIsMobile = () => window.innerWidth <= 768; + + let isMobile = getIsMobile(); + const responsive = new owl.EventBus(); + + env.isMobile = () => isMobile; + const updateEnv = debounce(() => { + if (isMobile !== getIsMobile()) { + isMobile = !isMobile; + responsive.trigger("resize"); } }, 15); window.addEventListener("resize", updateEnv); + return responsive; } +function useResponsive() { + const comp = owl.useComponent(); + const responsive = comp.env.responsive; + useBus(responsive, "resize", () => comp.render()); +} + + //------------------------------------------------------------------------------ // Application Startup //------------------------------------------------------------------------------ -setupResponsivePlugin(App.env); +const env = {}; +env.responsive = responsiveService(env); -owl.mount(App, { target: document.body }); +owl.mount(App, document.body, { env }); `; -const RESPONSIVE_XML = ` +const RESPONSIVE_XML = /*xml*/` +

    Control Panel

    - +

    Form View

    - +

    Chatter

    -
    Message
    +
    Message
    Mobile searchview
    @@ -1076,10 +935,10 @@ const RESPONSIVE_XML = ` -
    +
    -
    +
    @@ -1091,7 +950,8 @@ const RESPONSIVE_XML = ` `; -const RESPONSIVE_CSS = `body { +const RESPONSIVE_CSS = /*css*/` +body { margin: 0; } @@ -1149,7 +1009,8 @@ 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. @@ -1167,6 +1028,7 @@ class Card extends Component { this.state.showContent = !this.state.showContent; } } +Card.template = "Card"; class Counter extends Component { setup() { @@ -1177,11 +1039,13 @@ class Counter extends Component { this.state.val++; } } +Counter.template = "Counter"; // Main root component class App extends Component { setup() { this.state = useState({a: 1, b: 3}); + this.inc = this.inc.bind(this); } inc(key, delta) { @@ -1189,12 +1053,14 @@ class App extends Component { } } App.components = {Card, Counter}; +App.template = "App"; // Application setup -mount(App, { target: document.body }); +mount(App, document.body); `; -const SLOTS_XML = ` +const SLOTS_XML = /*xml*/` +
    @@ -1216,19 +1082,20 @@ const SLOTS_XML = `
    Content of card 1... [] - +
    Card 2... []
    - +
    `; -const SLOTS_CSS = `.main { +const SLOTS_CSS = /*css*/ ` +.main { display: flex; } @@ -1272,94 +1139,8 @@ 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 { - setup() { - 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. @@ -1376,12 +1157,14 @@ class Form extends Component { }); } } +Form.template = "Form"; // Application setup -mount(Form, { target: document.body }); +mount(Form, document.body); `; -const FORM_XML = ` +const FORM_XML = /*xml*/ ` +

    Form

    @@ -1420,20 +1203,22 @@ const FORM_XML = ` `; -const PORTAL_COMPONENTS = ` +const PORTAL_COMPONENTS = /*js*/` // 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; +const { Component, useState, mount, Portal } = owl; class Modal extends Component {} Modal.components = { Portal }; +Modal.template = "Modal"; class Dialog extends Component {} Dialog.components = { Modal }; +Dialog.template = "Dialog"; class Interstellar extends Component {} +Interstellar.template = "Interstellar"; // Main root component class App extends Component { @@ -1444,12 +1229,13 @@ class App extends Component { }); } App.components = { Dialog , Interstellar }; +App.template = "App"; // Application setup -mount(App, { target: document.body }); +mount(App, document.body); `; -const PORTAL_XML = ` +const PORTAL_XML = /*xml*/` @@ -1474,21 +1260,21 @@ 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 PORTAL_CSS = ` +const PORTAL_CSS = /*css*/` .owl-modal-supercontainer { position: static; } @@ -1522,9 +1308,10 @@ const PORTAL_CSS = ` } .owl-interstellar { border: groove; -}` +}`; -const WMS = `// This example is slightly more complex than usual. We demonstrate +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: @@ -1534,10 +1321,11 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate // - minimal width/height // - better heuristic for initial window position // - ... -const { Component, useState, mount } = owl; -const { useRef } = owl.hooks; +const { Component, useState, mount, useRef } = owl; +const { useBus } = utils; class HelloWorld extends Component {} +HelloWorld.template = "HelloWorld"; class Counter extends Component { setup() { @@ -1548,6 +1336,7 @@ class Counter extends Component { this.state.value++; } } +Counter.template = "Counter"; class Window extends Component { @@ -1558,14 +1347,16 @@ class Window extends Component { } close() { - this.trigger("close-window", { id: this.props.info.id }); + this.env.wservice.close(this.props.info.id); } startDragAndDrop(ev) { this.updateZIndex(); this.el.classList.add('dragging'); - const offsetX = this.props.info.left - ev.pageX; - const offsetY = this.props.info.top - ev.pageY; + + const current = this.props.info; + const offsetX = current.left - ev.pageX; + const offsetY = current.top - ev.pageY; let left, top; const el = this.el; @@ -1581,77 +1372,39 @@ class Window extends Component { } function stopDnD() { window.removeEventListener("mousemove", moveWindow); - const options = { id: self.props.info.id, left, top }; self.el.classList.remove('dragging'); - self.trigger("set-window-position", options); + + if (top !== undefined && left !== undefined) { + current.top = top; + current.left = left + } } } updateZIndex() { - this.trigger("update-z-index", { id: this.props.info.id }); + this.env.wservice.updateZIndex(this.props.info, this.el); } } +Window.template = "Window"; class WindowManager extends Component { setup() { - this.windows = []; - this.nextId = 1; - this.currentZindex = 1; - this.nextLeft = 0; - this.nextTop = 0; - } - - addWindow(name) { - const info = this.env.windows.find(w => w.name === name); - this.nextLeft = this.nextLeft + 30; - this.nextTop = this.nextTop + 30; - this.windows.push({ - id: this.nextId++, - title: info.title, - width: info.defaultWidth, - height: info.defaultHeight, - top: this.nextTop, - left: this.nextLeft, - zindex: this.currentZindex++, - component: info.component - }); - this.render(); - } - - closeWindow(ev) { - const id = ev.detail.id; - delete this.constructor.components[id]; - const index = this.windows.findIndex(w => w.id === id); - this.windows.splice(index, 1); - this.render(); - } - - setWindowPosition(ev) { - const id = ev.detail.id; - const w = this.windows.find(w => w.id === id); - w.top = ev.detail.top; - w.left = ev.detail.left; - } - - updateZIndex(ev) { - const id = ev.detail.id; - const w = this.windows.find(w => w.id === id); - w.zindex = this.currentZindex++; - ev.target.style["z-index"] = w.zindex; + useBus(this.env.wservice, "update", () => this.render()); } } WindowManager.components = { Window }; +WindowManager.template = "WindowManager"; class App extends Component { setup() { - this.wmRef = useRef("wm"); + this.addWindow = this.addWindow.bind(this); } - addWindow(name) { - this.wmRef.comp.addWindow(name); + this.env.wservice.add(name); } } App.components = { WindowManager }; +App.template = "App"; const windows = [ { @@ -1670,11 +1423,62 @@ const windows = [ } ]; -App.env.windows = windows; -mount(App, { target: document.body }); +function windowService() { + let activeWindows = []; + let nextId = 0; + const bus = new owl.EventBus(); + + let nextTop = 0; + let nextLeft = 0; + let nextZIndex = 1; + + function add(name) { + const info = windows.find((w) => w.name === name); + + activeWindows.push({ + id: nextId++, + title: info.title, + width: info.defaultWidth, + height: info.defaultHeight, + top: nextTop, + left: nextLeft, + zindex: nextZIndex++, + component: info.component + }); + + bus.trigger("update"); + nextTop += 30; + nextLeft += 30; + } + + function close(id) { + activeWindows = activeWindows.filter(w => w.id !== id); + bus.trigger("update"); + } + + const wservice = Object.assign(bus, { + add, close, + updateZIndex(window, el) { + window.zindex = nextZIndex++; + el.style["z-index"] = window.zindex; + } + }); + + Object.defineProperty(wservice, "activeWindows", { + get() { return activeWindows; } + }) + return wservice; +} + +const env = { + wservice: windowService(), +}; + +mount(App, document.body, { env }); `; -const WMS_XML = ` +const WMS_XML =/*xml*/` +
    @@ -1683,20 +1487,17 @@ const WMS_XML = `
    -
    - +
    +
    - +
    @@ -1711,7 +1512,8 @@ const WMS_XML = ` `; -const WMS_CSS = `body { +const WMS_CSS= /*css*/` +body { margin: 0; } @@ -1782,19 +1584,19 @@ const WMS_CSS = `body { font-size: 20px; }`; -const SFC = `// This example illustrates how Owl enables single file components, +const SFC =/*js*/` +// 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 { Component, useState, tags, mount } = owl; -const { xml, css } = tags; +const { Component, useState, xml, css, mount } = owl; // Counter component const COUNTER_TEMPLATE = xml\` - \`; @@ -1821,7 +1623,7 @@ App.template = APP_TEMPLATE; App.components = { Counter }; // Application setup -mount(App, { target: document.body }); +mount(App, document.body); `; export const SAMPLES = [ @@ -1840,12 +1642,6 @@ export const SAMPLES = [ description: "Single File Components", code: SFC }, - { - description: "Animations", - code: ANIMATION, - xml: ANIMATION_XML, - css: ANIMATION_CSS - }, { description: "Lifecycle demo", code: LIFECYCLE_DEMO, @@ -1859,15 +1655,10 @@ export const SAMPLES = [ css: HOOKS_CSS }, { - description: "Context", - code: CONTEXT_JS, - xml: CONTEXT_XML, - }, - { - description: "Todo List App (with store)", - code: TODO_APP_STORE, - css: TODO_APP_STORE_CSS, - xml: TODO_APP_STORE_XML + description: "Todo List App (with reactivity)", + code: TODO_APP_REACTIVITY, + css: TODO_APP_REACTIVITY_CSS, + xml: TODO_APP_REACTIVITY_XML }, { description: "Responsive app", @@ -1882,16 +1673,10 @@ export const SAMPLES = [ css: SLOTS_CSS }, { - description: "Window Management System", - code: WMS, - xml: WMS_XML, - css: WMS_CSS, - }, - { - description: "Asynchronous components", - code: ASYNC_COMPONENTS, - xml: ASYNC_COMPONENTS_XML, - css: ASYNC_COMPONENTS_CSS + description: "Window Management System", + code: WMS, + xml: WMS_XML, + css: WMS_CSS, }, { description: "Portal (Dialog)", diff --git a/tools/playground/templates.xml b/tools/playground/templates.xml index c0c1300a..63dbb68d 100644 --- a/tools/playground/templates.xml +++ b/tools/playground/templates.xml @@ -1,8 +1,8 @@
    - - + + @@ -12,8 +12,7 @@
    + t-att-style="leftPaneStyle">