mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
ref: move playground at root of folder
This commit is contained in:
@@ -1,427 +0,0 @@
|
||||
import h from "../libs/snabbdom/src/h";
|
||||
import sdAttrs from "../libs/snabbdom/src/modules/attributes";
|
||||
import sdProps from "../libs/snabbdom/src/modules/props";
|
||||
import sdListeners from "../libs/snabbdom/src/modules/eventlisteners";
|
||||
import { init } from "../libs/snabbdom/src/snabbdom";
|
||||
import { VNode } from "../libs/snabbdom/src/vnode";
|
||||
import { EventBus } from "./event_bus";
|
||||
import { QWeb } from "./qweb";
|
||||
import { idGenerator } from "./utils";
|
||||
|
||||
let getId = idGenerator();
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
}
|
||||
|
||||
export interface Meta<T extends Env, Props> {
|
||||
readonly id: number;
|
||||
vnode: VNode | null;
|
||||
isStarted: boolean;
|
||||
isMounted: boolean;
|
||||
isDestroyed: boolean;
|
||||
parent: Component<T, any, any> | null;
|
||||
children: { [key: number]: Component<T, any, any> };
|
||||
// children mapping: from templateID to widgetID
|
||||
// should it be a map number => Widget?
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
renderId: number;
|
||||
renderProps: Props | null;
|
||||
renderPromise: Promise<VNode> | null;
|
||||
boundHandlers: { [key: number]: any };
|
||||
}
|
||||
|
||||
const patch = init([sdListeners, sdAttrs, sdProps]);
|
||||
|
||||
export interface Type<T> extends Function {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Widget
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Component<
|
||||
T extends Env,
|
||||
Props extends {},
|
||||
State extends {}
|
||||
> extends EventBus {
|
||||
readonly __widget__: Meta<Env, Props>;
|
||||
template: string = "default";
|
||||
inlineTemplate: string | null = null;
|
||||
|
||||
get el(): HTMLElement | null {
|
||||
return this.__widget__.vnode ? (<any>this).__widget__.vnode.elm : null;
|
||||
}
|
||||
|
||||
env: T;
|
||||
state: State = <State>{};
|
||||
props: Props;
|
||||
refs: {
|
||||
[key: string]: Component<T, any, any> | HTMLElement | undefined;
|
||||
} = {};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates an instance of Component.
|
||||
*
|
||||
* The root widget of a component tree needs an environment:
|
||||
*
|
||||
* ```javascript
|
||||
* const root = new RootWidget(env, props);
|
||||
* ```
|
||||
*
|
||||
* Every other widget simply needs a reference to its parent:
|
||||
*
|
||||
* ```javascript
|
||||
* const child = new SomeWidget(parent, props);
|
||||
* ```
|
||||
*
|
||||
* Note that most of the time, only the root widget needs to be created by
|
||||
* hand. Other widgets should be created automatically by the framework (with
|
||||
* the t-widget directive in a template)
|
||||
*/
|
||||
constructor(parent: Component<T, any, any> | T, props?: Props) {
|
||||
super();
|
||||
|
||||
// is this a good idea?
|
||||
// Pro: if props is empty, we can create easily a widget
|
||||
// Con: this is not really safe
|
||||
// Pro: but creating widget (by a template) is always unsafe anyway
|
||||
this.props = <Props>props || <Props>{};
|
||||
let id: number = getId();
|
||||
let p: Component<T, any, any> | null = null;
|
||||
if (parent instanceof Component) {
|
||||
p = parent;
|
||||
this.env = parent.env;
|
||||
parent.__widget__.children[id] = this;
|
||||
} else {
|
||||
this.env = parent;
|
||||
}
|
||||
this.__widget__ = {
|
||||
id: id,
|
||||
vnode: null,
|
||||
isStarted: false,
|
||||
isMounted: false,
|
||||
isDestroyed: false,
|
||||
parent: p,
|
||||
children: {},
|
||||
cmap: {},
|
||||
renderId: 1,
|
||||
renderPromise: null,
|
||||
renderProps: props || null,
|
||||
boundHandlers: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* willStart is an asynchronous hook that can be implemented to perform some
|
||||
* action before the initial rendering of a component.
|
||||
*
|
||||
* It will be called exactly once before the initial rendering. It is useful
|
||||
* in some cases, for example, to load external assets (such as a JS library)
|
||||
* before the widget is rendered.
|
||||
*
|
||||
* Note that a slow willStart method will slow down the rendering of the user
|
||||
* interface. Therefore, some effort should be made to make this method as
|
||||
* fast as possible.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*/
|
||||
async willStart() {}
|
||||
|
||||
/**
|
||||
* mounted is a hook that is called each time a component is attached to the
|
||||
* DOM. This is a good place to add some listeners, or to interact with the
|
||||
* DOM, if the component needs to perform some measure for example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
* @see willUnmount
|
||||
*/
|
||||
mounted() {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time a component is detached from
|
||||
* the DOM. This is a good place to remove some listeners, for example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
* @see mounted
|
||||
*/
|
||||
willUnmount() {}
|
||||
|
||||
/**
|
||||
* destroyed is a hook called exactly once, when a component is destroyed.
|
||||
* When a component is destroyed, its children will be destroyed first.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*/
|
||||
destroyed() {}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Attach a child widget to a given html element
|
||||
*
|
||||
* This is most of the time not necessary, since widgets should primarily be
|
||||
* created/managed with the t-widget directive in a qweb template. However,
|
||||
* for the cases where we need more control, this method will do what is
|
||||
* necessary to make sure all the proper hooks are called (for example,
|
||||
* mounted/willUnmount)
|
||||
*
|
||||
* Note that this method makes a few assumptions:
|
||||
* - the child widget is indeed a child of the current widget
|
||||
* - the target is inside the dom of the current widget (typically a ref)
|
||||
*/
|
||||
attachChild(child: Component<T, any, any>, target: HTMLElement) {
|
||||
target.appendChild(child.el!);
|
||||
child.__mount();
|
||||
}
|
||||
|
||||
async mount(target: HTMLElement): Promise<void> {
|
||||
const vnode = await this._start();
|
||||
if (this.__widget__.isDestroyed) {
|
||||
// widget was destroyed before we get here...
|
||||
return;
|
||||
}
|
||||
this._patch(vnode);
|
||||
target.appendChild(this.el!);
|
||||
|
||||
if (document.body.contains(target)) {
|
||||
this._visitSubTree(w => {
|
||||
if (!w.__widget__.isMounted && this.el!.contains(w.el)) {
|
||||
w.__widget__.isMounted = true;
|
||||
w.mounted();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
detach() {
|
||||
if (this.el) {
|
||||
this._visitSubTree(w => {
|
||||
if (w.__widget__.isMounted) {
|
||||
w.willUnmount();
|
||||
w.__widget__.isMounted = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.el.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async render(force: boolean = false): Promise<void> {
|
||||
if (this.__widget__.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
const renderVDom = this._render(force);
|
||||
const renderId = this.__widget__.renderId;
|
||||
const vnode = await renderVDom;
|
||||
if (renderId === this.__widget__.renderId) {
|
||||
// we only update the vnode and the actual DOM if no other rendering
|
||||
// occurred between now and when the render method was initially called.
|
||||
this._patch(vnode);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.__widget__.isDestroyed) {
|
||||
for (let id in this.__widget__.children) {
|
||||
this.__widget__.children[id].destroy();
|
||||
}
|
||||
if (this.__widget__.isMounted) {
|
||||
this.willUnmount();
|
||||
}
|
||||
if (this.el) {
|
||||
this.el.remove();
|
||||
this.__widget__.isMounted = false;
|
||||
delete this.__widget__.vnode;
|
||||
}
|
||||
if (this.__widget__.parent) {
|
||||
let id = this.__widget__.id;
|
||||
delete this.__widget__.parent.__widget__.children[id];
|
||||
this.__widget__.parent = null;
|
||||
}
|
||||
this.clear();
|
||||
this.__widget__.isDestroyed = true;
|
||||
this.destroyed();
|
||||
}
|
||||
}
|
||||
|
||||
shouldUpdate(nextProps: Props): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the correct way to update the environment of a widget. Doing
|
||||
* this will cause a full rerender of the widget and its children, so this is
|
||||
* an operation that should not be done frequently.
|
||||
*
|
||||
* A good usecase for updating the environment would be to update some mostly
|
||||
* static config keys, such as a boolean to determine if we are in mobile
|
||||
* mode or not.
|
||||
*/
|
||||
async updateEnv(nextEnv: Partial<T>): Promise<void> {
|
||||
if (this.__widget__.parent && this.__widget__.parent.env === this.env) {
|
||||
this.env = Object.create(this.env);
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
if (this.__widget__.isMounted) {
|
||||
return this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProps(
|
||||
nextProps: Props,
|
||||
forceUpdate: boolean = false
|
||||
): Promise<void> {
|
||||
if (nextProps === this.__widget__.renderProps && !forceUpdate) {
|
||||
await this.__widget__.renderPromise;
|
||||
return;
|
||||
}
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the safest update method for widget: its job is to update the state
|
||||
* and rerender (if widget is mounted).
|
||||
*
|
||||
* Notes:
|
||||
* - it checks if we do not add extra keys to the state.
|
||||
* - it is ok to call updateState before the widget is started. In that
|
||||
* case, it will simply update the state and will not rerender
|
||||
*/
|
||||
async updateState(nextState: Partial<State>) {
|
||||
if (Object.keys(nextState).length === 0) {
|
||||
return;
|
||||
}
|
||||
Object.assign(this.state, nextState);
|
||||
if (this.__widget__.isStarted) {
|
||||
return this.render();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async _updateProps(nextProps: Props): Promise<void> {
|
||||
this.props = nextProps;
|
||||
return this.render();
|
||||
}
|
||||
|
||||
_patch(vnode) {
|
||||
this.__widget__.renderPromise = null;
|
||||
this.__widget__.vnode = patch(
|
||||
this.__widget__.vnode || document.createElement(vnode.sel!),
|
||||
vnode
|
||||
);
|
||||
}
|
||||
async _start(): Promise<VNode> {
|
||||
this.__widget__.renderProps = this.props;
|
||||
this.__widget__.renderPromise = this.willStart().then(() => {
|
||||
if (this.__widget__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
this.__widget__.isStarted = true;
|
||||
if (this.inlineTemplate) {
|
||||
this.env.qweb.addTemplate(
|
||||
this.inlineTemplate,
|
||||
this.inlineTemplate,
|
||||
true
|
||||
);
|
||||
}
|
||||
return this._render();
|
||||
});
|
||||
return this.__widget__.renderPromise;
|
||||
}
|
||||
|
||||
async _render(force: boolean = false): Promise<VNode> {
|
||||
this.__widget__.renderId++;
|
||||
const promises: Promise<void>[] = [];
|
||||
const template = this.inlineTemplate || this.template;
|
||||
let vnode = this.env.qweb.render(template, this, {
|
||||
promises,
|
||||
handlers: this.__widget__.boundHandlers,
|
||||
forceUpdate: force
|
||||
});
|
||||
|
||||
// this part is critical for the patching process to be done correctly. The
|
||||
// tricky part is that a child widget can be rerendered on its own, which
|
||||
// will update its own vnode representation without the knowledge of the
|
||||
// parent widget. With this, we make sure that the parent widget will be
|
||||
// able to patch itself properly after
|
||||
vnode.key = this.__widget__.id;
|
||||
this.__widget__.renderProps = this.props;
|
||||
this.__widget__.renderPromise = Promise.all(promises).then(() => vnode);
|
||||
return this.__widget__.renderPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by qweb t-widget directive
|
||||
*/
|
||||
_mount(vnode: VNode, elm: HTMLElement): VNode {
|
||||
this.__widget__.vnode = patch(elm, vnode);
|
||||
this.__mount();
|
||||
return this.__widget__.vnode;
|
||||
}
|
||||
|
||||
__mount() {
|
||||
if (this.__widget__.isMounted) {
|
||||
return;
|
||||
}
|
||||
if (this.__widget__.parent) {
|
||||
if (this.__widget__.parent!.__widget__.isMounted) {
|
||||
this.__widget__.isMounted = true;
|
||||
this.mounted();
|
||||
const children = this.__widget__.children;
|
||||
for (let id in children) {
|
||||
children[id].__mount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_visitSubTree(callback: (w: Component<T, any, any>) => boolean) {
|
||||
const shouldVisitChildren = callback(this);
|
||||
if (shouldVisitChildren) {
|
||||
const children = this.__widget__.children;
|
||||
for (let id in children) {
|
||||
children[id]._visitSubTree(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PureComponent<E extends Env, P, S> extends Component<E, P, S> {
|
||||
shouldUpdate(nextProps: P): boolean {
|
||||
for (let k in nextProps) {
|
||||
if (nextProps[k] !== this.props[k]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async updateState(nextState: Partial<S>) {
|
||||
for (let k in nextState) {
|
||||
if (nextState[k] !== this.state[k]) {
|
||||
return super.updateState(nextState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* We define here a simple event bus: it can
|
||||
* - emit events
|
||||
* - add/remove listeners.
|
||||
*
|
||||
* This is a useful pattern of communication in some cases.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export type Callback = (...args: any[]) => void;
|
||||
|
||||
export interface Subscription {
|
||||
owner: any;
|
||||
callback: Callback;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// EventBus
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class EventBus {
|
||||
subscriptions: { [eventType: string]: Subscription[] } = {};
|
||||
|
||||
/**
|
||||
* 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: string, owner: any, callback: 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: string, owner: any) {
|
||||
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: string, ...args: any[]) {
|
||||
const subs = this.subscriptions[eventType] || [];
|
||||
for (let sub of subs) {
|
||||
sub.callback.call(sub.owner, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all subscriptions.
|
||||
*/
|
||||
clear() {
|
||||
this.subscriptions = {};
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Component, PureComponent } from "./component";
|
||||
import { EventBus } from "./event_bus";
|
||||
import { QWeb } from "./qweb";
|
||||
import { Registry } from "./registry";
|
||||
import { connect, Store } from "./store";
|
||||
import * as utils from "./utils";
|
||||
|
||||
export const core = {
|
||||
QWeb,
|
||||
EventBus,
|
||||
Component,
|
||||
PureComponent,
|
||||
utils
|
||||
};
|
||||
|
||||
export const extras = {
|
||||
Store,
|
||||
connect,
|
||||
Registry
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import { SAMPLES } from "./samples.js";
|
||||
|
||||
const { QWeb, Component } = owl.core;
|
||||
|
||||
const MODES = {
|
||||
js: "ace/mode/javascript",
|
||||
css: "ace/mode/css",
|
||||
xml: "ace/mode/xml"
|
||||
};
|
||||
|
||||
const TEMPLATE = `
|
||||
<div class="playground">
|
||||
<div class="left-bar">
|
||||
<div class="menubar">
|
||||
<a class="tab" t-att-class="{active: state.currentTab==='js'}" t-on-click="setTab('js')">JS</a>
|
||||
<a class="tab" t-att-class="{active: state.currentTab==='css'}" t-on-click="setTab('css')">CSS</a>
|
||||
<a class="tab" t-att-class="{active: state.currentTab==='xml'}" t-on-click="setTab('xml')">XML</a>
|
||||
<div class="right-thing">
|
||||
<select t-on-change="setSample">
|
||||
<option t-foreach="SAMPLES" t-as="sample">
|
||||
<t t-esc="sample.description"/>
|
||||
</option>
|
||||
</select>
|
||||
<a class="btn run-code" t-on-click="runCode">▶ Run</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-editor" t-ref="editor"></div>
|
||||
</div>
|
||||
<div class="content" t-ref="content">
|
||||
<div class="welcome">
|
||||
<div>🦉 Odoo Web Lab 🦉</div>
|
||||
<div>v<t t-esc="version"/></div>
|
||||
<div class="url"><a href="https://github.com/ged-odoo/web-core">https://github.com/ged-odoo/web-core</a></div>
|
||||
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
class Playground extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.version = owl._version;
|
||||
this.SAMPLES = SAMPLES;
|
||||
this.inlineTemplate = TEMPLATE;
|
||||
this.state = {
|
||||
currentTab: "js",
|
||||
js: SAMPLES[0].code,
|
||||
css: "",
|
||||
xml: ""
|
||||
};
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.editor = ace.edit(this.refs.editor);
|
||||
this.editor.session.setOption("useWorker", false);
|
||||
this.editor.setValue(this.state.js, -1);
|
||||
this.editor.setFontSize("14px");
|
||||
this.editor.setTheme("ace/theme/monokai");
|
||||
this.editor.session.setMode("ace/mode/javascript");
|
||||
}
|
||||
|
||||
runCode() {
|
||||
this.updateStateFromEditor();
|
||||
|
||||
// inject js
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = "playground-iframe.html";
|
||||
|
||||
iframe.onload = () => {
|
||||
const doc = iframe.contentWindow.document;
|
||||
const script = doc.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.innerHTML = this.state.js;
|
||||
doc.body.appendChild(script);
|
||||
|
||||
// inject css
|
||||
const link = document.createElement("link");
|
||||
link.type = "text/css";
|
||||
link.rel = "stylesheet";
|
||||
link.innerHTML = this.state.css;
|
||||
doc.body.appendChild(link);
|
||||
};
|
||||
|
||||
this.refs.content.innerHTML = "";
|
||||
this.refs.content.appendChild(iframe);
|
||||
}
|
||||
|
||||
setSample(ev) {
|
||||
const sample = SAMPLES.find(s => s.description === ev.target.value);
|
||||
this.editor.setValue(sample.code, -1);
|
||||
}
|
||||
|
||||
updateStateFromEditor() {
|
||||
const value = this.editor.getValue();
|
||||
this.updateState({
|
||||
[this.state.currentTab]: value
|
||||
});
|
||||
}
|
||||
|
||||
setTab(tab) {
|
||||
this.updateStateFromEditor();
|
||||
this.editor.setValue(this.state[tab], -1);
|
||||
|
||||
const mode = MODES[tab];
|
||||
this.editor.session.setMode(mode);
|
||||
this.updateState({ currentTab: tab });
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const qweb = new QWeb();
|
||||
const env = { qweb };
|
||||
const playground = new Playground(env);
|
||||
playground.mount(document.body);
|
||||
});
|
||||
-1002
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* The registry is basically a simple hashmap. It is only a little safer and
|
||||
* more structured than a simple object.
|
||||
*/
|
||||
export class Registry<T> {
|
||||
private map: { [key: string]: T } = {};
|
||||
|
||||
/**
|
||||
* Add an element to the registry. Note that the add method returns the
|
||||
* registry, to it can be chained.
|
||||
*/
|
||||
add(key: string, item: T): Registry<T> {
|
||||
if (key in this.map) {
|
||||
throw new Error(`Key ${key} already exists!`);
|
||||
}
|
||||
this.map[key] = item;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element corresponding to the key
|
||||
*
|
||||
* Nothing is done to check that the key actually exists.
|
||||
*/
|
||||
get(key: string): T | undefined {
|
||||
return this.map[key];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
const HELLO_WORLD = `class HelloWorld extends owl.core.Component {
|
||||
inlineTemplate = \`<div>Hello <t t-esc="props.name"/></div>\`;
|
||||
}
|
||||
|
||||
const env = {
|
||||
qweb: new owl.core.QWeb()
|
||||
};
|
||||
|
||||
const hello = new HelloWorld(env, { name: "World" });
|
||||
hello.mount(document.body);
|
||||
`;
|
||||
|
||||
const WIDGET_COMPOSITION = `class Counter extends owl.core.Component {
|
||||
inlineTemplate = \`
|
||||
<div>
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.value"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>\`;
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = {
|
||||
value: props.initialState || 0
|
||||
};
|
||||
}
|
||||
|
||||
increment(delta) {
|
||||
this.updateState({ value: this.state.value + delta });
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.core.Component {
|
||||
inlineTemplate = \`
|
||||
<div>
|
||||
<t t-widget="Counter" t-props="{initialState: 1}"/>
|
||||
<t t-widget="Counter" t-props="{initialState: 42}"/>
|
||||
</div>\`;
|
||||
|
||||
widgets = { Counter };
|
||||
}
|
||||
|
||||
const env = {
|
||||
qweb: new owl.core.QWeb()
|
||||
};
|
||||
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
`;
|
||||
|
||||
const BENCHMARK_APP = `// todo`;
|
||||
const STATE_MANAGEMENT = `// todo`;
|
||||
|
||||
const EMPTY = ``;
|
||||
|
||||
export const SAMPLES = [
|
||||
{
|
||||
description: "Hello World",
|
||||
code: HELLO_WORLD
|
||||
},
|
||||
{
|
||||
description: "Widget Composition",
|
||||
code: WIDGET_COMPOSITION
|
||||
},
|
||||
{
|
||||
description: "Benchmark application",
|
||||
code: BENCHMARK_APP
|
||||
},
|
||||
{
|
||||
description: "State management app",
|
||||
code: STATE_MANAGEMENT
|
||||
},
|
||||
{
|
||||
description: "Empty",
|
||||
code: EMPTY
|
||||
}
|
||||
];
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Component } from "./component";
|
||||
import { shallowEqual } from "./utils";
|
||||
|
||||
export function connect(mapStateToProps) {
|
||||
return function(Comp) {
|
||||
return class extends Comp {
|
||||
constructor(parent, props?: any) {
|
||||
const env = parent instanceof Component ? parent.env : parent;
|
||||
const storeProps = mapStateToProps(env.store.state);
|
||||
props = Object.assign(props || {}, storeProps);
|
||||
super(parent, props);
|
||||
this.__widget__.currentStoreProps = storeProps;
|
||||
}
|
||||
mounted() {
|
||||
this.env.store.on("update", this, () => {
|
||||
const storeProps = mapStateToProps(this.env.store.state);
|
||||
if (!shallowEqual(storeProps, this.__widget__.currentStoreProps)) {
|
||||
this.__widget__.currentStoreProps = storeProps;
|
||||
// probably not optimal, will do 2 object.assign, one here and
|
||||
// one in updateProps.
|
||||
const nextProps = Object.assign(
|
||||
{},
|
||||
this.props,
|
||||
this.__widget__.currentStoreProps
|
||||
);
|
||||
this.updateProps(nextProps, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.store.off("update", this);
|
||||
}
|
||||
updateProps(nextProps, forceUpdate) {
|
||||
nextProps = Object.assign(nextProps, this.__widget__.currentStoreProps);
|
||||
return super.updateProps(nextProps, forceUpdate);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface StoreConfig {
|
||||
state?: any;
|
||||
actions?: any;
|
||||
mutations?: any;
|
||||
}
|
||||
|
||||
interface StoreOption {
|
||||
debug?: boolean;
|
||||
}
|
||||
export class Store extends EventBus {
|
||||
_state: any;
|
||||
actions: any;
|
||||
mutations: any;
|
||||
_isMutating: boolean = false;
|
||||
history: any[] = [];
|
||||
debug: boolean;
|
||||
|
||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||
super();
|
||||
this.debug = options.debug || false;
|
||||
this._state = Object.assign({}, config.state);
|
||||
this.actions = config.actions;
|
||||
this.mutations = config.mutations;
|
||||
|
||||
if (this.debug) {
|
||||
this.history.push({ state: this.state });
|
||||
}
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this._clone(this._state);
|
||||
}
|
||||
|
||||
dispatch(action, payload) {
|
||||
if (!this.actions[action]) {
|
||||
throw new Error(`[Error] action ${action} is undefined`);
|
||||
}
|
||||
this.actions[action](
|
||||
{
|
||||
commit: this.commit.bind(this),
|
||||
state: this.state
|
||||
},
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
async commit(type, payload) {
|
||||
if (!this.mutations[type]) {
|
||||
throw new Error(`[Error] mutation ${type} is undefined`);
|
||||
}
|
||||
this._isMutating = true;
|
||||
|
||||
this.mutations[type].call(null, this._state, payload);
|
||||
if (this.debug) {
|
||||
this.history.push({
|
||||
state: this.state,
|
||||
mutation: type,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
await Promise.resolve();
|
||||
if (this._isMutating) {
|
||||
this._isMutating = false;
|
||||
this.trigger("update", this.state);
|
||||
}
|
||||
}
|
||||
|
||||
_clone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
}
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
export function escape(str: string | number | undefined): string {
|
||||
if (str === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof str === "number") {
|
||||
return String(str);
|
||||
}
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, "'")
|
||||
.replace(/`/g, "`");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove trailing and leading spaces
|
||||
*/
|
||||
export function htmlTrim(s: string): string {
|
||||
let result = s.replace(/(^\s+|\s+$)/g, "");
|
||||
if (s[0] === " ") {
|
||||
result = " " + result;
|
||||
}
|
||||
if (result !== " " && s[s.length - 1] === " ") {
|
||||
result = result + " ";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a function that will generate unique id numbers
|
||||
*/
|
||||
export function idGenerator(): () => number {
|
||||
let nextID = 1;
|
||||
return () => nextID++;
|
||||
}
|
||||
|
||||
export type HashFn = (args: any[]) => string;
|
||||
|
||||
export function memoize<R, T extends (...args: any[]) => R>(
|
||||
f: T,
|
||||
hash?: HashFn
|
||||
): T {
|
||||
if (!hash) {
|
||||
hash = args => args.map(a => String(a)).join(",");
|
||||
}
|
||||
let cache: { [key: string]: R } = {};
|
||||
function memoizedFunction(...args: any[]) {
|
||||
let hashValue = hash!(args);
|
||||
if (!(hashValue in cache)) {
|
||||
cache[hashValue] = f(...args);
|
||||
}
|
||||
return cache[hashValue];
|
||||
}
|
||||
return memoizedFunction as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function, that, as long as it continues to be invoked, will not
|
||||
* be triggered. The function will be called after it stops being called for
|
||||
* N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
* leading edge, instead of the trailing.
|
||||
*
|
||||
* Inspired by https://davidwalsh.name/javascript-debounce-function
|
||||
*/
|
||||
export function debounce(
|
||||
func: Function,
|
||||
wait: number,
|
||||
immediate?: boolean
|
||||
): Function {
|
||||
let timeout;
|
||||
return function(this: any) {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
function later() {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
}
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface Tree<T> {
|
||||
children: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a node in a tree.
|
||||
*
|
||||
* This will traverse the tree (depth first) and return the first child that
|
||||
* matches the predicate, if any
|
||||
*/
|
||||
export function findInTree<T extends Tree<T>>(
|
||||
tree: T,
|
||||
predicate: (t: T) => boolean
|
||||
): T | null {
|
||||
if (predicate(tree)) {
|
||||
return tree;
|
||||
}
|
||||
for (let child of tree.children) {
|
||||
let match = findInTree(child, predicate);
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shallowEqual(objA, objB) {
|
||||
if (objA === objB) {
|
||||
return true;
|
||||
}
|
||||
const keysA = Object.keys(objA);
|
||||
for (let key of keysA) {
|
||||
if (!(key in objB) || objA[key] !== objB[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function patch(C: any, patchName: string, patch: any) {
|
||||
const proto = C.prototype;
|
||||
if (!proto.__patches) {
|
||||
proto.__patches = {
|
||||
origMethods: {},
|
||||
patches: {},
|
||||
current: []
|
||||
};
|
||||
}
|
||||
if (proto.__patches.patches[patchName]) {
|
||||
throw new Error(`Patch [${patchName}] already exists`);
|
||||
}
|
||||
proto.__patches.patches[patchName] = patch;
|
||||
applyPatch(proto, patch);
|
||||
proto.__patches.current.push(patchName);
|
||||
|
||||
function applyPatch(proto, patch) {
|
||||
Object.keys(patch).forEach(function(methodName) {
|
||||
const method = patch[methodName];
|
||||
if (typeof method === "function") {
|
||||
const original = proto[methodName];
|
||||
if (!(methodName in proto.__patches.origMethods)) {
|
||||
proto.__patches.origMethods[methodName] = original;
|
||||
}
|
||||
proto[methodName] = function(...args) {
|
||||
this._super = original;
|
||||
return method.call(this, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function unpatch(C: any, patchName: string) {
|
||||
const proto = C.prototype;
|
||||
const patchInfo = proto.__patches;
|
||||
delete proto.__patches;
|
||||
|
||||
// reset to original
|
||||
for (let k in patchInfo.origMethods) {
|
||||
proto[k] = patchInfo.origMethods[k];
|
||||
}
|
||||
|
||||
// apply other patches
|
||||
for (let name of patchInfo.current) {
|
||||
if (name !== patchName) {
|
||||
patch(C, name, patchInfo.patches[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user