const COMPONENTS = `// In this example, we show how components can be defined and created. const { Component, useState } = owl; class Greeter extends Component { constructor() { super(...arguments); this.state = useState({ word: 'Hello' }); } toggle() { this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'; } } // Main root component class App extends Component { constructor() { super(...arguments); this.state = useState({ name: 'World'}); } } App.components = { Greeter }; // Application setup const app = new App(); app.mount(document.body); `; const COMPONENTS_XML = `
,
`; const COMPONENTS_CSS = `.greeter { font-size: 20px; width: 300px; height: 100px; margin: 5px; text-align: center; line-height: 100px; background-color: #eeeeee; 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 } = owl; class Counter extends Component { constructor() { super(...arguments); this.state = useState({ value: 0 }); } increment() { this.state.value++; } } class App extends Component { constructor() { super(...arguments); 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 }; const app = new App(); app.mount(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

Click
`; 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 // // 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 } = owl; class DemoComponent extends Component { constructor() { super(...arguments); this.state = useState({ n: 0 }); console.log("constructor"); } 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"); } increment() { this.state.n++; } } class App extends Component { constructor() { super(...arguments); this.state = useState({ n: 0, flag: true }); } increment() { this.state.n++; } toggleSubComponent() { this.state.flag = !this.state.flag; } } App.components = { DemoComponent }; const app = new App(); app.mount(document.body); `; const LIFECYCLE_DEMO_XML = `
Demo Sub Component
(click on me to update me)
Props: , State: .
`; const LIFECYCLE_CSS = `button { font-size: 18px; margin: 5px; } .demo { margin: 10px; padding: 10px; background-color: #dddddd; width: 250px; }`; const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined. const {useState, onMounted, onWillUnmount} = owl.hooks; // We define here a custom behaviour: this hook tracks the state of the mouse // position function useMouse() { const position = useState({x:0, y: 0}); function update(e) { position.x = e.clientX; position.y = e.clientY; } onMounted(() => { window.addEventListener('mousemove', update); }); onWillUnmount(() => { window.removeEventListener('mousemove', update); }); return position; } // Main root component class App extends owl.Component { constructor() { super(...arguments); // simple state hook (reactive object) this.counter = useState({ value: 0 }); // this hooks is bound to the 'mouse' property. this.mouse = useMouse(); } increment() { this.counter.value++; } } // Application setup const app = new App(); app.mount(document.body); `; const HOOKS_DEMO_XML = `
Mouse: ,
`; const HOOKS_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 } = owl; const { useContext } = owl.hooks; 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; const app = new App(); app.mount(document.body); `; const CONTEXT_XML = `
`; const TODO_APP_STORE = `// 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 } = owl; const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks; //------------------------------------------------------------------------------ // Constants, helpers //------------------------------------------------------------------------------ const ENTER_KEY = 13; 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); } //------------------------------------------------------------------------------ // 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) { 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 { constructor() { super(...arguments); useAutofocus("input"); this.state = useState({ isEditing: false }); this.dispatch = useDispatch(); } handleKeyup(ev) { if (ev.keyCode === ENTER_KEY) { this.updateTitle(ev.target.value); } if (ev.keyCode === ESC_KEY) { ev.target.value = this.props.title; this.state.isEditing = false; } } handleBlur(ev) { this.updateTitle(ev.target.value); } updateTitle(title) { this.dispatch("updateTodo", {title, id: this.props.id}); this.state.isEditing = false; } } //------------------------------------------------------------------------------ // TodoApp //------------------------------------------------------------------------------ class TodoApp extends Component { constructor() { super(...arguments); 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 allChecked() { return this.todos.every(todo => todo.completed); } get remaining() { return this.todos.filter(todo => !todo.completed).length; } get remainingText() { const items = this.remaining < 2 ? "item" : "items"; return \` \${items} left\`; } addTodo(ev) { if (ev.keyCode === ENTER_KEY) { const title = ev.target.value; if (title.trim()) { this.dispatch("addTodo", title); } ev.target.value = ""; } } setFilter(filter) { 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(); const app = new TodoApp(); app.mount(document.body); `; const TODO_APP_STORE_XML = `

todos

  • `; const TODO_APP_STORE_CSS = `html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #4d4d4d; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 300; } :focus { outline: 0; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp h1 { position: absolute; top: -155px; width: 100%; font-size: 100px; font-weight: 100; text-align: center; color: rgba(175, 47, 47, 0.15); -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; border: 0; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } .toggle-all { width: 1px; height: 1px; border: none; /* Mobile Safari */ opacity: 0; position: absolute; right: 100%; bottom: 100%; } .toggle-all + label { width: 60px; height: 34px; font-size: 0; position: absolute; top: -52px; left: -13px; -webkit-transform: rotate(90deg); transform: rotate(90deg); } .toggle-all + label:before { content: "❯"; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked + label:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: calc(100% - 43px); padding: 12px 16px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle { opacity: 0; } .todo-list li .toggle + label { /* Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ */ background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center left; } .todo-list li .toggle:checked + label { background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E"); } .todo-list li label { word-break: break-all; padding: 15px 15px 15px 60px; display: block; line-height: 1.2; transition: color 0.4s; } .todo-list li.completed label { color: #d9d9d9; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: "×"; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { color: #777; padding: 10px 15px; height: 20px; text-align: center; border-top: 1px solid #e6e6e6; } .footer:before { content: ""; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #bfbfbf; font-size: 10px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio: 0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } .filters a { cursor: pointer; } `; const RESPONSIVE = `// 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 // to resize events and update the env if needed. Then, the whole interface // will be updated, creating and destroying components as needed. // // To see this in action, try resizing the window. The application will switch // to mobile mode whenever it has less than 768px. //------------------------------------------------------------------------------ // Components //------------------------------------------------------------------------------ class Navbar extends owl.Component {} class MobileSearchView extends owl.Component {} class ControlPanel extends owl.Component {} ControlPanel.components = { MobileSearchView }; class AdvancedComponent extends owl.Component {} class FormView extends owl.Component {} FormView.components = { AdvancedComponent }; class Chatter extends owl.Component { constructor() { super(...arguments); this.messages = Array.from(Array(100).keys()); } } class App extends owl.Component {} 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(); } }, 15); window.addEventListener("resize", updateEnv); } //------------------------------------------------------------------------------ // Application Startup //------------------------------------------------------------------------------ setupResponsivePlugin(App.env); const app = new App(); app.mount(document.body); `; const RESPONSIVE_XML = `

    Control Panel

    Form View

    Chatter

    Message
    Mobile searchview
    This component is only created in desktop mode.
    `; const RESPONSIVE_CSS = `body { margin: 0; } .app { height: 100%; flex-direction: column; } .app.desktop { display: flex; } .navbar { flex: 0 0 30px; height: 30px; background-color: cadetblue; color: white; line-height: 30px; } .controlpanel { flex: 0 0 100px; height: 100px; background-color: #dddddd; padding: 8px; } .content-wrapper { flex: 1 1 auto; position: relative; } .content { display: flex; position: absolute; top: 0; right: 0; bottom: 0; left: 0; } .formview { overflow-y: auto; flex: 1 1 60%; min-height: 200px; padding: 8px; } .chatter { overflow-y: auto; flex: 1 1 40%; background-color: #eeeeee; color: #333333; padding: 8px; } `; const SLOTS = `// 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. // // 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 const { Component, useState } = owl; class Card extends Component { constructor() { super(...arguments); this.state = useState({ showContent: true }); } toggleDisplay() { this.state.showContent = !this.state.showContent; } } class Counter extends Component { constructor() { super(...arguments); this.state = useState({val: 1}); } inc() { this.state.val++; } } // Main root component class App extends Component { constructor() { super(...arguments); this.state = useState({a: 1, b: 3}); } inc(key, delta) { this.state[key] += delta; } } App.components = {Card, Counter}; // Application setup const app = new App(); app.mount(document.body);`; const SLOTS_XML = `
    Content of card 1... []
    Card 2... []
    `; const SLOTS_CSS = `.main { display: flex; } .card { display: flex; flex-direction: column; background-color: #eeeeee; width: 200px; height: 100px; margin: 10px; border: 1px solid gray; } .card.full { height: 100px; } .card.small { height: 25px; } .card-title { flex: 0 0 25px; font-weight: bold; background-color: darkcyan; color: white; padding: 2px; } .card-title button { float: right; } .card-content { flex: 1 1 auto; padding: 5px; border-top: 1px solid white; } .card-footer { border-top: 1px solid white; }`; const 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 } = 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}; const app = new App(); app.mount(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 // 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 } = owl; class Form extends Component { constructor() { super(...arguments); this.state = useState({ text: "", othertext: "", number: 11, color: "", bool: false }); } } // Application setup const form = new Form(); form.mount(document.body); `; const FORM_XML = `

    Form

    Text (immediate):
    Other text (lazy):
    Number:
    Boolean:
    Color, with a select:
    Color, with radio buttons:

    State

    Text:
    Other Text:
    Number:
    Boolean: TrueFalse
    Color:
    `; const PORTAL_COMPONENTS = ` // This shows the expected use case of Portal // which is to implement something similar // to bootstrap modal const { Component, useState } = 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 const app = new App(); app.mount(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 PORTAL_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 = `// This example is slightly more complex than usual. We demonstrate // here a way to manage sub windows in Owl, declaratively. This is still just a // demonstration. Managing windows can be as complex as we want. For example, // we could implement the following features: // - resizing windows // - minimizing windows // - configuration options for windows to make a window non resizeable // - minimal width/height // - better heuristic for initial window position // - ... const { Component, useState } = owl; const { useRef } = owl.hooks; class HelloWorld extends Component {} class Counter extends Component { constructor() { super(...arguments); this.state = useState({ value: 0 }); } inc() { this.state.value++; } } class Window extends Component { 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}\`; } close() { this.trigger("close-window", { id: 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; let left, top; const el = this.el; const self = this; window.addEventListener("mousemove", moveWindow); window.addEventListener("mouseup", stopDnD, { once: true }); function moveWindow(ev) { left = Math.max(offsetX + ev.pageX, 0); top = Math.max(offsetY + ev.pageY, 0); el.style.left = \`\${left}px\`; el.style.top = \`\${top}px\`; } function stopDnD() { window.removeEventListener("mousemove", moveWindow); const options = { id: self.props.info.id, left, top }; self.el.classList.remove('dragging'); self.trigger("set-window-position", options); } } updateZIndex() { this.trigger("update-z-index", { id: this.props.info.id }); } } class WindowManager extends Component { constructor() { super(...arguments); 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; } } WindowManager.components = { Window }; class App extends Component { constructor() { super(...arguments); this.wmRef = useRef("wm"); } addWindow(name) { this.wmRef.comp.addWindow(name); } } App.components = { WindowManager }; const windows = [ { name: "Hello", title: "Hello", component: HelloWorld, defaultWidth: 200, defaultHeight: 100 }, { name: "Counter", title: "Click Counter", component: Counter, defaultWidth: 300, defaultHeight: 120 } ]; App.env.windows = windows; const app = new App(); app.mount(document.body); `; const WMS_XML = `
    ×
    World
    `; const WMS_CSS = `body { margin: 0; } .app { width: 100%; height: 100%; display: grid; grid-template-rows: auto 50px; } .window-manager { position: relative; width: 100%; height: 100%; background-color: #eeeeee; overflow: hidden; } .menubar { background-color: #875a7b; color: white; } .menubar button { height: 40px; font-size: 18px; margin: 5px; } .window { display: grid; grid-template-rows: 30px auto; border: 1px solid gray; background-color: white; position: absolute; box-shadow: 1px 1px 2px 1px grey; } .window.dragging { opacity: 0.75; } .window .header { background-color: #875a7b; display: grid; grid-template-columns: auto 24px; color: white; line-height: 30px; padding-left: 5px; cursor: default; user-select: none; } .window .header .close { cursor: pointer; font-size: 22px; padding-left: 4px; padding-right: 4px; font-weight: bold; } .counter { font-size: 20px; } .counter button { width: 80px; height:40px; font-size: 20px; }`; export const SAMPLES = [ { description: "Components", code: COMPONENTS, xml: COMPONENTS_XML, css: COMPONENTS_CSS }, { description: "Form Input Bindings", code: FORM, xml: FORM_XML }, { description: "Animations", code: ANIMATION, xml: ANIMATION_XML, css: ANIMATION_CSS }, { description: "Lifecycle demo", code: LIFECYCLE_DEMO, xml: LIFECYCLE_DEMO_XML, css: LIFECYCLE_CSS }, { description: "Hooks", code: HOOKS_DEMO, xml: HOOKS_DEMO_XML, 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: "Responsive app", code: RESPONSIVE, css: RESPONSIVE_CSS, xml: RESPONSIVE_XML }, { description: "Slots And Generic Components", code: SLOTS, xml: SLOTS_XML, css: SLOTS_CSS }, { description: "Window Management System", code: WMS, xml: WMS_XML, css: WMS_CSS, }, { description: "Asynchronous components", code: ASYNC_COMPONENTS, xml: ASYNC_COMPONENTS_XML, css: ASYNC_COMPONENTS_CSS }, { description: "Portal (Dialog)", code: PORTAL_COMPONENTS, xml: PORTAL_XML, css: PORTAL_CSS, }, ];