[REF] initial prototype of owl 2

This commit is contained in:
Géry Debongnie
2020-11-26 16:45:25 +01:00
committed by Aaron Bohy
parent c06049076a
commit e746574a1d
186 changed files with 29153 additions and 33120 deletions
+19 -771
View File
@@ -1,781 +1,29 @@
import { Observer } from "../core/observer";
import { OwlEvent } from "../core/owl_event";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { patch, VNode } from "../vdom/index";
import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
import { Browser, browser } from "../browser";
import type { ComponentNode } from "./component_node";
/**
* Owl Component System
*
* This file introduces a declarative and composable component system. It
* contains:
*
* - the Env interface (generic type for the environment)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class
*/
// -----------------------------------------------------------------------------
// Component Class
// -----------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
export class Component {
static template: string = "";
/**
* An Env (environment) is an object that will be (mostly) shared between all
* components of an Owl application. It is the location which should contain
* the qweb instance necessary to render all components.
*
* Note that it is totally fine to extend the environment with application
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
* if we are in "mobile" mode), or a shared bus could be useful.
*/
export interface Env {
qweb: QWeb;
browser: Browser;
}
props: any;
env: any;
__owl__: ComponentNode;
export type MountPosition = "first-child" | "last-child" | "self";
interface MountOptions {
position?: MountPosition;
}
export const enum STATUS {
CREATED,
WILLSTARTED, // willstart has been called
RENDERED, // first render is completed (so, vnode is now defined)
MOUNTED, // is ready, and in DOM. It has a valid el
UNMOUNTED, // has a valid el, but is not in DOM
DESTROYED,
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
* component tree.
*/
interface Internal<T extends Env> {
// each component has a unique id, useful mostly to handle parent/child
// relationships
readonly id: number;
depth: number;
vnode: VNode | null;
pvnode: VNode | null;
status: STATUS;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<any, T> | null;
children: { [key: number]: Component<any, T> };
// children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered.
cmap: { [key: number]: number };
currentFiber: Fiber | null;
// parentLastFiberId is there to help the parent component to detect, among
// its children, those that are not used anymore and thus can be destroyed
parentLastFiberId: number;
// when a rendering is initiated by a parent, it may set variables in 'scope'
// (typically when the component is rendered in a slot). We need to
// store that information in case the component would be re-rendered later on.
scope: any;
boundHandlers: { [key: number]: any };
observer: Observer | null;
renderFn: CompiledTemplate;
mountedCB: Function | null;
willUnmountCB: Function | null;
willPatchCB: Function | null;
patchedCB: Function | null;
willStartCB: Function | null;
willUpdatePropsCB: Function | null;
classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
}
export const portalSymbol = Symbol("portal"); // FIXME
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<Props extends {} = any, T extends Env = Env> {
readonly __owl__: Internal<T>;
static template?: string | null = null;
static _template?: string | null = null;
static current: Component | null = null;
static components = {};
static props?: any;
static defaultProps?: any;
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
/**
* The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
constructor(props: any, env: any, node: ComponentNode) {
this.props = props;
this.env = env;
this.__owl__ = node;
}
get el(): HTMLElement | Text | undefined {
const node = this.__owl__;
return node.bdom ? (node.bdom.firstNode() as any) : undefined;
}
env: T;
props: Props;
//--------------------------------------------------------------------------
// Lifecycle
//--------------------------------------------------------------------------
/**
* Creates an instance of Component.
*
* Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent?: Component<any, T> | null, props?: Props) {
Component.current = this;
let constr = this.constructor as any;
const defaultProps = constr.defaultProps;
if (defaultProps) {
props = props || ({} as Props);
this.__applyDefaultProps(props, defaultProps);
}
this.props = <Props>props;
if (QWeb.dev) {
QWeb.utils.validateProps(constr, this.props);
}
const id: number = nextId++;
let depth;
if (parent) {
this.env = parent.env;
const __powl__ = parent.__owl__;
__powl__.children[id] = this;
depth = __powl__.depth + 1;
} else {
// we are the root component
this.env = (this.constructor as any).env;
if (!this.env.qweb) {
this.env.qweb = new QWeb();
}
// TODO: remove this in owl 2.0
if (!this.env.browser) {
this.env.browser = browser;
}
this.env.qweb.on("update", this, () => {
switch (this.__owl__.status) {
case STATUS.MOUNTED:
this.render(true);
break;
case STATUS.DESTROYED:
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
break;
}
});
depth = 0;
}
const qweb = this.env.qweb;
const template = constr.template || this.__getTemplate(qweb);
this.__owl__ = {
id: id,
depth: depth,
vnode: null,
pvnode: null,
status: STATUS.CREATED,
parent: parent || null,
children: {},
cmap: {},
currentFiber: null,
parentLastFiberId: 0,
boundHandlers: {},
mountedCB: null,
willUnmountCB: null,
willPatchCB: null,
patchedCB: null,
willStartCB: null,
willUpdatePropsCB: null,
observer: null,
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null,
scope: null,
};
if (constr.style) {
this.__applyStyles(constr);
}
this.setup();
}
/**
* setup is run just after the component is constructed. This is the standard
* location where the component can setup its hooks. It has some advantages
* over the constructor:
* - it can be patched (useful in odoo ecosystem)
* - it does not need to propagate the arguments to the super call
*
* Note: this method should not be called manually.
*/
setup() {}
/**
* 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 component 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() {}
/**
* The willUpdateProps is an asynchronous hook, called just before new props
* are set. This is useful if the component needs some asynchronous task
* performed, depending on the props (for example, assuming that the props are
* some record Id, fetching the record data).
*
* This hook is not called during the first render (but willStart is called
* and performs a similar job).
*/
async willUpdateProps(nextProps: Props) {}
/**
* The willPatch hook is called just before the DOM patching process starts.
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*/
willPatch(): any {}
/**
* This hook is called whenever a component did actually update its props,
* state or env.
*
* This method is not called on the initial render. It is useful to interact
* with the DOM (for example, through an external library) whenever the
* component was updated.
*
* Updating the component state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*/
patched() {}
/**
* willUnmount is a hook that is called each time just before a component is
* unmounted 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() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*
* It needs to be implemented by a component that is designed to handle the
* error properly.
*/
catchError?(error?: Error): void;
//--------------------------------------------------------------------------
// 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.
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
const position = options.position || "last-child";
const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber;
switch (__owl__.status) {
case STATUS.CREATED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__prepareAndRender(fiber, () => {});
return scheduler.addFiber(fiber);
}
case STATUS.WILLSTARTED:
case STATUS.RENDERED:
currentFiber.target = target;
currentFiber.position = position;
return scheduler.addFiber(currentFiber);
case STATUS.UNMOUNTED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
}
case STATUS.MOUNTED: {
if (position !== "self" && this.el!.parentNode !== target) {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
} else {
return Promise.resolve();
}
}
case STATUS.DESTROYED:
throw new Error("Cannot mount a destroyed component");
}
}
/**
* 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() {
if (this.__owl__.status === STATUS.MOUNTED) {
this.__callWillUnmount();
this.el!.remove();
}
}
/**
* 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: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber;
if (!__owl__.vnode && !currentFiber) {
return;
}
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
return scheduler.addFiber(currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const status = __owl__.status;
const fiber = new Fiber(null, this, force, null, null);
Promise.resolve().then(() => {
if (__owl__.status === STATUS.MOUNTED || status !== STATUS.MOUNTED) {
if (fiber.isCompleted || fiber.isRendered) {
return;
}
this.__render(fiber);
} else {
// we were mounted when render was called, but we aren't anymore, so we
// were actually about to be unmounted ; we can thus forget about this
// fiber
fiber.isCompleted = true;
__owl__.currentFiber = null;
}
});
return scheduler.addFiber(fiber);
}
/**
* 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 component. Most
* components will be automatically destroyed.
*/
destroy() {
const __owl__ = this.__owl__;
if (__owl__.status !== STATUS.DESTROYED) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
el.remove();
}
}
}
/**
* 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: Props): boolean {
return true;
}
/**
* 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<T = any>(eventType: string, payload?: T) {
this.__trigger<T>(this, eventType, payload);
}
//--------------------------------------------------------------------------
// 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: Component | null) {
const __owl__ = this.__owl__;
if (__owl__.status === STATUS.MOUNTED) {
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.status = STATUS.UNMOUNTED;
}
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__.status = STATUS.DESTROYED;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
}
}
__callMounted() {
const __owl__ = this.__owl__;
__owl__.status = STATUS.MOUNTED;
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
}
}
__callWillUnmount() {
const __owl__ = this.__owl__;
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.status = STATUS.UNMOUNTED;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
__owl__.currentFiber.root.counter = 0;
}
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.status === STATUS.MOUNTED) {
comp.__callWillUnmount();
}
}
}
/**
* Private trigger method, allows to choose the component which triggered
* the event in the first place
*/
__trigger<T>(component: Component, eventType: string, payload?: T) {
if (this.el) {
const ev = new OwlEvent<T>(component, eventType, {
bubbles: true,
cancelable: true,
detail: payload,
});
const triggerHook = this.env[portalSymbol as any];
if (triggerHook) {
triggerHook(ev);
}
this.el.dispatchEvent(ev);
}
}
/**
* 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: Props, parentFiber: Fiber, scope: any): Promise<void> {
this.__owl__.scope = scope;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
parentFiber.lastChild!.sibling = fiber;
}
parentFiber.lastChild = fiber;
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, nextProps);
}
await Promise.all([
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps),
]);
if (fiber.isCompleted) {
return;
}
this.props = nextProps;
this.__render(fiber);
}
}
/**
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(target: HTMLElement | VNode | DocumentFragment, vnode: VNode) {
this.__owl__.vnode = patch(target as any, vnode);
}
/**
* The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
parentFiber.lastChild!.sibling = fiber;
}
parentFiber.lastChild = fiber;
this.__prepareAndRender(fiber, cb);
return fiber;
}
/**
* Apply the stylesheets defined by the component. Note that we need to make
* sure all inherited stylesheets are applied as well. We then delete the
* `style` key from the constructor to make sure we do not apply it again.
*/
private __applyStyles(constr) {
while (constr && constr.style) {
if (constr.hasOwnProperty("style")) {
activateSheet(constr.style, constr.name);
delete constr.style;
}
constr = constr.__proto__;
}
}
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) {
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
let template: string = p.name;
while (!(template in qweb.templates) && p !== Component) {
p = p.__proto__;
template = p.name;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
p._template = template;
}
}
return p._template;
}
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try {
const proms = Promise.all([
this.willStart(),
this.__owl__.willStartCB && this.__owl__.willStartCB(),
]);
this.__owl__.status = STATUS.WILLSTARTED;
await proms;
if (this.__owl__.status === <any>STATUS.DESTROYED) {
return Promise.resolve();
}
} catch (e) {
fiber.handleError(e);
return Promise.resolve();
}
if (!fiber.isCompleted) {
this.__render(fiber);
this.__owl__.status = STATUS.RENDERED;
cb();
}
}
__render(fiber: Fiber) {
const __owl__ = this.__owl__;
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let error;
try {
let vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers,
fiber: fiber,
});
// we iterate over the children to detect those that no longer belong to the
// current rendering: those ones, if not mounted yet, can (and have to) be
// destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) {
const child = __owl__.children[childKey];
const childOwl = child.__owl__;
if (childOwl.status !== STATUS.MOUNTED && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
// if some of its parent have been unmounted, the child could actually
// still be attached to its parent, and this may be important if we
// want to remount the parent, because the vdom need to match the
// actual DOM
child.__destroy(childOwl.parent);
if (childOwl.pvnode) {
// we remove the key here to make sure that the patching algorithm
// is able to make the difference between this pvnode and an eventual
// other instance of the same component
delete childOwl.pvnode.key;
// Since the component has been unmounted, we do not want to actually
// call a remove hook. This is pretty important, since the t-component
// directive actually disabled it, so the vdom algorithm will just
// not remove the child elm if we don't remove the hook.
delete childOwl.pvnode.data!.hook!.remove;
}
}
}
if (!vnode) {
throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
}
fiber.vnode = vnode;
// we apply here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
const data = vnode.data!;
data.class = Object.assign(data.class || {}, __owl__.classObj);
}
} catch (e) {
error = e;
}
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
fiber.root.counter--;
fiber.isRendered = true;
if (error) {
fiber.handleError(error);
}
}
/**
* Apply default props (only top level).
*
* Note that this method does modify in place the props
*/
__applyDefaultProps(props: Object, defaultProps: Object) {
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
render(): Promise<void> {
return this.__owl__.render();
}
}
interface MountParameters {
env?: Env;
target: HTMLElement | DocumentFragment;
props?: any;
position?: MountOptions["position"];
}
interface Type<T> extends Function {
new (...args: any[]): T;
}
export async function mount<T extends Type<Component>>(
C: T,
params: MountParameters
): Promise<InstanceType<T>> {
const { env, props, target } = params;
let origEnv = C.hasOwnProperty("env") ? (C as any).env : null;
if (env) {
(C as any as typeof Component).env = env;
}
const component: Component = new C(null, props);
if (origEnv) {
(C as any).env = origEnv;
} else {
delete (C as any).env;
}
const position = params.position || "last-child";
await component.mount(target, { position });
return component as any;
}
+256
View File
@@ -0,0 +1,256 @@
import type { App } from "../app";
import { BDom, VNode } from "../blockdom";
import { Component } from "./component";
import {
Fiber,
makeChildFiber,
makeRootFiber,
MountFiber,
RootFiber,
__internal__destroyed,
} from "./fibers";
import { STATUS } from "./status";
export function component(
name: string | typeof Component,
props: any,
key: string,
ctx: ComponentNode,
parent: any
): ComponentNode {
let node: any = ctx.children[key];
let isDynamic = typeof name !== "string";
if (node && node.status < STATUS.MOUNTED) {
node.destroy();
node = undefined;
}
if (isDynamic && node && node.component.constructor !== name) {
node = undefined;
}
const parentFiber = ctx.fiber!;
if (node) {
node.updateAndRender(props, parentFiber);
} else {
// new component
const C = isDynamic ? name : parent.constructor.components[name as any];
node = new ComponentNode(C, props, ctx.app);
ctx.children[key] = node;
const fiber = makeChildFiber(node, parentFiber);
node.initiateRender(fiber);
}
return node;
}
// -----------------------------------------------------------------------------
// Component VNode
// -----------------------------------------------------------------------------
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = any> implements VNode<ComponentNode> {
el?: HTMLElement | Text | undefined;
handlers: any = null;
app: App;
fiber: Fiber | null = null;
component: InstanceType<T>;
bdom: BDom | null = null;
status: STATUS = STATUS.NEW;
renderFn: Function;
children: { [key: string]: ComponentNode } = Object.create(null);
slots: any = {};
refs: any = {};
willStart: LifecycleHook[] = [];
willUpdateProps: LifecycleHook[] = [];
willUnmount: LifecycleHook[] = [];
mounted: LifecycleHook[] = [];
willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = [];
destroyed: LifecycleHook[] = [];
constructor(C: T, props: any, app: App) {
currentNode = this;
this.app = app;
this.component = new C(props, app.env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(null, this.component, this);
this.component.setup();
}
mountComponent(target: any): Promise<InstanceType<T>> {
const fiber = new MountFiber(this, target);
this.app.scheduler.addFiber(fiber);
this.initiateRender(fiber);
return fiber.promise.then(() => this.component);
}
async initiateRender(fiber: Fiber | MountFiber) {
if (this.mounted.length) {
fiber.root.mounted.push(fiber);
}
const component = this.component;
const prom = Promise.all(this.willStart.map((f) => f.call(component)));
await prom;
if (this.status === STATUS.NEW && this.fiber === fiber) {
this._render(fiber);
}
}
async render() {
if (this.fiber && !this.fiber.bdom) {
return this.fiber.root.promise;
}
if (!this.bdom && !this.fiber) {
// should find a way to return the future mounting promise
return;
}
const fiber = makeRootFiber(this);
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status === STATUS.DESTROYED) {
return;
}
if (this.fiber === fiber) {
this._render(fiber);
}
return fiber.root.promise;
}
_render(fiber: Fiber | RootFiber) {
try {
fiber.bdom = this.renderFn();
} catch (e) {
fiber.root.error = e;
this.handleError(fiber);
}
fiber.root.counter--;
}
handleError(fiber: Fiber) {
fiber.node.app.destroy();
}
destroy() {
if (this.status === STATUS.MOUNTED) {
callWillUnmount(this);
this.bdom!.remove();
}
callDestroyed(this);
function callWillUnmount(node: ComponentNode) {
const component = node.component;
for (let cb of node.willUnmount) {
cb.call(component);
}
for (let child of Object.values(node.children)) {
if (child.status === STATUS.MOUNTED) {
callWillUnmount(child);
}
}
}
function callDestroyed(node: ComponentNode) {
const component = node.component;
node.status = STATUS.DESTROYED;
for (let child of Object.values(node.children)) {
callDestroyed(child);
}
for (let cb of node.destroyed) {
cb.call(component);
}
}
}
async updateAndRender(props: any, parentFiber: Fiber) {
// update
const fiber = makeChildFiber(this, parentFiber);
if (this.willPatch.length) {
parentFiber.root.willPatch.push(fiber);
}
if (this.patched.length) {
parentFiber.root.patched.push(fiber);
}
const component = this.component;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
if (fiber !== this.fiber) {
return;
}
this.component.props = props;
this._render(fiber);
}
// ---------------------------------------------------------------------------
// Block DOM methods
// ---------------------------------------------------------------------------
firstNode(): Node | undefined {
const bdom = this.bdom;
return bdom ? bdom.firstNode() : undefined;
}
mount(parent: HTMLElement, anchor: ChildNode) {
const bdom = this.fiber!.bdom!;
this.bdom = bdom;
bdom.mount(parent, anchor);
this.status = STATUS.MOUNTED;
this.fiber!.appliedToDom = true;
this.fiber = null;
if (this.handlers) {
for (let i = 0; i < this.handlers.length; i++) {
const handler = this.handlers[i];
const eventType = handler[0];
const el = bdom.el!;
el.addEventListener(eventType, (ev: Event) => {
const info = this.handlers![i];
const [, ctx, method] = info;
(ctx.__owl__.component as any)[method](ev);
});
}
}
}
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
this.bdom!.moveBefore(other ? other.bdom : null, afterNode);
}
patch() {
this.bdom!.patch(this!.fiber!.bdom!, false);
this.fiber!.appliedToDom = true;
this.fiber = null;
}
beforeRemove() {
visitRemovedNodes(this);
}
remove() {
this.bdom!.remove();
}
}
function visitRemovedNodes(node: ComponentNode) {
if (node.status === STATUS.MOUNTED) {
const component = node.component;
for (let cb of node.willUnmount) {
cb.call(component);
}
}
for (let child of Object.values(node.children)) {
visitRemovedNodes(child);
}
node.status = STATUS.DESTROYED;
if (node.destroyed.length) {
__internal__destroyed.push(node);
}
}
-516
View File
@@ -1,516 +0,0 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context";
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
import { STATUS } from "./component";
//------------------------------------------------------------------------------
// t-component
//------------------------------------------------------------------------------
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}",
});
QWeb.utils.defineProxy = function defineProxy(target, source) {
for (let k in source) {
Object.defineProperty(target, k, {
get() {
return source[k];
},
set(val) {
source[k] = val;
},
});
}
};
QWeb.utils.assignHooks = function assignHooks(dataObj, hooks) {
if ("hook" in dataObj) {
const hookObject = dataObj.hook;
for (let name in hooks) {
const current = hookObject[name];
const fn = hooks[name];
if (current) {
hookObject[name] = (...args) => {
current(...args);
fn(...args);
};
} else {
hookObject[name] = fn;
}
}
} else {
dataObj.hook = hooks;
}
};
/**
* The t-component 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
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-component directive for the following
* situation:
* ```xml
* <Child
* t-key="'somestring'"
* flag="state.flag"
* t-transition="fade"/>
* ```
*
* ```js
* // we assign utils on top of the function because it will be useful for
* // each components
* let utils = this.utils;
*
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-component directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // def3 is the promise that will contain later either the new component
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the component was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent component: it maps keys to component ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
* key5 in context.__owl__.cmap
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // We keep the index of the position of the component in the closure. We push
* // null to reserve the slot, and will replace it later by the component vnode,
* // when it will be ready (do not forget that preparing/rendering a component is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a component, 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 component 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 component. 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.component && context.components[componentKey4] || QWeb.component[componentKey4];
* if (!W4) {
* throw new Error("Cannot find the definition of component 'child'");
* }
* w4 = new W4(owner, props4);
*
* // Whenever we rerender the parent component, we need to be sure that we
* // are able to find the component instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the component from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // __prepare is called, to basically call willStart, then render the
* // component
* def3 = w4.__prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the component). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
* // vnode.
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // component at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the __mount method will patch the component vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4.__mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the component
* utils.transitionInsert(vn, "fade");
* },
* remove() {
* // override with empty function to prevent from removing the node
* // directly. It will be removed when destroy is called anyway, which
* // delays the removal if there are animations.
* },
* destroy() {
* // if there are animations, we delay the call to destroy on the
* // component, if not, we call it directly.
* let finalize = () => {
* w4.destroy();
* };
* utils.transitionRemove(vn, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // component, so we can reuse it later whenever we update the component
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to __updateProps is the actual component update
* // 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 component was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the component's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
* }
* // like above, we register the pvnode to the children list, so it
* // will not be patched out of the dom.
* let pvnode = w4.__owl__.pvnode;
* c1[_2_index] = pvnode;
* });
* }
*
* // we register the deferred here so the parent can coordinate its patch operation
* // with all the children.
* extra.promises.push(def3);
* return vn1;
* ```
*/
QWeb.addDirective({
name: "component",
extraNames: ["props"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine(`// Component '${value}'`);
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
ctx.rootContext.shouldDefineScope = true;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
const events: [string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {};
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, value]);
} else if (name === "t-transition") {
if (QWeb.enableTransitions) {
transition = value;
}
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
if (value.includes("=>")) {
props[name] = ctx.captureExpression(value);
} else {
props[name] = ctx.formatExpression(value) || "undefined";
}
}
}
}
// computing the props string representing the props object
let propStr = Object.keys(props)
.map((k) => k + ":" + props[k])
.join(",");
let componentID = ctx.generateID();
let hasDefinedKey = false;
let templateKey;
if (node.tagName === "t" && !node.hasAttribute("t-key") && value.match(INTERP_REGEXP)) {
defineComponentKey();
const id = ctx.generateID();
// the ___ is to make sure we have no possible conflict with normal
// template keys
ctx.addLine(`let k${id} = '___' + componentKey${componentID}`);
templateKey = `k${id}`;
} else {
templateKey = ctx.generateTemplateKey();
}
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
let finalizeComponentCode = `w${componentID}.destroy();`;
if (ref) {
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
delete w${componentID}.__owl__.transitionInserted;
utils.transitionRemove(vn, '${transition}', finalize);`;
}
let createHook = "";
let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class");
let styleAttr = node.getAttribute("style");
let tattStyle = node.getAttribute("t-att-style");
if (tattStyle) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
if (classAttr) {
let classDef = classAttr
.trim()
.split(/\s+/)
.map((a) => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `utils.toClassObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
}
let eventsCode = events
.map(function ([name, value]) {
const capture = name.match(/\.capture/);
name = capture ? name.replace(/\.capture/, "") : name;
const { event, handler } = makeHandlerCode(
ctx,
name,
value,
false,
T_COMPONENT_MODS_CODE
);
if (capture) {
return `vn.elm.addEventListener('${event}', ${handler}, true);`;
}
return `vn.elm.addEventListener('${event}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `utils.assignHooks(vnode.data, {create(_, vn){${styleCode}${eventsCode}}});`;
}
ctx.addLine(
`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`
);
let shouldProxy = !ctx.parentNode;
if (shouldProxy) {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
ctx.addLine(`let props${componentID} = Object.assign({}, ${dynamicProp}, {${propStr}});`);
} else {
ctx.addLine(`let props${componentID} = {${propStr}};`);
}
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
);
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
let registerCode = "";
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
// SLOTS
const hasSlots = node.childNodes.length;
let scope = hasSlots ? `utils.combine(context, scope)` : "undefined";
ctx.addIf(`w${componentID}`);
// need to update component
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.status === ${STATUS.DESTROYED}) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
if (registerCode) {
ctx.addLine(registerCode);
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
}
ctx.addElse();
// new component
function defineComponentKey() {
if (!hasDefinedKey) {
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
hasDefinedKey = true;
}
}
defineComponentKey();
const contextualValue = value.match(INTERP_REGEXP) ? "false" : ctx.formatExpression(value);
ctx.addLine(
`let W${componentID} = ${contextualValue} || context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
if (transition) {
ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`);
ctx.addLine(
`w${componentID}.__patch = (t, vn) => {__patch${componentID}.call(w${componentID}, t, vn); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
);
}
ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
if (hasSlots) {
const clone = <Element>node.cloneNode(true);
// The next code is a fallback for compatibility reason. It accepts t-set
// elements that are direct children with a non empty body as nodes defining
// the content of a slot.
//
// This is wrong, but is necessary to prevent breaking all existing Owl
// code using slots. This will be removed in v2.0 someday. Meanwhile,
// please use t-set-slot everywhere you need to set the content of a
// slot.
for (let node of clone.children) {
if (node.hasAttribute("t-set") && node.hasChildNodes()) {
node.setAttribute("t-set-slot", node.getAttribute("t-set")!);
node.removeAttribute("t-set");
}
}
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
const slotNames = new Set<string>();
const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
// check if this is defined in a sub component (in which case it should
// be ignored)
let el = slotNode.parentElement;
let isInSubComponent = false;
while (el !== clone) {
if (
el!.hasAttribute("t-component") ||
el!.tagName[0] === el!.tagName[0].toUpperCase()
) {
isInSubComponent = true;
break;
}
el = el.parentElement;
}
if (isInSubComponent) {
continue;
}
let key = slotNode.getAttribute("t-set-slot")!;
if (slotNames.has(key)) {
continue;
}
slotNames.add(key);
slotNode.removeAttribute("t-set-slot");
slotNode.parentElement!.removeChild(slotNode);
const slotFn = qweb._compile(`slot_${key}_template`, { elem: slotNode, hasParent: true });
QWeb.slots[`${slotId}_${key}`] = slotFn;
}
}
if (clone.childNodes.length) {
let hasContent = false;
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
hasContent =
hasContent || (child instanceof Text ? Boolean(child.textContent.trim().length) : true);
t.appendChild(child);
}
if (hasContent) {
const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true });
QWeb.slots[`${slotId}_default`] = slotFn;
}
}
}
ctx.addLine(
`let fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine(
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
if (registerCode) {
ctx.addLine(registerCode);
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
}
ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
return true;
},
});
-366
View File
@@ -1,366 +0,0 @@
import { h, VNode } from "../vdom/index";
import { Component, MountPosition, STATUS } from "./component";
import { scheduler } from "./scheduler";
/**
* Owl Fiber Class
*
* Fibers are small abstractions designed to contain all the internal state
* associated with a "rendering work unit", relative to a specific component.
*
* A rendering will cause the creation of a fiber for each impacted components.
*
* Fibers capture all that necessary information, which is critical to owl
* asynchronous rendering pipeline. Fibers can be cancelled, can be in different
* states and in general determine the state of the rendering.
*/
export class Fiber {
static nextId: number = 1;
id: number = Fiber.nextId++;
// The force attribute determines if a rendering should bypass the `shouldUpdate`
// method potentially implemented by a component. It is usually set to false.
force: boolean;
// isCompleted means that the rendering corresponding to this fiber's work is
// done, either because the component has been mounted or patched, or because
// fiber has been cancelled.
isCompleted: boolean = false;
// the fibers corresponding to component updates (updateProps) need to call
// the willPatch and patched hooks from the corresponding component. However,
// fibers corresponding to a new component do not need to do that. So, the
// shouldPatch hook is the boolean that we check whenever we need to apply
// a patch.
shouldPatch: boolean = true;
// isRendered is the last state of a fiber. If true, this means that it has
// been rendered and is inert (so, it should not be taken into account when
// counting the number of active fibers).
isRendered: boolean = false;
// the counter number is a critical information. It is only necessary for a
// root fiber. For that fiber, this number counts the number of active sub
// fibers. When that number reaches 0, the fiber can be applied by the
// scheduler.
counter: number = 0;
target: HTMLElement | DocumentFragment | null;
position: MountPosition | null;
scope: any;
component: Component;
vnode: VNode | null = null;
root: Fiber;
child: Fiber | null = null;
sibling: Fiber | null = null;
lastChild: Fiber | null = null;
parent: Fiber | null = null;
error?: Error;
constructor(
parent: Fiber | null,
component: Component,
force: boolean,
target: HTMLElement | DocumentFragment | null,
position: MountPosition | null
) {
this.component = component;
this.force = force;
this.target = target;
this.position = position;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = __owl__.currentFiber;
if (oldFiber && !oldFiber.isCompleted) {
this.force = true;
if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber);
return oldFiber;
} else {
this._remapFiber(oldFiber);
}
}
this.root.counter++;
__owl__.currentFiber = this;
}
/**
* When the oldFiber is not completed yet, and both oldFiber and this fiber
* are root fibers, we want to reuse the oldFiber instead of creating a new
* one. Doing so will guarantee that the initiator(s) of those renderings will
* be notified (the promise will resolve) when the last rendering will be done.
*
* This function thus assumes that oldFiber is a root fiber.
*/
_reuseFiber(oldFiber: Fiber) {
oldFiber.cancel(); // cancel children fibers
oldFiber.target = this.target || oldFiber.target;
oldFiber.position = this.position || oldFiber.position;
oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) {
// remove relation to children
oldFiber.child.parent = null;
oldFiber.child = null;
oldFiber.lastChild = null;
}
oldFiber.counter = 1; // re-initialize counter
oldFiber.id = Fiber.nextId++;
}
/**
* In some cases, a rendering initiated at some component can detect that it
* should be part of a larger rendering initiated somewhere up the component
* tree. In that case, it needs to cancel the previous rendering and
* remap itself as a part of the current parent rendering.
*/
_remapFiber(oldFiber: Fiber) {
oldFiber.cancel();
this.shouldPatch = oldFiber.shouldPatch;
if (oldFiber === oldFiber.root) {
oldFiber.counter++;
}
if (oldFiber.parent && !this.parent) {
// re-map links
this.parent = oldFiber.parent;
this.root = this.parent.root;
this.sibling = oldFiber.sibling;
if (this.parent.lastChild === oldFiber) {
this.parent.lastChild = this;
}
if (this.parent.child === oldFiber) {
this.parent.child = this;
} else {
let current = this.parent.child!;
while (true) {
if (current.sibling === oldFiber) {
current.sibling = this;
break;
}
current = current.sibling!;
}
}
}
}
/**
* This function has been taken from
* https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7
*/
_walk(doWork: (f: Fiber) => Fiber | null) {
let root = this;
let current: Fiber = this;
while (true) {
const child = doWork(current);
if (child) {
current = child;
continue;
}
if (current === root) {
return;
}
while (!current.sibling) {
if (!current.parent || current.parent === root) {
return;
}
current = current.parent;
}
current = current.sibling;
}
}
/**
* Successfully complete the work of the fiber: call the mount or patch hooks
* and patch the DOM. This function is called once the fiber and its children
* are ready, and the scheduler decides to process it.
*/
complete() {
let component = this.component;
this.isCompleted = true;
const status = component.__owl__.status;
if (status === STATUS.DESTROYED) {
return;
}
// build patchQueue
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function (f) {
f.component.__owl__.currentFiber = null;
patchQueue.push(f);
return f.child;
};
this._walk(doWork);
const patchLen = patchQueue.length;
// call willPatch hook on each fiber of patchQueue
if (status === STATUS.MOUNTED) {
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
}
}
// call __patch on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.target && i === 0) {
let target;
if (fiber.position === "self") {
target = fiber.target;
if ((target as HTMLElement).tagName.toLowerCase() !== fiber.vnode!.sel) {
throw new Error(
`Cannot attach '${component.constructor.name}' to target node (not same tag name)`
);
}
// In self mode, we *know* we are to take possession of the target
// Hence we manually create the corresponding VNode and copy the "key" in data
const selfVnodeData = fiber.vnode!.data ? { key: fiber.vnode!.data.key } : {};
const selfVnode = h(fiber.vnode!.sel, selfVnodeData);
selfVnode.elm = target;
target = selfVnode;
} else {
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
}
component.__patch(target!, fiber.vnode!);
} else {
const vnode = component.__owl__.vnode;
if (fiber.shouldPatch && vnode) {
component.__patch(vnode, fiber.vnode!);
// When updating a Component's props (in directive),
// the component has a pvnode AND should be patched.
// However, its pvnode.elm may have changed if it is a High Order Component
if (component.__owl__.pvnode) {
component.__owl__.pvnode.elm = component.__owl__.vnode!.elm;
}
} else {
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
}
// insert into the DOM (mount case)
let inDOM = false;
if (this.target) {
switch (this.position) {
case "first-child":
this.target.prepend(this.component.el!);
break;
case "last-child":
this.target.appendChild(this.component.el!);
break;
}
inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
if (status === STATUS.MOUNTED || inDOM) {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else {
component.__callMounted();
}
}
} else {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__owl__.status = STATUS.UNMOUNTED;
}
}
}
/**
* Cancel a fiber and all its children.
*/
cancel() {
this._walk((f) => {
if (!f.isRendered) {
f.root.counter--;
}
f.isCompleted = true;
return f.child;
});
}
/**
* 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.
*/
handleError(error: Error) {
let component = this.component;
this.vnode = component.__owl__.vnode || h("div");
const qweb = component.env.qweb;
let root = component;
function handle(error) {
let canCatch = false;
qweb.trigger("error", error);
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
if (canCatch) {
try {
component.catchError!(error);
} catch (e) {
root = component;
component = component.__owl__.parent!;
return handle(e);
}
return true;
}
return false;
}
let isHandled = handle(error);
if (!isHandled) {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
this.root.counter = 0;
this.root.error = error;
scheduler.flush();
// at this point, the state of the application is corrupted and we could
// have a lot of issues or crashes. So we destroy the application in a try
// catch and swallow these errors because the fiber is already in error,
// and this is the actual issue that needs to be solved, not those followup
// errors.
try {
root.destroy();
} catch (e) {}
}
}
}
+185
View File
@@ -0,0 +1,185 @@
import type { BDom } from "../blockdom";
import { mount } from "../blockdom";
import type { ComponentNode } from "./component_node";
import { STATUS } from "./status";
// import { mountBlock } from "./bdom/block";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber;
if (current) {
// current is necessarily a rootfiber here
let root = parent.root;
cancelFibers(root, current.children);
current.children = [];
current.parent = parent;
root.counter++;
current.root = root;
return current;
}
return new Fiber(node, parent);
}
export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
let root = current.root;
root.counter -= cancelFibers(root, current.children);
current.children = [];
root.counter++;
current.bdom = null;
return current;
}
const fiber = new RootFiber(node);
if (node.willPatch.length) {
fiber.willPatch.push(fiber);
}
if (node.patched.length) {
fiber.patched.push(fiber);
}
return fiber;
}
/**
* @returns number of not-yet rendered fibers cancelled
*/
function cancelFibers(root: any, fibers: Fiber[]): number {
let result = 0;
for (let fiber of fibers) {
fiber.node.fiber = null;
fiber.root = root;
if (!fiber.bdom) {
result++;
}
result += cancelFibers(root, fiber.children);
}
return result;
}
export class Fiber {
node: ComponentNode;
bdom: BDom | null = null;
root: RootFiber;
parent: Fiber | null;
children: Fiber[] = [];
appliedToDom = false;
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
node.fiber = this;
this.parent = parent;
if (parent) {
const root = parent.root;
root.counter++;
this.root = root;
parent.children.push(this);
} else {
this.root = this as any;
}
}
}
export class RootFiber extends Fiber {
counter: number = 1;
error: Error | null = null;
resolve: any;
promise: Promise<any>;
reject: any;
// only add stuff in this if they have registered some hooks
willPatch: Fiber[] = [];
patched: Fiber[] = [];
mounted: Fiber[] = [];
constructor(node: ComponentNode) {
super(node, null);
this.counter = 1;
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
complete() {
const node = this.node;
// Step 1: calling all willPatch lifecycle hooks
for (let fiber of this.willPatch) {
// because of the asynchronous nature of the rendering, some parts of the
// UI may have been rendered, then deleted in a followup rendering, and we
// do not want to call onWillPatch in that case.
let node = fiber.node;
if (node.fiber === fiber) {
const component = node.component;
for (let cb of node.willPatch) {
cb.call(component);
}
}
}
// Step 2: patching the dom
node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0);
this.appliedToDom = true;
// Step 3: calling all destroyed hooks
for (let node of __internal__destroyed) {
for (let cb of node.destroyed) {
cb();
}
}
__internal__destroyed.length = 0;
// Step 4: calling all mounted lifecycle hooks
let current;
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
// Step 5: calling all patched hooks
let patchedFibers = this.patched;
while ((current = patchedFibers.pop())) {
if (current.appliedToDom) {
for (let cb of current.node.patched) {
cb();
}
}
}
// unregistering the fiber
node.fiber = null;
}
}
export let __internal__destroyed: ComponentNode[] = [];
export class MountFiber extends RootFiber {
target: HTMLElement;
constructor(node: ComponentNode, target: HTMLElement) {
super(node);
this.target = target;
}
complete() {
const node = this.node;
node.bdom = this.bdom;
mount(node.bdom!, this.target);
node.status = STATUS.MOUNTED;
this.appliedToDom = true;
let current;
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
node.fiber = null;
}
}
+10
View File
@@ -0,0 +1,10 @@
export function mainEventHandler(data: any, ev: Event) {
if (typeof data === "function") {
data(ev);
} else {
const ctx = data[0];
const method = data[1];
const args = data[2] || [];
ctx.__owl__.component[method](...args, ev);
}
}
-115
View File
@@ -1,115 +0,0 @@
import { QWeb } from "../qweb/index";
//------------------------------------------------------------------------------
// Prop validation helper
//------------------------------------------------------------------------------
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
QWeb.utils.validateProps = function (Widget, props: Object) {
const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
const propName = propsDef[i];
if (propName[propName.length - 1] === "?") {
// optional prop
break;
}
if (!(propName in props)) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
}
}
for (let key in props) {
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
}
}
} else if (propsDef) {
// propsDef is an object now
for (let propName in propsDef) {
if (props[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
} else {
continue;
}
}
let isValid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
} catch (e) {
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
throw e;
}
if (!isValid) {
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
}
}
for (let propName in props) {
if (!(propName in propsDef)) {
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
}
}
}
};
/**
* Check if an invidual prop value matches its (static) prop definition
*/
function isValidProp(prop, propDef): boolean {
if (propDef === true) {
return true;
}
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
// slight abuse of language: we want to consider primitive values as well.
//
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
}
// propsDef is an object
if (propDef.optional && prop === undefined) {
return true;
}
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
if (propDef.validate) {
result = result && propDef.validate(prop);
}
if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
}
}
}
}
return result;
}
+35 -69
View File
@@ -1,24 +1,12 @@
import { Fiber } from "./fiber";
import { browser } from "../browser";
import { Fiber, RootFiber } from "./fibers";
import { STATUS } from "./status";
/**
* Owl Scheduler Class
*
* The scheduler is the part of Owl that will effectively apply a rendering
* whenever a fiber is ready.
*
* Briefly, it can be used to register root fibers. Whenever there is an
* active root fiber, it will poll continuously each animation frame (so, about
* once every 16ms) and whenever a root fiber is ready, it will apply it.
*/
interface Task {
fiber: Fiber;
callback: (err?: Error) => void;
}
// -----------------------------------------------------------------------------
// Scheduler
// -----------------------------------------------------------------------------
export class Scheduler {
tasks: Task[] = [];
tasks: Set<RootFiber> = new Set();
isRunning: boolean = false;
requestAnimationFrame: Window["requestAnimationFrame"];
@@ -35,37 +23,10 @@ export class Scheduler {
this.isRunning = false;
}
addFiber(fiber: Fiber): Promise<void> {
// if the fiber was remapped into a larger rendering fiber, it may not be a
// root fiber. But we only want to register root fibers
fiber = fiber.root;
return new Promise((resolve, reject) => {
if (fiber.error) {
return reject(fiber.error);
}
this.tasks.push({
fiber,
callback: () => {
if (fiber.error) {
return reject(fiber.error);
}
resolve();
},
});
if (!this.isRunning) {
this.start();
}
});
}
rejectFiber(fiber: Fiber, reason: string) {
fiber = fiber.root;
const index = this.tasks.findIndex((t) => t.fiber === fiber);
if (index >= 0) {
const [task] = this.tasks.splice(index, 1);
fiber.cancel();
fiber.error = new Error(reason);
task.callback();
addFiber(fiber: Fiber) {
this.tasks.add(fiber.root);
if (!this.isRunning) {
this.start();
}
}
@@ -74,28 +35,35 @@ export class Scheduler {
* Other tasks are left unchanged.
*/
flush() {
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter((task) => {
if (task.fiber.isCompleted) {
task.callback();
return false;
this.tasks.forEach((fiber) => {
if (fiber.root !== fiber) {
// this is wrong! should be something like
// if (this.tasks.has(fiber.root)) {
// // parent rendering has completed
// fiber.resolve();
// this.tasks.delete(fiber);
// }
this.tasks.delete(fiber);
return;
}
if (task.fiber.counter === 0) {
if (!task.fiber.error) {
try {
task.fiber.complete();
} catch (e) {
task.fiber.handleError(e);
}
if (fiber.error) {
this.tasks.delete(fiber);
fiber.reject(fiber.error);
return;
}
if (fiber.node.status === STATUS.DESTROYED) {
this.tasks.delete(fiber);
return;
}
if (fiber.counter === 0) {
if (!fiber.error) {
fiber.complete();
}
task.callback();
return false;
fiber.resolve();
this.tasks.delete(fiber);
}
return true;
});
this.tasks = tasks.concat(this.tasks);
if (this.tasks.length === 0) {
if (this.tasks.size === 0) {
this.stop();
}
}
@@ -109,5 +77,3 @@ export class Scheduler {
});
}
}
export const scheduler = new Scheduler(browser.requestAnimationFrame);
+24
View File
@@ -0,0 +1,24 @@
import type { Component } from "./component";
// -----------------------------------------------------------------------------
// Status
// -----------------------------------------------------------------------------
export const enum STATUS {
NEW,
MOUNTED, // is ready, and in DOM. It has a valid el
DESTROYED,
}
type STATUS_DESCR = "new" | "mounted" | "destroyed";
export function status(component: Component): STATUS_DESCR {
switch (component.__owl__.status) {
case STATUS.NEW:
return "new";
case STATUS.MOUNTED:
return "mounted";
case STATUS.DESTROYED:
return "destroyed";
}
}
-70
View File
@@ -1,70 +0,0 @@
/**
* Owl Style System
*
* This files contains the Owl code related to processing (extended) css strings
* and creating/adding <style> tags to the document head.
*/
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
export function processSheet(str: string): string {
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
const selectorStack: string[][] = [];
const parts: string[] = [];
let rules: string[] = [];
function generateSelector(stackIndex: number, parentSelector?: string) {
const parts: string[] = [];
for (const selector of selectorStack[stackIndex]) {
let part = (parentSelector && parentSelector + " " + selector) || selector;
if (part.includes("&")) {
part = selector.replace(/&/g, parentSelector || "");
}
if (stackIndex < selectorStack.length - 1) {
part = generateSelector(stackIndex + 1, part);
}
parts.push(part);
}
return parts.join(", ");
}
function generateRules() {
if (rules.length) {
parts.push(generateSelector(0) + " {");
parts.push(...rules);
parts.push("}");
rules = [];
}
}
while (tokens.length) {
let token = tokens.shift()!;
if (token === "}") {
generateRules();
selectorStack.pop();
} else {
if (tokens[0] === "{") {
generateRules();
selectorStack.push(token.split(/\s*,\s*/));
tokens.shift();
}
if (tokens[0] === ";") {
rules.push(" " + token + ";");
}
}
}
return parts.join("\n");
}
export function registerSheet(id: string, css: string) {
const sheet = document.createElement("style");
sheet.innerHTML = processSheet(css);
STYLESHEETS[id] = sheet;
}
export function activateSheet(id, name) {
const sheet = STYLESHEETS[id];
if (!sheet) {
throw new Error(
`Invalid css stylesheet for component '${name}'. Did you forget to use the 'css' tag helper?`
);
}
sheet.setAttribute("component", name);
document.head.appendChild(sheet);
}