diff --git a/src/index.ts b/src/index.ts index a73587fe..f767bbbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,8 +16,6 @@ import { mainEventHandler } from "./component/handler"; import { Portal } from "./portal"; export type { Reactive } from "./reactivity"; - - config.shouldNormalizeDom = false; config.mainEventHandler = mainEventHandler; (UTILS as any).Portal = Portal; diff --git a/tools/playground/app.js b/tools/playground/app.js index 9ebdf292..83beee7c 100644 --- a/tools/playground/app.js +++ b/tools/playground/app.js @@ -39,18 +39,9 @@ function makeCodeIframe(js, css, xml) { const script = doc.createElement("script"); script.type = "module"; const content = ` - 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() { + (async function(TEMPLATES) { ${js} - })()`; + })(\`${sanitizedXML}\`)`; script.innerHTML = content; doc.body.appendChild(script); }); diff --git a/tools/playground/samples.js b/tools/playground/samples.js index 2d654edf..6e86c8c8 100644 --- a/tools/playground/samples.js +++ b/tools/playground/samples.js @@ -3,6 +3,8 @@ const COMPONENTS = /*js*/ ` const { Component, useState, mount } = owl; class Greeter extends Component { + static template = "Greeter"; + setup() { this.state = useState({ word: 'Hello' }); } @@ -11,20 +13,19 @@ class Greeter extends Component { this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'; } } -Greeter.template = "Greeter"; // Main root component -class App extends Component { - setup() { - this.state = useState({ name: 'World'}); - } +class Root extends Component { + static components = { Greeter }; + static template = "Root" + + setup() { + this.state = useState({ name: 'World'}); + } } -App.components = { Greeter }; -App.template = "App"; // Application setup -mount(App, document.body) -`; +mount(Root, document.body, { templates: TEMPLATES, dev: true });`; const COMPONENTS_XML = /*xml*/` @@ -32,9 +33,9 @@ const COMPONENTS_XML = /*xml*/` , -
+ -
+
`; @@ -57,28 +58,40 @@ const LIFECYCLE_DEMO = /*js*/` // 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, onWillStart, onMounted, onWillUnmount, onWillUpdateProps, onPatched, onWillPatch } = owl; +const { Component, useState, mount, useComponent, onWillStart, onMounted, onWillUnmount, onWillUpdateProps, onPatched, onWillPatch, onWillRender, onRendered, onWillDestroy} = owl; + +function useLogLifecycle() { + const component = useComponent(); + const name = component.constructor.name; + onWillStart(() => console.log(\`\${name}:willStart\`)); + onMounted(() => console.log(\`\${name}:mounted\`)); + onWillUpdateProps(() => console.log(\`\${name}:willUpdateProps\`)); + onWillRender(() => console.log(\`\${name}:willRender\`)); + onRendered(() => console.log(\`\${name}:rendered\`)); + onWillPatch(() => console.log(\`\${name}:willPatch\`)); + onPatched(() => console.log(\`\${name}:patched\`)); + onWillUnmount(() => console.log(\`\${name}:willUnmount\`)); + onWillDestroy(() => console.log(\`\${name}:willDestroy\`)); +} class DemoComponent extends Component { - setup() { - this.state = useState({ n: 0 }); - console.log("setup"); + static template = "DemoComponent"; - 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")); + setup() { + useLogLifecycle(); + this.state = useState({ n: 0 }); } increment() { this.state.n++; } } -DemoComponent.template = "DemoComponent"; -class App extends Component { +class Root extends Component { + static template = "Root"; + static components = { DemoComponent }; + setup() { + useLogLifecycle(); this.state = useState({ n: 0, flag: true }); } @@ -90,10 +103,8 @@ class App extends Component { this.state.flag = !this.state.flag; } } -App.components = { DemoComponent }; -App.template = "App"; -mount(App, document.body); +mount(Root, document.body, { templates: TEMPLATES, dev: true }); `; const LIFECYCLE_DEMO_XML = /*xml*/ ` @@ -104,7 +115,7 @@ const LIFECYCLE_DEMO_XML = /*xml*/ `
Props: , State: .
-
+
@@ -128,7 +139,7 @@ button { const HOOKS_DEMO = /*js*/ ` // In this example, we show how hooks can be used or defined. -const { mount, useState, onMounted, onWillUnmount } = owl; +const { Component, mount, useState, onWillDestroy } = owl; // We define here a custom behaviour: this hook tracks the state of the mouse // position @@ -136,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); }); @@ -151,7 +160,9 @@ function useMouse() { // Main root component -class App extends owl.Component { +class Root extends Component { + static template = "Root"; + setup() { // simple state hook (reactive object) this.counter = useState({ value: 0 }); @@ -164,15 +175,14 @@ class App extends owl.Component { this.counter.value++; } } -App.template = "App"; // Application setup -mount(App, document.body); +mount(Root, document.body, { templates: TEMPLATES, dev: true }); `; const HOOKS_DEMO_XML = /*xml*/ ` -
+
Mouse: ,
@@ -186,12 +196,13 @@ const HOOKS_CSS = /*css*/ `button { }`; const TODO_APP_REACTIVITY = /*js*/ ` + // This example is an implementation of the TodoList application, from the // www.todomvc.com project. This is a non trivial application with some // interesting user interactions. It uses the local storage for persistence. // // In this implementation, we use the owl reactivity mechanism. -const { Component, useState, mount, useRef, onPatched, onMounted, reactive } = owl; +const { Component, useState, mount, useRef, onPatched, onMounted, reactive, useEnv, useEffect } = owl; //------------------------------------------------------------------------------ // Constants, helpers @@ -201,85 +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); } //------------------------------------------------------------------------------ -// TodoItem +// Task store //------------------------------------------------------------------------------ -class TodoItem extends Component { +class TaskList { + constructor(tasks) { + this.tasks = tasks || []; + const taskIds = this.tasks.map((t) => t.id); + this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1; + } + + addTask(text) { + text = text.trim(); + if (text) { + const task = { + id: this.nextId++, + text: text, + isCompleted: false, + }; + this.tasks.push(task); + } + } + + toggleTask(task) { + task.isCompleted = !task.isCompleted; + } + + toggleTask(id) { + const task = this.tasks.find(t => t.id === id); + task.isCompleted = !task.isCompleted; + } + + toggleAll(value) { + for (let task of this.tasks) { + task.isCompleted = value; + } + } + + clearCompleted() { + const tasks = this.tasks.filter(t => t.isCompleted); + for (let task of tasks) { + this.deleteTask(task); + } + } + + deleteTask(id) { + const index = this.tasks.findIndex((t) => t.id === id); + this.tasks.splice(index, 1); + } + + updateTask(id, text) { + const value = text.trim(); + if (!value) { + this.deleteTask(id); + } else { + const task = this.tasks.find(t => t.id === id); + task.text = value; + } + } +} + +function createTaskStore() { + const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks)); + const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]"); + const taskStore = reactive(new TaskList(initialTasks), saveTasks); + saveTasks(); + return taskStore; +} + +//------------------------------------------------------------------------------ +// Todo +//------------------------------------------------------------------------------ +class Todo extends Component { + static template = "Todo"; + setup() { useAutofocus("input"); - this.todo = this.env.todo; - this.todoState = useState(this.todo.state); - this.state = useState({}); + 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.todo.updateTodo({title, id: this.props.id}); + updateText(text) { + this.store.updateTask(this.props.id, text); this.state.isEditing = false; } } -TodoItem.template = "TodoItem"; //------------------------------------------------------------------------------ -// TodoApp +// TodoList //------------------------------------------------------------------------------ -class TodoApp extends Component { +class TodoList extends Component { + static template = "TodoList"; + static components = { Todo }; + setup() { - this.todo = this.env.todo; - this.todoState = useState(this.todo.state); - + this.store = useStore(); this.state = useState({ filter: "all" }); - this.setFilter = this.setFilter.bind(this); } - get todos() { - return this.todoState.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 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 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() { @@ -289,9 +362,9 @@ class TodoApp extends Component { addTodo(ev) { if (ev.keyCode === ENTER_KEY) { - const title = ev.target.value; - if (title.trim()) { - this.todo.addTodo(title); + const text = ev.target.value; + if (text.trim()) { + this.store.addTask(text); } ev.target.value = ""; } @@ -301,108 +374,31 @@ class TodoApp extends Component { this.state.filter = filter; } } -TodoApp.components = { TodoItem }; -TodoApp.template = "TodoApp"; //------------------------------------------------------------------------------ // App Initialization //------------------------------------------------------------------------------ - -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); - try { - localStorage.setItem(key, str); - } catch (e) {}; - } - - function loadState() { - const localState = localStorage.getItem(key); - return localState ? JSON.parse(localState) : {}; - } - - return { state: reactiveState }; -} - -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 env = { store: createTaskStore() }; +mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true }); `; const TODO_APP_REACTIVITY_XML = /*xml*/ ` -
+

todos

-
- +
+
    - - + +
-
-
  • +
  • - + - +
    - +
  • `; @@ -821,8 +817,8 @@ html .clear-completed:active { `; const RESPONSIVE = /*js*/ ` -// In this example, we show how we can modify keys in the global environment to -// make a responsive application. +// 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 @@ -830,79 +826,115 @@ const RESPONSIVE = /*js*/ ` // // 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); +} -const { debounce, useBus } = utils; //------------------------------------------------------------------------------ // Components //------------------------------------------------------------------------------ -class Navbar extends owl.Component {} -Navbar.template = "Navbar"; +class Navbar extends owl.Component { + static template = "Navbar"; +} -class MobileSearchView extends owl.Component {} -MobileSearchView.template = "MobileSearchView"; +class MobileSearchView extends Component { + static template = "MobileSearchView"; +} -class ControlPanel extends owl.Component {} -ControlPanel.components = { MobileSearchView }; -ControlPanel.template = "ControlPanel"; +class ControlPanel extends Component { + static template = "ControlPanel"; + static components = { MobileSearchView }; + setup() { + this.ui = useUI(); + } +} -class AdvancedComponent extends owl.Component {} -AdvancedComponent.template = "AdvancedComponent"; +class AdvancedComponent extends Component { + static template = "AdvancedComponent"; +} -class FormView extends owl.Component {} -FormView.components = { AdvancedComponent }; -FormView.template = "FormView"; +class FormView extends Component { + static template = "FormView"; + static components = { AdvancedComponent }; + setup() { + this.ui = useUI(); + } +} + +class Chatter extends Component { + static template = "Chatter"; -class Chatter extends owl.Component { setup() { this.messages = Array.from(Array(100).keys()); } } -Chatter.template = "Chatter"; -class App extends owl.Component { - setup() { - useResponsive(); - } -} -App.template = "App"; -App.components = { Navbar, ControlPanel, FormView, Chatter }; +class Root extends Component { + static template = "Root"; + static components = { Navbar, ControlPanel, FormView, Chatter }; -//------------------------------------------------------------------------------ -// Responsive plugin -//------------------------------------------------------------------------------ - -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; + setup() { + this.ui = useUI(); + } } -function useResponsive() { - const comp = owl.useComponent(); - const responsive = comp.env.responsive; - useBus(responsive, "resize", () => comp.render()); -} //------------------------------------------------------------------------------ // Application Startup //------------------------------------------------------------------------------ -const env = {}; -env.responsive = responsiveService(env); +const env = { + ui: createUI() +}; -owl.mount(App, document.body, { env }); +mount(Root, document.body, { templates: TEMPLATES, env }); `; const RESPONSIVE_XML = /*xml*/` @@ -911,12 +943,12 @@ const RESPONSIVE_XML = /*xml*/`

    Control Panel

    - +

    Form View

    - +
    @@ -935,10 +967,10 @@ const RESPONSIVE_XML = /*xml*/` -
    +
    -
    +
    @@ -1010,53 +1042,56 @@ body { `; 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 { - setup() { - 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; + } } -Card.template = "Card"; class Counter extends Component { - setup() { - this.state = useState({val: 1}); - } + static template = "Counter"; + + setup() { + this.state = useState({val: 1}); + } - inc() { - this.state.val++; - } + inc() { + 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); - } +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}; -App.template = "App"; // Application setup -mount(App, document.body); +mount(Root, document.body, { templates: TEMPLATES, dev: true}); `; const SLOTS_XML = /*xml*/` @@ -1079,20 +1114,21 @@ const SLOTS_XML = /*xml*/`
    -
    +
    Content of card 1... [] - +
    Card 2... []
    - +
    -`; + +`; const SLOTS_CSS = /*css*/ ` .main { @@ -1147,20 +1183,21 @@ const FORM = /*js*/` const { Component, useState, mount } = owl; class Form extends Component { - setup() { - this.state = useState({ - text: "", - othertext: "", - number: 11, - color: "", - bool: false - }); - } + static template = "Form"; + + setup() { + this.state = useState({ + text: "", + othertext: "", + number: 11, + color: "", + bool: false + }); + } } -Form.template = "Form"; // Application setup -mount(Form, document.body); +mount(Form, document.body, { templates: TEMPLATES, dev: true }); `; const FORM_XML = /*xml*/ ` @@ -1203,113 +1240,6 @@ const FORM_XML = /*xml*/ ` `; -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, 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 { - state = useState({ - name: 'Portal used for Dialog (Modal)', - dialog: false, - text: 'Hello !', - }); -} -App.components = { Dialog , Interstellar }; -App.template = "App"; - -// Application setup -mount(App, document.body); -`; - -const PORTAL_XML = /*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 = /*css*/` -.owl-modal-supercontainer { - position: static; -} -.owl-modal-backdrop { - position: fixed; - top: 0; - left:0; - background-color: #000000; - opacity: 0.5; - width: 100vw; - height: 100vh; - z-index: 1000; -} -.owl-modal-container { - opacity:1; - z-index: 1050; - position: fixed; - top: 0; - left:0; - width: 100%; - height: 100%; -} -.owl-dialog-body { - max-width: 500px; - margin: 0 auto; - position: relative; - text-align: center; - padding: 2rem; - background-color: #FFFFFF; - max-height: 100%; -} -.owl-interstellar { - border: groove; -}`; - 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 @@ -1321,46 +1251,82 @@ const WMS = /*js*/` // - minimal width/height // - better heuristic for initial window position // - ... -const { Component, useState, mount, useRef } = owl; -const { useBus } = utils; +const { Component, useState, mount, useRef, reactive, useEnv, onMounted } = owl; -class HelloWorld extends Component {} -HelloWorld.template = "HelloWorld"; +// ----------------------------------------------------------------------------- +// Window manager code +// ----------------------------------------------------------------------------- -class Counter extends Component { - setup() { - this.state = useState({ value: 0 }); +class WindowManager { + // contains all components with metadata + static Windows = {}; + static nextZIndex = 1; + activeWindows = []; + nextId = 1; + + add(type) { + const Comp = WindowManager.Windows[type]; + const left = Math.round(Math.random()*(window.innerHeight - Comp.defaultHeight)); + const top = Math.round(Math.random()*(window.innerWidth - Comp.defaultWidth)); + this.activeWindows.push({ + id: this.nextId++, + title: Comp.defaultTitle, + width: Comp.defaultWidth, + height: Comp.defaultHeight, + top, + left, + Component: Comp, + }); } - - inc() { - this.state.value++; + + close(id) { + const index = this.activeWindows.findIndex(w => w.id === id); + this.activeWindows.splice(index, 1); } } -Counter.template = "Counter"; + +function createWindowService() { + return reactive(new WindowManager()); +} + +function useWindowService() { + const env = useEnv(); + return useState(env.windowService); +} + +// ----------------------------------------------------------------------------- +// Generic Window Component +// ----------------------------------------------------------------------------- class Window extends Component { + static template = "Window"; + + setup() { + this.windowService = useWindowService(); + this.root = useRef('root'); + onMounted(this.updateZIndex); + } get style() { - let { width, height, top, left, zindex } = this.props.info; - - return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`; + let { width, height, top, left } = this.props.info; + return \`width: $\{width}px;height: $\{height}px;top:$\{top}px;left:$\{left}px\`; } close() { - this.env.wservice.close(this.props.info.id); + this.windowService.close(this.props.info.id); } startDragAndDrop(ev) { this.updateZIndex(); - this.el.classList.add('dragging'); + const root = this.root; + const el = root.el; + el.classList.add('dragging'); const current = this.props.info; const offsetX = current.left - ev.pageX; const offsetY = current.top - ev.pageY; let left, top; - const el = this.el; - const self = this; window.addEventListener("mousemove", moveWindow); window.addEventListener("mouseup", stopDnD, { once: true }); @@ -1372,7 +1338,7 @@ class Window extends Component { } function stopDnD() { window.removeEventListener("mousemove", moveWindow); - self.el.classList.remove('dragging'); + el.classList.remove('dragging'); if (top !== undefined && left !== undefined) { current.top = top; @@ -1382,104 +1348,83 @@ class Window extends Component { } updateZIndex() { - this.env.wservice.updateZIndex(this.props.info, this.el); + this.root.el.style['z-index'] = WindowManager.neztZIndex++; } } -Window.template = "Window"; -class WindowManager extends Component { +// ----------------------------------------------------------------------------- +// Two concrete Window type implementations +// ----------------------------------------------------------------------------- + +class HelloWorld extends Component { + static template = "HelloWorld"; + static defaultTitle = "Hello Owl!"; + static defaultWidth = 200; + static defaultHeight = 100; +} + + +class Counter extends Component { + static template = "Counter"; + static defaultTitle = "Click Counter"; + static defaultWidth = 300; + static defaultHeight = 120; + + state = useState({ value: 0 }); + + inc() { + this.state.value++; + } +} + +// register window components +WindowManager.Windows.Hello = HelloWorld; +WindowManager.Windows.Counter = Counter; + +// ----------------------------------------------------------------------------- +// Window Container +// ----------------------------------------------------------------------------- + +class WindowContainer extends Component { + static template = "WindowContainer"; + static components = { Window }; + setup() { - useBus(this.env.wservice, "update", () => this.render()); + this.windowService = useWindowService(); } } -WindowManager.components = { Window }; -WindowManager.template = "WindowManager"; -class App extends Component { +// ----------------------------------------------------------------------------- +// Root Component +// ----------------------------------------------------------------------------- + +class Root extends Component { + static template = "Root"; + static components = { WindowContainer }; + setup() { - this.addWindow = this.addWindow.bind(this); + this.windowService = useWindowService(); } - addWindow(name) { - this.env.wservice.add(name); + + addWindow(type) { + this.windowService.add(type); } } -App.components = { WindowManager }; -App.template = "App"; -const windows = [ - { - name: "Hello", - title: "Hello", - component: HelloWorld, - defaultWidth: 200, - defaultHeight: 100 - }, - { - name: "Counter", - title: "Click Counter", - component: Counter, - defaultWidth: 300, - defaultHeight: 120 - } -]; - -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; -} +// ----------------------------------------------------------------------------- +// Setup +// ----------------------------------------------------------------------------- const env = { - wservice: windowService(), + windowService: createWindowService(), }; -mount(App, document.body, { env }); +mount(Root, document.body, { templates: TEMPLATES, env, dev: true }); `; const WMS_XML =/*xml*/` -
    +
    × @@ -1487,26 +1432,26 @@ const WMS_XML =/*xml*/`
    -
    - - +
    + +
    -
    - +
    +
    - World + Some content here...
    - +
    @@ -1585,45 +1530,34 @@ body { }`; 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. +// This example illustrates how one can write Owl components with +// inline templates. 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\` -
    - - -
    \`; - -class App extends Component {} -App.template = APP_TEMPLATE; -App.components = { Counter }; +// Root +class Root extends Component { + static template = xml\` +
    + + +
    \`; + + static components = { Counter }; +} // Application setup -mount(App, document.body); +mount(Root, document.body, { templates: TEMPLATES, dev: true}); `; export const SAMPLES = [ @@ -1639,7 +1573,7 @@ export const SAMPLES = [ xml: FORM_XML }, { - description: "Single File Components", + description: "Inline templates", code: SFC }, { @@ -1649,7 +1583,7 @@ export const SAMPLES = [ css: LIFECYCLE_CSS }, { - description: "Hooks", + description: "Customized hook", code: HOOKS_DEMO, xml: HOOKS_DEMO_XML, css: HOOKS_CSS @@ -1678,10 +1612,4 @@ export const SAMPLES = [ xml: WMS_XML, css: WMS_CSS, }, - { - description: "Portal (Dialog)", - code: PORTAL_COMPONENTS, - xml: PORTAL_XML, - css: PORTAL_CSS, - }, ];