[IMP] owl: update to v0.23.0

This commit is contained in:
Géry Debongnie
2019-10-16 22:42:20 +02:00
parent 58d03f6077
commit 1571a16cfd
3 changed files with 653 additions and 483 deletions
+402 -351
View File
File diff suppressed because it is too large Load Diff
+80 -20
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
const {useState, useRef} = owl.hooks; const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -150,6 +150,51 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
return zip.generateAsync({ type: "blob" }); return zip.generateAsync({ type: "blob" });
} }
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
function loadSamples() {
let result = SAMPLES.slice();
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.Component.current;
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tabbed editor // Tabbed editor
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -157,13 +202,14 @@ class TabbedEditor extends owl.Component {
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
this.state = useState({ this.state = useState({
currentTab: props.js ? "js" : props.xml ? "xml" : "css" currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
}); });
this.setTab = owl.utils.debounce(this.setTab, 250, true); this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {}; this.sessions = {};
this._setupSessions(props); this._setupSessions(props);
this.editorNode = useRef("editor"); this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this);
} }
mounted() { mounted() {
@@ -175,18 +221,14 @@ class TabbedEditor extends owl.Component {
this.editor.setSession(this.sessions[this.state.currentTab]); this.editor.setSession(this.sessions[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4; const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize); this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", () => { this.editor.on("blur", this._updateCode);
const editorValue = this.editor.getValue(); this.interval = setInterval(this._updateCode, 3000);
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
});
} }
willUnmount() {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
}
willUpdateProps(nextProps) { willUpdateProps(nextProps) {
this._setupSessions(nextProps); this._setupSessions(nextProps);
} }
@@ -195,14 +237,16 @@ class TabbedEditor extends owl.Component {
const session = this.sessions[this.state.currentTab]; const session = this.sessions[this.state.currentTab];
let content = this.props[this.state.currentTab]; let content = this.props[this.state.currentTab];
if (content === false) { if (content === false) {
const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css"; const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab]; content = this.props[tab];
this.state.currentTab = tab; this.state.currentTab = tab;
} }
if (this.editor.getValue() !== content) {
session.setValue(content, -1); session.setValue(content, -1);
this.editor.setSession(session); this.editor.setSession(session);
this.editor.resize(); this.editor.resize();
} }
}
setTab(tab) { setTab(tab) {
if (this.state.currentTab !== tab) { if (this.state.currentTab !== tab) {
@@ -230,7 +274,7 @@ class TabbedEditor extends owl.Component {
_setupSessions(props) { _setupSessions(props) {
for (let tab of ["js", "xml", "css"]) { for (let tab of ["js", "xml", "css"]) {
if (props[tab] && !this.sessions[tab]) { if (props[tab] !== false && !this.sessions[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]); this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false); this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4; const tabSize = tab === "xml" ? 2 : 4;
@@ -239,6 +283,17 @@ class TabbedEditor extends owl.Component {
} }
} }
} }
_updateCode() {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -248,12 +303,13 @@ class App extends owl.Component {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.version = owl.__info__.version; this.version = owl.__info__.version;
this.SAMPLES = SAMPLES; this.SAMPLES = useSamples();
this.isDirty = false;
this.state = useState({ this.state = useState({
js: SAMPLES[0].code, js: this.SAMPLES[0].code,
css: SAMPLES[0].css || "", css: this.SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML, xml: this.SAMPLES[0].xml || DEFAULT_XML,
error: false, error: false,
displayWelcome: true, displayWelcome: true,
splitLayout: true, splitLayout: true,
@@ -302,10 +358,12 @@ class App extends owl.Component {
} }
setSample(ev) { setSample(ev) {
const sample = SAMPLES.find(s => s.description === ev.target.value); const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code; this.state.js = sample.code;
this.state.css = sample.css || ""; this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML; this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
this.isDirty = false;
} }
get leftPaneStyle() { get leftPaneStyle() {
@@ -338,7 +396,10 @@ class App extends owl.Component {
}); });
} }
updateCode(ev) { updateCode(ev) {
if (this.state[ev.detail.type] !== ev.detail.value) {
this.state[ev.detail.type] = ev.detail.value; this.state[ev.detail.type] = ev.detail.value;
this.isDirty = true;
}
} }
toggleLayout() { toggleLayout() {
this.state.splitLayout = !this.state.splitLayout; this.state.splitLayout = !this.state.splitLayout;
@@ -363,7 +424,6 @@ class App extends owl.Component {
} }
App.components = { TabbedEditor }; App.components = { TabbedEditor };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application initialization // Application initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+150 -91
View File
@@ -318,6 +318,66 @@ const HOOKS_CSS = `button {
font-size: 16px; 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 {
theme = useContext(this.env.themeContext);
get style () {
const theme = this.theme;
return \`background-color: \${theme.background}; color: \${theme.foreground}\`;
}
}
class Toolbar extends Component {
static components = { ToolbarButton };
}
// Main root component
class App extends Component {
static components = { Toolbar };
toggleTheme() {
const { background, foreground } = this.env.themeContext.state;
this.env.themeContext.state.background = foreground;
this.env.themeContext.state.foreground = background;
}
}
// Application setup
const themeContext = new Context({
background: '#000',
foreground: '#fff',
});
const env = {
qweb: new owl.QWeb(TEMPLATES),
themeContext: themeContext,
};
const app = new App(env);
app.mount(document.body);
`;
const CONTEXT_XML = `<templates>
<button t-name="ToolbarButton" t-att-style="style">
<t t-esc="props.name"/>
</button>
<div t-name="Toolbar">
<ToolbarButton name="'A'"/>
<ToolbarButton name="'B'"/>
<ToolbarButton name="'C'"/>
</div>
<div t-name="App">
<button t-on-click="toggleTheme">Toggle Mode</button>
<Toolbar/>
</div>
</templates>
`;
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the 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 // www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence. // interesting user interactions. It uses the local storage for persistence.
@@ -325,99 +385,94 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
// In this implementation, we use the owl Store class to manage the state. It // In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store. // is very similar to the VueX store.
const { Component, useState } = owl; const { Component, useState } = owl;
const { useRef } = owl.hooks; const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers
//------------------------------------------------------------------------------
const ENTER_KEY = 13; const ENTER_KEY = 13;
const ESC_KEY = 27; const ESC_KEY = 27;
const LOCALSTORAGE_KEY = "todomvc"; 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 Definition // Store
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const initialState = { todos: [], nextId: 1};
const actions = { const actions = {
addTodo({ state }, title) { addTodo({ state }, title) {
state.todos.push({ const todo = {
id: state.nextId++, id: state.nextId++,
title, title,
completed: false completed: false
}); }
state.todos.push(todo);
}, },
removeTodo({ state }, id) { removeTodo({ state }, id) {
const index = state.todos.findIndex(t => t.id === id); const index = state.todos.findIndex(t => t.id === id);
state.todos.splice(index, 1); state.todos.splice(index, 1);
}, },
toggleTodo({ state, dispatch }, id) { updateTodo({state, dispatch}, {id, title}) {
const value = title.trim();
if (!value) {
dispatch('removeTodo', id);
} else {
const todo = state.todos.find(t => t.id === id); const todo = state.todos.find(t => t.id === id);
dispatch("editTodo", { id, completed: !todo.completed }); todo.title = value;
}
},
toggleTodo({ state }, id) {
const todo = state.todos.find(t => t.id === id);
todo.completed = !todo.completed;
}, },
clearCompleted({ state, dispatch }) { clearCompleted({ state, dispatch }) {
state.todos for (let todo of state.todos) {
.filter(todo => todo.completed) if (todo.completed) {
.forEach(todo => {
dispatch("removeTodo", todo.id); dispatch("removeTodo", todo.id);
}); }
}
}, },
toggleAll({ state, dispatch }, completed) { toggleAll({ state, dispatch }, completed) {
state.todos.forEach(todo => { for (let todo of state.todos) {
dispatch("editTodo", {
id: todo.id,
completed
});
});
},
editTodo({ state }, { id, title, completed }) {
const todo = state.todos.find(t => t.id === id);
if (title !== undefined) {
todo.title = title;
}
if (completed !== undefined) {
todo.completed = completed; todo.completed = completed;
} }
} },
}; };
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) : { todos: [], nextId: 1};
}
function makeStore() {
const state = loadState();
const store = new owl.store.Store({ state, actions });
store.on("update", null, () => saveState(store.state));
return store;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoItem // TodoItem
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoItem extends Component { class TodoItem extends Component {
state = useState({ isEditing: false }); state = useState({ isEditing: false });
inputRef = useRef("input"); dispatch = useDispatch();
removeTodo() { constructor(...args) {
this.env.store.dispatch("removeTodo", this.props.id); super(...args);
useAutofocus("input");
} }
toggleTodo() { editTodo() {
this.env.store.dispatch("toggleTodo", this.props.id);
}
async editTodo() {
this.state.isEditing = true; this.state.isEditing = true;
} }
focusInput() {
this.inputRef.el.value = "";
this.inputRef.el.focus();
this.inputRef.el.value = this.props.title;
}
handleKeyup(ev) { handleKeyup(ev) {
if (ev.keyCode === ENTER_KEY) { if (ev.keyCode === ENTER_KEY) {
this.updateTitle(ev.target.value); this.updateTitle(ev.target.value);
@@ -433,48 +488,34 @@ class TodoItem extends Component {
} }
updateTitle(title) { updateTitle(title) {
const value = title.trim(); this.dispatch("updateTodo", {title, id: this.props.id});
if (!value) {
this.removeTodo(this.props.id);
} else {
this.env.store.dispatch("editTodo", {
id: this.props.id,
title: value
});
this.state.isEditing = false; this.state.isEditing = false;
} }
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoApp // TodoApp
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoApp extends owl.store.ConnectedComponent { class TodoApp extends Component {
static components = { TodoItem }; static components = { TodoItem };
state = useState({ filter: "all" }); state = useState({ filter: "all" });
todos = useStore(state => state.todos);
dispatch = useDispatch();
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
get visibleTodos() { get visibleTodos() {
let todos = this.storeProps.todos; switch (this.state.filter) {
if (this.state.filter === "active") { case "active": return this.todos.filter(t => !t.completed);
todos = todos.filter(t => !t.completed); case "completed": return this.todos.filter(t => t.completed);
case "all": return this.todos;
} }
if (this.state.filter === "completed") {
todos = todos.filter(t => t.completed);
}
return todos;
} }
get allChecked() { get allChecked() {
return this.storeProps.todos.every(todo => todo.completed); return this.todos.every(todo => todo.completed);
} }
get remaining() { get remaining() {
return this.storeProps.todos.filter(todo => !todo.completed).length; return this.todos.filter(todo => !todo.completed).length;
} }
get remainingText() { get remainingText() {
@@ -500,12 +541,25 @@ class TodoApp extends owl.store.ConnectedComponent {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// App Initialization // App Initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const store = makeStore(); function saveState(state) {
const qweb = new owl.QWeb(TEMPLATES); const str = JSON.stringify(state);
const env = { window.localStorage.setItem(LOCALSTORAGE_KEY, str);
qweb, }
store,
}; function loadState() {
const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
return localState ? JSON.parse(localState) : initialState;
}
function makeEnv() {
const state = loadState();
const store = new owl.Store({ state, actions });
store.on("update", null, () => saveState(store.state));
const qweb = new owl.QWeb(TEMPLATES);
return { qweb, store };
}
const env = makeEnv();
const app = new TodoApp(env); const app = new TodoApp(env);
app.mount(document.body); app.mount(document.body);
`; `;
@@ -516,7 +570,7 @@ const TODO_APP_STORE_XML = `<templates>
<h1>todos</h1> <h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/> <input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header> </header>
<section class="main" t-if="storeProps.todos.length"> <section class="main" t-if="todos.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="dispatch('toggleAll', !allChecked)"/> <input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="dispatch('toggleAll', !allChecked)"/>
<label for="toggle-all"></label> <label for="toggle-all"></label>
<ul class="todo-list"> <ul class="todo-list">
@@ -525,7 +579,7 @@ const TODO_APP_STORE_XML = `<templates>
</t> </t>
</ul> </ul>
</section> </section>
<footer class="footer" t-if="storeProps.todos.length"> <footer class="footer" t-if="todos.length">
<span class="todo-count"> <span class="todo-count">
<strong> <strong>
<t t-esc="remaining"/> <t t-esc="remaining"/>
@@ -543,7 +597,7 @@ const TODO_APP_STORE_XML = `<templates>
<a t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a> <a t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li> </li>
</ul> </ul>
<button class="clear-completed" t-if="storeProps.todos.length gt remaining" t-on-click="dispatch('clearCompleted')"> <button class="clear-completed" t-if="todos.length gt remaining" t-on-click="dispatch('clearCompleted')">
Clear completed Clear completed
</button> </button>
</footer> </footer>
@@ -551,13 +605,13 @@ const TODO_APP_STORE_XML = `<templates>
<li t-name="TodoItem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}"> <li t-name="TodoItem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}">
<div class="view"> <div class="view">
<input class="toggle" type="checkbox" t-on-change="toggleTodo" t-att-checked="props.completed"/> <input class="toggle" type="checkbox" t-on-change="dispatch('toggleTodo', props.id)" t-att-checked="props.completed"/>
<label t-on-dblclick="editTodo"> <label t-on-dblclick="editTodo">
<t t-esc="props.title"/> <t t-esc="props.title"/>
</label> </label>
<button class="destroy" t-on-click="removeTodo"></button> <button class="destroy" t-on-click="dispatch('removeTodo', props.id)"></button>
</div> </div>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-mounted="focusInput" t-on-blur="handleBlur"/> <input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
</li> </li>
</templates>`; </templates>`;
@@ -1655,6 +1709,11 @@ export const SAMPLES = [
xml: HOOKS_DEMO_XML, xml: HOOKS_DEMO_XML,
css: HOOKS_CSS css: HOOKS_CSS
}, },
{
description: "Context",
code: CONTEXT_JS,
xml: CONTEXT_XML,
},
{ {
description: "Todo List App (with store)", description: "Todo List App (with store)",
code: TODO_APP_STORE, code: TODO_APP_STORE,