diff --git a/owl.js b/owl.js index cbe006b6..17ff8c7c 100644 --- a/owl.js +++ b/owl.js @@ -1,71 +1,6 @@ (function (exports) { 'use strict'; - /** - * We define here a simple event bus: it can - * - emit events - * - add/remove listeners. - * - * This is a useful pattern of communication in many cases. For OWL, each - * components and stores are event buses. - */ - //------------------------------------------------------------------------------ - // EventBus - //------------------------------------------------------------------------------ - class EventBus { - constructor() { - this.subscriptions = {}; - } - /** - * Add a listener for the 'eventType' events. - * - * Note that the 'owner' of this event can be anything, but will more likely - * be a widget or a class. The idea is that the callback will be called with - * the proper owner bound. - * - * Also, the owner should be kind of unique. This will be used to remove the - * listener. - */ - on(eventType, owner, callback) { - if (!callback) { - throw new Error("Missing callback"); - } - if (!this.subscriptions[eventType]) { - this.subscriptions[eventType] = []; - } - this.subscriptions[eventType].push({ - owner, - callback - }); - } - /** - * Remove a listener - */ - off(eventType, owner) { - const subs = this.subscriptions[eventType]; - if (subs) { - this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); - } - } - /** - * Emit an event of type 'eventType'. Any extra arguments will be passed to - * the listeners callback. - */ - trigger(eventType, ...args) { - const subs = this.subscriptions[eventType] || []; - for (let i = 0, iLen = subs.length; i < iLen; i++) { - const sub = subs[i]; - sub.callback.call(sub.owner, ...args); - } - } - /** - * Remove all subscriptions. - */ - clear() { - this.subscriptions = {}; - } - } - /** * Owl Observer * @@ -171,9 +106,15 @@ } } set(target, key, value) { - this.rev++; - this._addProp(target, key, value); - target.__owl__.rev++; + let alreadyDefined = key in target && + Object.getOwnPropertyDescriptor(target, key).configurable === false; + if (alreadyDefined) { + target[key] = value; + } + else { + this._addProp(target, key, value); + this._updateRevNumber(target); + } this.notifyChange(); } _observeObj(obj, parent) { @@ -201,23 +142,26 @@ }, set(newVal) { if (newVal !== value) { - self.rev++; if (!self.allowMutations) { throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`); } + self._updateRevNumber(obj); value = newVal; self.observe(newVal, obj); - obj.__owl__.rev++; - let parent = obj; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent) && parent !== obj); self.notifyChange(); } } }); this.observe(value, obj); } + _updateRevNumber(target) { + this.rev++; + target.__owl__.rev++; + let parent = target; + do { + parent.__owl__.deepRev++; + } while ((parent = parent.__owl__.parent) && parent !== target); + } } /** @@ -913,6 +857,14 @@ } } return classes.join(" "); + }, + shallowEqual(p1, p2) { + for (let k in p1) { + if (p1[k] !== p2[k]) { + return false; + } + } + return true; } }; function parseXML(xml) { @@ -1069,7 +1021,11 @@ template = new Function("context", "extra", ctx.code.join("\n")); } catch (e) { - throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`); + const templateName = ctx.templateName.replace(/`/g, "'"); + console.groupCollapsed(`Invalid Code generated by ${templateName}`); + console.warn(ctx.code.join("\n")); + console.groupEnd(); + throw new Error(`Invalid generated code while compiling template '${templateName}': ${e.message}`); } if (isDebug) { console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` + @@ -1471,19 +1427,20 @@ } /** * Perform string interpolation on the given string. Note that if the whole - * string is an expression, it simply returns it (formatted). + * string is an expression, it simply returns it (formatted and enclosed in + * parentheses). * For instance: * 'Hello {{x}}!' -> `Hello ${x}` - * '{{x}}' -> x + * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') */ interpolate(s) { let matches = s.match(/\{\{.*?\}\}/g); if (matches && matches[0].length === s.length) { - return this.formatExpression(s.slice(2, -2)); + return `(${this.formatExpression(s.slice(2, -2))})`; } matches = s.match(/\#\{.*?\}/g); if (matches && matches[0].length === s.length) { - return this.formatExpression(s.slice(2, -1)); + return `(${this.formatExpression(s.slice(2, -1))})`; } let formatter = expr => "${" + this.formatExpression(expr) + "}"; let r = s @@ -1502,7 +1459,7 @@ // Widget //------------------------------------------------------------------------------ let nextId = 1; - class Component extends EventBus { + class Component { //-------------------------------------------------------------------------- // Lifecycle //-------------------------------------------------------------------------- @@ -1526,7 +1483,6 @@ * the t-widget directive in a template) */ constructor(parent, props) { - super(); this.refs = {}; const defaultProps = this.constructor.defaultProps; if (defaultProps) { @@ -1565,6 +1521,10 @@ mountedHandlers: {} }; } + /** + * The `el` is the root element of the widget. Note that it could be null: + * this is the case if the widget is not mounted yet, or is destroyed. + */ get el() { return this.__owl__.vnode ? this.__owl__.vnode.elm : null; } @@ -1641,10 +1601,16 @@ //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- + /** + * Mount the component to a target element. + * + * This should only be done if the component was created manually. Components + * created declaratively in templates are managed by the Owl system. + */ async mount(target) { const vnode = await this._prepare(); if (this.__owl__.isDestroyed) { - // widget was destroyed before we get here... + // component was destroyed before we get here... return; } this._patch(vnode); @@ -1653,34 +1619,6 @@ this._callMounted(); } } - _callMounted() { - const __owl__ = this.__owl__; - const children = __owl__.children; - for (let id in children) { - const comp = children[id]; - if (!comp.__owl__.isMounted && this.el.contains(comp.el)) { - comp._callMounted(); - } - } - __owl__.isMounted = true; - const handlers = __owl__.mountedHandlers; - for (let key in handlers) { - handlers[key](); - } - this.mounted(); - } - _callWillUnmount() { - this.willUnmount(); - const __owl__ = this.__owl__; - __owl__.isMounted = false; - const children = __owl__.children; - for (let id in children) { - const comp = children[id]; - if (comp.__owl__.isMounted) { - comp._callWillUnmount(); - } - } - } unmount() { if (this.__owl__.isMounted) { this._callWillUnmount(); @@ -1717,6 +1655,15 @@ } } } + /** + * Destroy the component. This operation is quite complex: + * - it recursively destroy all children + * - call the willUnmount hooks if necessary + * - remove the dom node from the dom + * + * This should only be called manually if you created the widget. Most widgets + * will be automatically destroyed. + */ destroy() { const __owl__ = this.__owl__; if (!__owl__.isDestroyed) { @@ -1727,26 +1674,11 @@ } } } - _destroy(parent) { - const __owl__ = this.__owl__; - const isMounted = __owl__.isMounted; - if (isMounted) { - this.willUnmount(); - __owl__.isMounted = false; - } - const children = __owl__.children; - for (let key in children) { - children[key]._destroy(this); - } - if (parent) { - let id = __owl__.id; - delete parent.__owl__.children[id]; - __owl__.parent = null; - } - this.clear(); - __owl__.isDestroyed = true; - delete __owl__.vnode; - } + /** + * This method is called by the component system whenever its props are + * updated. If it returns true, then the component will be rendered. + * Otherwise, it will skip the rendering (also, its props will not be updated) + */ shouldUpdate(nextProps) { return true; } @@ -1769,12 +1701,79 @@ await this.render(true); } } + /** + * Sets a key (from the state) to a specific value. This is mostly useful to + * work around the limitation in observed value with new keys. + */ set(target, key, value) { this.__owl__.observer.set(target, key, value); } + /** + * Emit a custom event of type 'eventType' with the given 'payload' on the + * component's el, if it exists. However, note that the event will only bubble + * up to the parent DOM nodes. Thus, it must be called between mounted() and + * willUnmount(). + */ + trigger(eventType, payload) { + if (this.el) { + const ev = new CustomEvent(eventType, { + bubbles: true, + cancelable: true, + detail: payload + }); + this.el.dispatchEvent(ev); + } + } //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- + _destroy(parent) { + const __owl__ = this.__owl__; + const isMounted = __owl__.isMounted; + if (isMounted) { + this.willUnmount(); + __owl__.isMounted = false; + } + const children = __owl__.children; + for (let key in children) { + children[key]._destroy(this); + } + if (parent) { + let id = __owl__.id; + delete parent.__owl__.children[id]; + __owl__.parent = null; + } + __owl__.isDestroyed = true; + delete __owl__.vnode; + } + _callMounted() { + const __owl__ = this.__owl__; + const children = __owl__.children; + for (let id in children) { + const comp = children[id]; + if (!comp.__owl__.isMounted && this.el.contains(comp.el)) { + comp._callMounted(); + } + } + __owl__.isMounted = true; + const handlers = __owl__.mountedHandlers; + for (let key in handlers) { + handlers[key](); + } + this.mounted(); + } + _callWillUnmount() { + this.willUnmount(); + const __owl__ = this.__owl__; + __owl__.isMounted = false; + const children = __owl__.children; + for (let id in children) { + const comp = children[id]; + if (comp.__owl__.isMounted) { + comp._callWillUnmount(); + } + } + } async _updateProps(nextProps, forceUpdate = false, patchQueue) { const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); if (shouldUpdate) { @@ -1994,6 +1993,71 @@ return result; } + /** + * We define here a simple event bus: it can + * - emit events + * - add/remove listeners. + * + * This is a useful pattern of communication in many cases. For OWL, each + * components and stores are event buses. + */ + //------------------------------------------------------------------------------ + // EventBus + //------------------------------------------------------------------------------ + class EventBus { + constructor() { + this.subscriptions = {}; + } + /** + * Add a listener for the 'eventType' events. + * + * Note that the 'owner' of this event can be anything, but will more likely + * be a widget or a class. The idea is that the callback will be called with + * the proper owner bound. + * + * Also, the owner should be kind of unique. This will be used to remove the + * listener. + */ + on(eventType, owner, callback) { + if (!callback) { + throw new Error("Missing callback"); + } + if (!this.subscriptions[eventType]) { + this.subscriptions[eventType] = []; + } + this.subscriptions[eventType].push({ + owner, + callback + }); + } + /** + * Remove a listener + */ + off(eventType, owner) { + const subs = this.subscriptions[eventType]; + if (subs) { + this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); + } + } + /** + * Emit an event of type 'eventType'. Any extra arguments will be passed to + * the listeners callback. + */ + trigger(eventType, ...args) { + const subs = this.subscriptions[eventType] || []; + for (let i = 0, iLen = subs.length; i < iLen; i++) { + const sub = subs[i]; + sub.callback.call(sub.owner, ...args); + } + } + /** + * Remove all subscriptions. + */ + clear() { + this.subscriptions = {}; + } + } + /** * Owl QWeb Directives * @@ -2281,7 +2345,7 @@ * - t-on * - t-ref * - t-transition - * - t-widget/t-props/t-keepalive + * - t-widget/t-keepalive * - t-mounted */ //------------------------------------------------------------------------------ @@ -2426,6 +2490,9 @@ //------------------------------------------------------------------------------ // t-widget //------------------------------------------------------------------------------ + const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, { + self: "if (e.target !== vn.elm) {return}" + }); /** * The t-widget directive is certainly a complicated and hard to maintain piece * of code. To help you, fellow developer, if you have to maintain it, I offer @@ -2438,11 +2505,15 @@ * ```xml * * ``` * * ```js + * // we assign utils on top of the function because it will be useful for + * // each widgets + * let utils = this.utils; + * * // this is the virtual node representing the parent div * let c1 = [], p1 = { key: 1 }; * var vn1 = h("div", p1, c1); @@ -2457,7 +2528,7 @@ * let _2_index = c1.length; * c1.push(null); * - * // def3 is the deferred that will contain later either the new widget + * // def3 is the promise that will contain later either the new widget * // creation, or the props update... * let def3; * @@ -2476,31 +2547,32 @@ * // computation, so it is certainly better to do it only once * let props4 = { flag: context["state"].flag }; * - * // If we have a widget, currently rendering, but not ready yet, and which was - * // rendered with different props, we do not want to wait for it to be ready, - * // then update it. We simply destroy it, and start anew. - * if ( - * w4 && - * w4.__owl__.renderPromise && - * !w4.__owl__.isStarted && - * props4 !== w4.__owl__.renderProps - * ) { - * w4.destroy(); - * w4 = false; + * // If we have a widget, currently rendering, but not ready yet, we do not want + * // to wait for it to be ready if we can avoid it + * if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) { + * // we check if the props are the same. In that case, we can simply reuse + * // the previous rendering and skip all useless work + * if (utils.shallowEqual(props4, w4.__owl__.renderProps)) { + * def3 = w4.__owl__.renderPromise; + * } else { + * // if the props are not the same, we destroy the widget and starts anew. + * // this will be faster than waiting for its rendering, then updating it + * w4.destroy(); + * w4 = false; + * } * } * * if (!w4) { * // in this situation, we need to create a new widget. First step is * // to get a reference to the class, then create an instance with * // current context as parent, and the props. - * let W4 = context.widgets["child"]; + * let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4]; + * if (!W4) { * throw new Error("Cannot find the definition of widget 'child'"); * } * w4 = new W4(owner, props4); * - * let utils = this.utils; - * * // Whenever we rerender the parent widget, we need to be sure that we * // are able to find the widget instance. To do that, we register it to * // the parent cmap (children map). Note that the 'template' key is @@ -2557,7 +2629,9 @@ * } else { * // this is the 'update' path of the directive. * // the call to _updateProps is the actual widget update - * def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); + * // Note that we only update the props if we cannot reuse the previous + * // rendering work (in the case it was rendered with the same props) + * def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); * def3 = def3.then(() => { * // if widget was destroyed in the meantime, we do nothing (so, this * // means that the parent's element children list will have a null in @@ -2588,28 +2662,42 @@ ctx.rootContext.shouldDefineOwner = true; ctx.rootContext.shouldDefineQWeb = true; ctx.rootContext.shouldDefineUtils = true; - let props = node.getAttribute("t-props"); let keepAlive = node.getAttribute("t-keepalive") ? true : false; // t-on- events and t-transition const events = []; let transition = ""; const attributes = node.attributes; + const props = {}; for (let i = 0; i < attributes.length; i++) { const name = attributes[i].name; + const value = attributes[i].textContent; if (name.startsWith("t-on-")) { - events.push([name.slice(5), attributes[i].textContent]); + const [eventName, ...mods] = name.slice(5).split("."); + let extraArgs; + let handlerName = value.replace(/\(.*\)/, function (args) { + extraArgs = args.slice(1, -1); + return ""; + }); + events.push([eventName, mods, handlerName, extraArgs]); } else if (name === "t-transition") { - transition = attributes[i].textContent; + transition = value; + } + else if (!name.startsWith("t-")) { + if (name !== "class" && name !== "style") { + // this is a prop! + props[name] = ctx.formatExpression(value); + } } } let key = node.getAttribute("t-key"); if (key) { key = ctx.formatExpression(key); } - if (props) { - props = ctx.formatExpression(props); - } + // computing the props string representing the props object + let propStr = Object.keys(props) + .map(k => k + ":" + props[k]) + .join(","); let dummyID = ctx.generateID(); let defID = ctx.generateID(); let widgetID = ctx.generateID(); @@ -2660,7 +2748,7 @@ tattStyle = attVar; } let updateClassCode = ""; - if (classAttr || tattClass || styleAttr || tattStyle) { + if (classAttr || tattClass || styleAttr || tattStyle || events.length) { let classCode = ""; if (classAttr) { classCode = @@ -2679,15 +2767,40 @@ }`; updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`; } + let eventsCode = events + .map(function ([eventName, mods, handlerName, extraArgs]) { + let params = extraArgs + ? `owner, ${ctx.formatExpression(extraArgs)}` + : "owner"; + let handler; + if (mods.length > 0) { + handler = `function (e) {`; + handler += mods + .map(function (mod) { + return T_WIDGET_MODS_CODE[mod]; + }) + .join(""); + handler += `owner['${handlerName}'].call(${params}, e);}`; + } + else { + handler = `owner['${handlerName}'].bind(${params})`; + } + return `vn.elm.addEventListener('${eventName}', ${handler});`; + }) + .join(""); const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); - const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : ""; - createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`; + const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : ""; + createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`; } ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`); - ctx.addLine(`let props${widgetID} = ${props || "{}"};`); - ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode && props${widgetID} !== w${widgetID}.__owl__.renderProps`); + ctx.addLine(`let props${widgetID} = {${propStr}};`); + ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`); + ctx.addIf(`utils.shallowEqual(props${widgetID}, w${widgetID}.__owl__.renderProps)`); + ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`); + ctx.addElse(); ctx.addLine(`w${widgetID}.destroy();`); - ctx.addLine(`w${widgetID} = false`); + ctx.addLine(`w${widgetID} = false;`); + ctx.closeIf(); ctx.closeIf(); ctx.addIf(`!w${widgetID}`); // new widget @@ -2697,16 +2810,13 @@ ctx.addLine(`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`); ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`); ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`); - for (let [event, method] of events) { - ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`); - } ctx.addLine(`def${defID} = w${widgetID}._prepare();`); // hack: specify empty remove hook to prevent the node from being removed from the DOM // FIXME: click to re-add widget during remove transition -> leak ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`); ctx.addElse(); // need to update widget - ctx.addLine(`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`); + ctx.addLine(`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`); let keepAliveCode = ""; if (keepAlive) { keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`; @@ -3068,9 +3178,9 @@ exports.connect = connect; exports.utils = utils; - exports.__info__.version = '0.12.0'; - exports.__info__.date = '2019-05-29T09:26:54.764Z'; - exports.__info__.hash = 'f60904b'; + exports.__info__.version = '0.13.0'; + exports.__info__.date = '2019-06-05T08:48:29.138Z'; + exports.__info__.hash = '7d3c374'; exports.__info__.url = 'https://github.com/odoo/owl'; }(this.owl = this.owl || {})); diff --git a/playground/app.js b/playground/app.js index 1a211c83..e5265818 100644 --- a/playground/app.js +++ b/playground/app.js @@ -39,26 +39,32 @@ const DEFAULT_HTML = ` `; -const APP_PY = `import sys -import thread -import webbrowser +const APP_PY = `#!/usr/bin/env python3 + +import threading import time -import BaseHTTPServer, SimpleHTTPServer +from http.server import SimpleHTTPRequestHandler, HTTPServer def start_server(): - httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 3600), SimpleHTTPServer.SimpleHTTPRequestHandler) + SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript' + httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler) httpd.serve_forever() -thread.start_new_thread(start_server,()) url = 'http://127.0.0.1:3600' -webbrowser.open_new(url) -while True: - try: - time.sleep(1) - except KeyboardInterrupt: - sys.exit(0) +if __name__ == "__main__": + print("Owl Application") + print("---------------") + print("Server running on: {}".format(url)) + threading.Thread(target=start_server, daemon=True).start() + + while True: + try: + time.sleep(1) + except KeyboardInterrupt: + httpd.server_close() + quit(0) `; /** @@ -242,20 +248,20 @@ class App extends owl.Component { }); } updateCode(ev) { - this.state[ev.type] = ev.value; + this.state[ev.detail.type] = ev.detail.value; } toggleLayout() { this.state.splitLayout = !this.state.splitLayout; } updatePanelHeight(ev) { - if (!ev.delta) { + if (!ev.detail.delta) { return; } let height = this.state.topPanelHeight; if (!height) { height = document.getElementsByClassName("tabbed-editor")[0].clientHeight; } - this.state.topPanelHeight = height + ev.delta; + this.state.topPanelHeight = height + ev.detail.delta; } async downloadCode() { @@ -287,10 +293,11 @@ class TabbedEditor extends owl.Component { this.sessions[tab].setUndoManager(new ace.UndoManager()); } } + this.editor = null; } mounted() { - this.editor = ace.edit(this.refs.editor); + this.editor = this.editor || ace.edit(this.refs.editor); this.editor.setValue(this.props[this.state.currentTab], -1); this.editor.setFontSize("12px"); @@ -314,20 +321,16 @@ class TabbedEditor extends owl.Component { const session = this.sessions[this.state.currentTab]; session.setValue(this.props[this.state.currentTab], -1); this.editor.setSession(session); - } - - willUnmount() { - this.editor.destroy(); - delete this.editor; + this.editor.resize(); } setTab(tab) { - if (this.state.currentTab !== tab) { - this.state.currentTab = tab; - const session = this.sessions[this.state.currentTab]; - session.doc.setValue(this.props[tab], -1); - this.editor.setSession(session); - } + if (this.state.currentTab !== tab) { + this.state.currentTab = tab; + const session = this.sessions[this.state.currentTab]; + session.doc.setValue(this.props[tab], -1); + this.editor.setSession(session); + } } onMouseDown(ev) { diff --git a/playground/playground.css b/playground/playground.css index f8900e22..daec8301 100644 --- a/playground/playground.css +++ b/playground/playground.css @@ -185,4 +185,6 @@ body { font-size: 30px; color: darkred; text-align: center; + padding-left: 30px; + padding-right: 30px; } diff --git a/playground/samples.js b/playground/samples.js index 3351b395..8134210a 100644 --- a/playground/samples.js +++ b/playground/samples.js @@ -85,7 +85,7 @@ const WIDGET_COMPOSITION_XML = `
- +
`; @@ -287,7 +287,7 @@ const LIFECYCLE_DEMO_XML = `
- +
Demo Sub Widget. Props: . State: . (click on me to update me)
@@ -515,7 +515,7 @@ const TODO_APP_STORE_XML = `
    - +
diff --git a/playground/templates.xml b/playground/templates.xml index 5b384e6b..202fb376 100644 --- a/playground/templates.xml +++ b/playground/templates.xml @@ -11,7 +11,9 @@
-
+