[IMP] owl: update to v0.19.0

This commit is contained in:
Géry Debongnie
2019-08-28 12:02:00 +02:00
parent acc85a52fc
commit 507163d708
2 changed files with 444 additions and 224 deletions
+151 -150
View File
@@ -1109,18 +1109,6 @@
function setTextContent(node, text) { function setTextContent(node, text) {
node.textContent = text; node.textContent = text;
} }
function getTextContent(node) {
return node.textContent;
}
function isElement(node) {
return node.nodeType === 1;
}
function isText(node) {
return node.nodeType === 3;
}
function isComment(node) {
return node.nodeType === 8;
}
const htmlDomApi = { const htmlDomApi = {
createElement, createElement,
createElementNS, createElementNS,
@@ -1133,10 +1121,6 @@
nextSibling, nextSibling,
tagName, tagName,
setTextContent, setTextContent,
getTextContent,
isElement,
isText,
isComment
}; };
function addNS(data, children, sel) { function addNS(data, children, sel) {
data.ns = "http://www.w3.org/2000/svg"; data.ns = "http://www.w3.org/2000/svg";
@@ -1312,12 +1296,12 @@
this._addTemplate(name, doc.firstChild); this._addTemplate(name, doc.firstChild);
} }
/** /**
* Load templates from a xml (as a string). This will look up for the first * Load templates from a xml (as a string or xml document). This will look up
* <templates> tag, and will consider each child of this as a template, with * for the first <templates> tag, and will consider each child of this as a
* the name given by the t-name attribute. * template, with the name given by the t-name attribute.
*/ */
addTemplates(xmlstr) { addTemplates(xmlstr) {
const doc = parseXML(xmlstr); const doc = typeof xmlstr === 'string' ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0]; const templates = doc.getElementsByTagName("templates")[0];
if (!templates) { if (!templates) {
return; return;
@@ -2752,7 +2736,14 @@
ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`); ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`);
ctx.addElse(); ctx.addElse();
// need to update component // need to update component
const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue"; let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
}
if (QWeb.dev) { if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`); ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
} }
@@ -2958,7 +2949,10 @@
renderPromise: null, renderPromise: null,
renderProps: props || null, renderProps: props || null,
boundHandlers: {}, boundHandlers: {},
mountedHandlers: {} mountedHandlers: {},
observer: null,
render: null,
classObj: null
}; };
} }
/** /**
@@ -3054,44 +3048,57 @@
* *
* Note that a component can be mounted an unmounted several times * Note that a component can be mounted an unmounted several times
*/ */
async mount(target) { async mount(target, renderBeforeRemount = false) {
if (this.__owl__.isMounted) { const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return; return;
} }
if (!this.__owl__.vnode) { if (!__owl__.vnode) {
// we use the fact that renderId === 1 as a way to determine that the
// component is mounted for the first time
const vnode = await this.__prepare(); const vnode = await this.__prepare();
if (this.__owl__.isDestroyed) { if (__owl__.isDestroyed) {
// component was destroyed before we get here... // component was destroyed before we get here...
return; return;
} }
this.__patch(vnode); this.__patch(vnode);
} }
else if (renderBeforeRemount) {
const patchQueue = [];
await this.__render(false, patchQueue, undefined, undefined);
this.__applyPatchQueue(patchQueue);
}
target.appendChild(this.el); target.appendChild(this.el);
if (document.body.contains(target)) { if (document.body.contains(target)) {
this.__callMounted(); this.__callMounted();
} }
} }
/**
* The unmount method is the opposite of the mount method. It is useful
* to call willUnmount calls and remove the component from the DOM.
*/
unmount() { unmount() {
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
this.__callWillUnmount(); this.__callWillUnmount();
this.el.remove(); this.el.remove();
} }
} }
async render(force = false, patchQueue, scope, vars) { /**
* The render method is the main entry point to render a component (once it
* is ready. This method is not initially called when the component is
* rendered the first time).
*
* This method will cause all its sub components to potentially rerender
* themselves. Note that `render` is not called if a component is updated via
* its props.
*/
async render(force = false) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (!__owl__.isMounted) { if (!__owl__.isMounted) {
return; return;
} }
const shouldPatch = !patchQueue; const patchQueue = [];
if (shouldPatch) { const renderId = ++__owl__.renderId;
patchQueue = []; await this.__render(force, patchQueue, undefined, undefined);
} if (__owl__.isMounted && renderId === __owl__.renderId) {
__owl__.renderId++;
const renderId = __owl__.renderId;
await this.__render(force, patchQueue, scope, vars);
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering // we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called. // occurred between now and when the render method was initially called.
this.__applyPatchQueue(patchQueue); this.__applyPatchQueue(patchQueue);
@@ -3162,6 +3169,17 @@
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Private // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/**
* Private helper to perform a full destroy, from the point of view of an Owl
* component. It does not remove the el (this is done only once on the top
* level destroyed component, for performance reasons).
*
* The job of this method is mostly to call willUnmount hooks, and to perform
* all necessary internal cleanup.
*
* Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times.
*/
__destroy(parent) { __destroy(parent) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted; const isMounted = __owl__.isMounted;
@@ -3214,6 +3232,10 @@
} }
} }
} }
/**
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(nextProps, forceUpdate = false, patchQueue, scope, vars) { async __updateProps(nextProps, forceUpdate = false, patchQueue, scope, vars) {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
@@ -3223,15 +3245,16 @@
} }
await this.willUpdateProps(nextProps); await this.willUpdateProps(nextProps);
this.props = nextProps; this.props = nextProps;
await this.render(forceUpdate, patchQueue, scope, vars); await this.__render(forceUpdate, patchQueue, scope, vars);
} }
} }
/**
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode) { __patch(vnode) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel); const target = __owl__.vnode || document.createElement(vnode.sel);
if (this.__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, this.__owl__.classObj);
}
__owl__.vnode = patch(target, vnode); __owl__.vnode = patch(target, vnode);
} }
__prepare(scope, vars) { __prepare(scope, vars) {
@@ -3286,9 +3309,7 @@
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const promises = []; const promises = [];
const patch = [this]; const patch = [this];
if (__owl__.isMounted) { patchQueue.push(patch);
patchQueue.push(patch);
}
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = false; __owl__.observer.allowMutations = false;
} }
@@ -3318,6 +3339,12 @@
// parent component. With this, we make sure that the parent component will be // parent component. With this, we make sure that the parent component will be
// able to patch itself properly after // able to patch itself properly after
vnode.key = __owl__.id; vnode.key = __owl__.id;
// we applly here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
}
return Promise.all(promises).then(() => vnode); return Promise.all(promises).then(() => vnode);
} }
/** /**
@@ -3344,6 +3371,10 @@
this.mounted(); this.mounted();
} }
} }
/**
* Enable the observe feature on the state. We only create an observer if
* there is some state to be observed.
*/
__observeState() { __observeState() {
if (this.state) { if (this.state) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
@@ -3401,6 +3432,14 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Error handling // Error handling
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
/**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
function errorHandler(error, component) { function errorHandler(error, component) {
let canCatch = false; let canCatch = false;
let qweb = component.env.qweb; let qweb = component.env.qweb;
@@ -3423,22 +3462,21 @@
} }
class ConnectedComponent extends Component { class ConnectedComponent extends Component {
constructor(parent, props) { constructor() {
super(parent, props); super(...arguments);
this.deep = true; this.deep = true;
this.hashFunction = ({ storeProps }, options) => { this.hashFunction = (storeProps, options) => {
const observer = this.__owl__.store.observer; const revFn = this.__owl__.revFn;
let refFunction = this.deep ? observer.deepRevNumber : observer.revNumber; const rev = revFn(storeProps);
if ("__owl__" in storeProps) { if (rev > 0) {
return refFunction.call(observer, storeProps); return rev;
} }
const { currentStoreProps } = options;
let hash = 0; let hash = 0;
for (let key in storeProps) { for (let key in storeProps) {
const val = storeProps[key]; const val = storeProps[key];
const hashVal = refFunction.call(observer, val); const hashVal = revFn(val);
if (hashVal === 0) { if (hashVal === 0) {
if (val !== currentStoreProps[key]) { if (val !== options.prevStoreProps[key]) {
options.didChange = true; options.didChange = true;
} }
} }
@@ -3448,20 +3486,6 @@
} }
return hash; return hash;
}; };
const store = this.getStore(this.env);
const ownProps = this.props || {};
const storeProps = this.constructor.mapStoreToProps(store.state, ownProps, store.getters);
const mergedProps = Object.assign({}, ownProps, storeProps);
this.props = mergedProps;
this.__owl__.ownProps = ownProps;
this.__owl__.currentStoreProps = storeProps;
this.__owl__.store = store;
this.__owl__.storeHash = this.hashFunction({
state: store.state,
storeProps: storeProps
}, {
currentStoreProps: storeProps
});
} }
getStore(env) { getStore(env) {
return env.store; return env.store;
@@ -3469,6 +3493,26 @@
static mapStoreToProps(storeState, ownProps, getters) { static mapStoreToProps(storeState, ownProps, getters) {
return {}; return {};
} }
dispatch(name, ...payload) {
return this.__owl__.store.dispatch(name, ...payload);
}
/**
* Need to do this here so 'deep' can be overrided by subcomponent easily
*/
async __prepareAndRender(scope, vars) {
const store = this.getStore(this.env);
const ownProps = this.props || {};
this.storeProps = this.constructor.mapStoreToProps(store.state, ownProps, store.getters);
const observer = store.observer;
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
this.__owl__.store = store;
this.__owl__.revFn = revFn.bind(observer);
this.__owl__.storeHash = this.hashFunction(this.storeProps, {
prevStoreProps: this.storeProps
});
this.__owl__.rev = observer.rev;
return super.__prepareAndRender(scope, vars);
}
/** /**
* We do not use the mounted hook here for a subtle reason: we want the * We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However, * updates to be called for the parents before the children. However,
@@ -3478,78 +3522,63 @@
this.__owl__.store.on("update", this, this.__checkUpdate); this.__owl__.store.on("update", this, this.__checkUpdate);
super.__callMounted(); super.__callMounted();
} }
willUnmount() { __callWillUnmount() {
this.__owl__.store.off("update", this); this.__owl__.store.off("update", this);
super.willUnmount(); super.__callWillUnmount();
} }
async __checkUpdate(updateId) { __destroy(parent) {
if (updateId === this.__owl__.currentUpdateId) { this.__owl__.store.off("update", this);
return; super.__destroy(parent);
} }
const ownProps = this.__owl__.ownProps; async __updateProps(nextProps, f, p, s, v) {
const storeProps = this.constructor.mapStoreToProps(this.__owl__.store.state, ownProps, this.__owl__.store.getters); this.__updateStoreProps(nextProps);
const options = { return super.__updateProps(nextProps, f, p, s, v);
currentStoreProps: this.__owl__.currentStoreProps }
}; __updateStoreProps(nextProps) {
const storeHash = this.hashFunction({ const store = this.__owl__.store;
state: this.__owl__.store.state, const storeProps = this.constructor.mapStoreToProps(store.state, nextProps, store.getters);
storeProps: storeProps const options = { prevStoreProps: this.storeProps, didChange: false };
}, options); const storeHash = this.hashFunction(storeProps, options);
this.storeProps = storeProps;
let didChange = options.didChange; let didChange = options.didChange;
if (storeHash !== this.__owl__.storeHash) { if (storeHash !== this.__owl__.storeHash) {
didChange = true;
this.__owl__.storeHash = storeHash; this.__owl__.storeHash = storeHash;
didChange = true;
} }
if (didChange) { this.__owl__.rev = store.observer.rev;
this.__owl__.currentStoreProps = storeProps; return didChange;
await this.__updateProps(ownProps, false);
}
} }
__updateProps(nextProps, forceUpdate, patchQueue) { async __checkUpdate() {
const __owl__ = this.__owl__; const observer = this.__owl__.store.observer;
__owl__.currentUpdateId = __owl__.store._updateId; if (observer.rev === this.__owl__.rev) {
if (__owl__.ownProps !== nextProps) { // update was already done by updateProps, from parent
__owl__.currentStoreProps = this.constructor.mapStoreToProps(__owl__.store.state, nextProps, __owl__.store.getters); return;
}
const didChange = this.__updateStoreProps(this.props);
if (didChange) {
return this.render();
} }
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
} }
} }
class Store extends EventBus { class Store extends EventBus {
constructor(config, options = {}) { constructor(config, options = {}) {
super(); super();
this._commitLevel = 0;
this.history = [];
this._updateId = 1;
this.debug = options.debug || false; this.debug = options.debug || false;
this.actions = config.actions; this.actions = config.actions;
this.mutations = config.mutations;
this.env = config.env; this.env = config.env;
this.observer = new Observer(); this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this); this.observer.notifyCB = this.__notifyComponents.bind(this);
this.observer.allowMutations = false;
this.state = this.observer.observe(config.state || {}); this.state = this.observer.observe(config.state || {});
this.getters = {}; this.getters = {};
this._gettersCache = {}; if (config.getters) {
if (this.debug) { const firstArg = {
this.history.push({ state: this.state }); state: this.state,
} getters: this.getters,
const cTypes = ["undefined", "number", "string"];
for (let entry of Object.entries(config.getters || {})) {
const name = entry[0];
const func = entry[1];
this.getters[name] = payload => {
if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
this._gettersCache[name] = this._gettersCache[name] || {};
this._gettersCache[name][payload] =
this._gettersCache[name][payload] ||
func({ state: this.state, getters: this.getters }, payload);
return this._gettersCache[name][payload];
}
return func({ state: this.state, getters: this.getters }, payload);
}; };
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
}
} }
} }
dispatch(action, ...payload) { dispatch(action, ...payload) {
@@ -3557,7 +3586,6 @@
throw new Error(`[Error] action ${action} is undefined`); throw new Error(`[Error] action ${action} is undefined`);
} }
const result = this.actions[action]({ const result = this.actions[action]({
commit: this.commit.bind(this),
dispatch: this.dispatch.bind(this), dispatch: this.dispatch.bind(this),
env: this.env, env: this.env,
state: this.state, state: this.state,
@@ -3565,30 +3593,6 @@
}, ...payload); }, ...payload);
return result; return result;
} }
commit(type, ...payload) {
if (!this.mutations[type]) {
throw new Error(`[Error] mutation ${type} is undefined`);
}
this._commitLevel++;
this.observer.allowMutations = true;
const res = this.mutations[type].call(null, {
commit: this.commit.bind(this),
state: this.state,
getters: this.getters
}, ...payload);
if (this._commitLevel === 1) {
this.observer.allowMutations = false;
if (this.debug) {
this.history.push({
state: this.state,
mutation: type,
payload: [...payload]
});
}
}
this._commitLevel--;
return res;
}
/** /**
* Instead of using trigger to emit an update event, we actually implement * Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than * our own function to do that. The reason is that we need to be smarter than
@@ -3604,15 +3608,12 @@
* updating all widgets concurrently, except for parents/children. * updating all widgets concurrently, except for parents/children.
*/ */
async __notifyComponents() { async __notifyComponents() {
this._updateId++;
const current = this._updateId;
this._gettersCache = {};
const subs = this.subscriptions.update || []; const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) { for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i]; const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true; const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) { if (shouldCallback) {
await sub.callback.call(sub.owner, current); await sub.callback.call(sub.owner);
} }
} }
} }
@@ -4012,9 +4013,9 @@
exports.store = store; exports.store = store;
exports.utils = utils; exports.utils = utils;
exports.__info__.version = '0.18.0'; exports.__info__.version = '0.19.0';
exports.__info__.date = '2019-08-22T13:04:23.405Z'; exports.__info__.date = '2019-08-28T09:55:59.136Z';
exports.__info__.hash = '70a7016'; exports.__info__.hash = 'f289cb1';
exports.__info__.url = 'https://github.com/odoo/owl'; exports.__info__.url = 'https://github.com/odoo/owl';
}(this.owl = this.owl || {})); }(this.owl = this.owl || {}));
+293 -74
View File
@@ -268,56 +268,42 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
const ENTER_KEY = 13; const ENTER_KEY = 13;
const ESC_KEY = 27; const ESC_KEY = 27;
const LOCALSTORAGE_KEY = "todos-odoo"; const LOCALSTORAGE_KEY = "todomvc";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Store Definition // Store Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const actions = { const actions = {
addTodo({ commit }, title) {
commit("addTodo", title);
},
removeTodo({ commit }, id) {
commit("removeTodo", id);
},
toggleTodo({ state, commit }, id) {
const todo = state.todos.find(t => t.id === id);
commit("editTodo", { id, completed: !todo.completed });
},
clearCompleted({ state, commit }) {
state.todos
.filter(todo => todo.completed)
.forEach(todo => {
commit("removeTodo", todo.id);
});
},
toggleAll({ state, commit }, completed) {
state.todos.forEach(todo => {
commit("editTodo", {
id: todo.id,
completed
});
});
},
editTodo({ commit }, { id, title }) {
commit("editTodo", { id, title });
}
};
const mutations = {
addTodo({ state }, title) { addTodo({ state }, title) {
const id = state.nextId++; state.todos.push({
const todo = { id: state.nextId++,
id,
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) {
const todo = state.todos.find(t => t.id === id);
dispatch("editTodo", { id, completed: !todo.completed });
},
clearCompleted({ state, dispatch }) {
state.todos
.filter(todo => todo.completed)
.forEach(todo => {
dispatch("removeTodo", todo.id);
});
},
toggleAll({ state, dispatch }, completed) {
state.todos.forEach(todo => {
dispatch("editTodo", {
id: todo.id,
completed
});
});
},
editTodo({ state }, { id, title, completed }) { editTodo({ state }, { id, title, completed }) {
const todo = state.todos.find(t => t.id === id); const todo = state.todos.find(t => t.id === id);
if (title !== undefined) { if (title !== undefined) {
@@ -329,24 +315,20 @@ const mutations = {
} }
}; };
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() { function makeStore() {
const todos = JSON.parse( const state = loadState();
window.localStorage.getItem(LOCALSTORAGE_KEY) || "[]" const store = new owl.store.Store({ state, actions });
); store.on("update", null, () => saveState(store.state));
const nextId = Math.max(0, ...todos.map(t => t.id || 0)) + 1;
const state = {
todos,
nextId
};
const store = new owl.store.Store({
state,
actions,
mutations
});
store.on("update", null, () => {
const state = JSON.stringify(store.state.todos);
window.localStorage.setItem(LOCALSTORAGE_KEY, state);
});
return store; return store;
} }
@@ -357,11 +339,11 @@ class TodoItem extends owl.Component {
state = { isEditing: false }; state = { isEditing: false };
removeTodo() { removeTodo() {
this.env.dispatch("removeTodo", this.props.id); this.env.store.dispatch("removeTodo", this.props.id);
} }
toggleTodo() { toggleTodo() {
this.env.dispatch("toggleTodo", this.props.id); this.env.store.dispatch("toggleTodo", this.props.id);
} }
async editTodo() { async editTodo() {
@@ -393,7 +375,7 @@ class TodoItem extends owl.Component {
if (!value) { if (!value) {
this.removeTodo(this.props.id); this.removeTodo(this.props.id);
} else { } else {
this.env.dispatch("editTodo", { this.env.store.dispatch("editTodo", {
id: this.props.id, id: this.props.id,
title: value title: value
}); });
@@ -415,7 +397,7 @@ class TodoApp extends owl.store.ConnectedComponent {
}; };
} }
get visibleTodos() { get visibleTodos() {
let todos = this.props.todos; let todos = this.storeProps.todos;
if (this.state.filter === "active") { if (this.state.filter === "active") {
todos = todos.filter(t => !t.completed); todos = todos.filter(t => !t.completed);
} }
@@ -426,11 +408,11 @@ class TodoApp extends owl.store.ConnectedComponent {
} }
get allChecked() { get allChecked() {
return this.props.todos.every(todo => todo.completed); return this.storeProps.todos.every(todo => todo.completed);
} }
get remaining() { get remaining() {
return this.props.todos.filter(todo => !todo.completed).length; return this.storeProps.todos.filter(todo => !todo.completed).length;
} }
get remainingText() { get remainingText() {
@@ -442,20 +424,12 @@ class TodoApp extends owl.store.ConnectedComponent {
if (ev.keyCode === ENTER_KEY) { if (ev.keyCode === ENTER_KEY) {
const title = ev.target.value; const title = ev.target.value;
if (title.trim()) { if (title.trim()) {
this.env.dispatch("addTodo", title); this.dispatch("addTodo", title);
} }
ev.target.value = ""; ev.target.value = "";
} }
} }
clearCompleted() {
this.env.dispatch("clearCompleted");
}
toggleAll() {
this.env.dispatch("toggleAll", !this.allChecked);
}
setFilter(filter) { setFilter(filter) {
this.state.filter = filter; this.state.filter = filter;
} }
@@ -469,7 +443,6 @@ const qweb = new owl.QWeb(TEMPLATES);
const env = { const env = {
qweb, qweb,
store, store,
dispatch: store.dispatch.bind(store),
}; };
const app = new TodoApp(env); const app = new TodoApp(env);
app.mount(document.body); app.mount(document.body);
@@ -481,8 +454,8 @@ 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="props.todos.length"> <section class="main" t-if="storeProps.todos.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="toggleAll"/> <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">
<t t-foreach="visibleTodos" t-as="todo"> <t t-foreach="visibleTodos" t-as="todo">
@@ -490,7 +463,7 @@ const TODO_APP_STORE_XML = `<templates>
</t> </t>
</ul> </ul>
</section> </section>
<footer class="footer" t-if="props.todos.length"> <footer class="footer" t-if="storeProps.todos.length">
<span class="todo-count"> <span class="todo-count">
<strong> <strong>
<t t-esc="remaining"/> <t t-esc="remaining"/>
@@ -508,7 +481,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="props.todos.length gt remaining" t-on-click="clearCompleted"> <button class="clear-completed" t-if="storeProps.todos.length gt remaining" t-on-click="dispatch('clearCompleted')">
Clear completed Clear completed
</button> </button>
</footer> </footer>
@@ -1334,6 +1307,246 @@ const FORM_XML = `<templates>
</templates> </templates>
`; `;
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
// - ...
class HelloWorld extends owl.Component {}
class Counter extends owl.Component {
state = { value: 0 };
inc() {
this.state.value++;
}
}
class Window extends owl.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 owl.Component {
components = { Window };
windows = [];
nextId = 1;
currentZindex = 1;
addWindow(name) {
const info = this.env.windows.find(w => w.name === name);
const id = \`w\${this.nextId++}\`;
this.windows.push({
id: id,
title: info.title,
width: info.defaultWidth,
height: info.defaultHeight,
top: 0,
left: 0,
zindex: this.currentZindex++
});
this.components[id] = info.component;
this.render();
}
closeWindow(ev) {
const id = ev.detail.id;
delete this.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;
}
}
class App extends owl.Component {
components = { WindowManager };
addWindow(name) {
this.refs.wm.addWindow(name);
}
}
const qweb = new owl.QWeb(TEMPLATES);
const windows = [
{
name: "Hello",
title: "Hello",
component: HelloWorld,
defaultWidth: 200,
defaultHeight: 100
},
{
name: "Counter",
title: "Click Counter",
component: Counter,
defaultWidth: 300,
defaultHeight: 120
}
];
const env = { qweb, windows };
const app = new App(env);
app.mount(document.body);
`;
const WMS_XML = `<templates>
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex">
<div class="header">
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
<span class="close" t-on-click="close">×</span>
</div>
<t t-slot="default"/>
</div>
<div t-name="WindowManager" class="window-manager"
t-on-close-window="closeWindow"
t-on-update-z-index="updateZIndex"
t-on-set-window-position="setWindowPosition">
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
<t t-component="{{w.id}}"/>
</Window>
</div>
<div t-name="App" class="app">
<WindowManager t-ref="wm"/>
<div class="menubar">
<button t-on-click="addWindow('Hello')">Say Hello</button>
<button t-on-click="addWindow('Counter')">Counter</button>
</div>
</div>
<div t-name="HelloWorld">
World
</div>
<div t-name="Counter" class="counter">
<button t-on-click="inc">Inc</button>
<span><t t-esc="state.value"/></span>
</div>
</templates>
`;
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 = [ export const SAMPLES = [
{ {
description: "Components", description: "Components",
@@ -1376,6 +1589,12 @@ export const SAMPLES = [
xml: SLOTS_XML, xml: SLOTS_XML,
css: SLOTS_CSS css: SLOTS_CSS
}, },
{
description: "Window Management System",
code: WMS,
xml: WMS_XML,
css: WMS_CSS,
},
{ {
description: "Asynchronous components", description: "Asynchronous components",
code: ASYNC_COMPONENTS, code: ASYNC_COMPONENTS,