mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] add browser bindings to standard environment
This could be done by each application, but it does cost only a few lines of code, and it helps standardizing the Owl ecosystem. For example, some library (such as o_spreadsheet) needs to mock side effects, and Odoo also needs to do that, so this prevents duplicated effort. closes #686
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
- [Setting an Environment](#setting-an-environment)
|
- [Setting an Environment](#setting-an-environment)
|
||||||
- [Using a sub environment](#using-a-sub-environment)
|
- [Using a sub environment](#using-a-sub-environment)
|
||||||
- [Content of an Environment](#content-of-an-environment)
|
- [Content of an Environment](#content-of-an-environment)
|
||||||
|
- [Special keys](#special-keys)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ Some good use cases for additional keys in the environment are:
|
|||||||
|
|
||||||
- some configuration keys,
|
- some configuration keys,
|
||||||
- session information,
|
- session information,
|
||||||
- generic services (such as doing rpcs, or accessing local storage).
|
- generic services (such as doing rpcs).
|
||||||
|
|
||||||
Doing it this way means that components are easily testable: we can simply
|
Doing it this way means that components are easily testable: we can simply
|
||||||
create a test environment with mock services.
|
create a test environment with mock services.
|
||||||
@@ -125,3 +126,26 @@ async function start() {
|
|||||||
await app.mount(document.body);
|
await app.mount(document.body);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Special Keys
|
||||||
|
|
||||||
|
There are two special key/value added by Owl if not provided in the environment:
|
||||||
|
the `QWeb` instance and a `browser` object:
|
||||||
|
|
||||||
|
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
|
||||||
|
for Owl to be able to render anything
|
||||||
|
- `browser`: this is an object that contains some common access points to the
|
||||||
|
browser methods with a side effect. This is particularly useful when one want
|
||||||
|
to test more advanced components, and be able to mock those methods.
|
||||||
|
|
||||||
|
More specifically, the `browser` object contains the following methods and objects:
|
||||||
|
|
||||||
|
- `setTimeout`
|
||||||
|
- `clearTimeout`
|
||||||
|
- `setInterval`
|
||||||
|
- `clearInterval`
|
||||||
|
- `requestAnimationFrame`
|
||||||
|
- `random`
|
||||||
|
- `Date`
|
||||||
|
- `fetch`
|
||||||
|
- `localStorage`
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export interface Browser {
|
||||||
|
setTimeout: Window["setTimeout"];
|
||||||
|
clearTimeout: Window["clearTimeout"];
|
||||||
|
setInterval: Window["setInterval"];
|
||||||
|
clearInterval: Window["clearInterval"];
|
||||||
|
requestAnimationFrame: Window["requestAnimationFrame"];
|
||||||
|
random: Math["random"];
|
||||||
|
Date: typeof Date;
|
||||||
|
fetch: Window["fetch"];
|
||||||
|
localStorage: Window["localStorage"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const browser: Browser = {
|
||||||
|
setTimeout: window.setTimeout.bind(window),
|
||||||
|
clearTimeout: window.clearTimeout.bind(window),
|
||||||
|
setInterval: window.setInterval.bind(window),
|
||||||
|
clearInterval: window.clearInterval.bind(window),
|
||||||
|
requestAnimationFrame: window.requestAnimationFrame.bind(window),
|
||||||
|
random: Math.random,
|
||||||
|
Date: window.Date,
|
||||||
|
fetch: (window.fetch || (() => {})).bind(window),
|
||||||
|
localStorage: window.localStorage
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { Fiber } from "./fiber";
|
|||||||
import "./props_validation";
|
import "./props_validation";
|
||||||
import { Scheduler, scheduler } from "./scheduler";
|
import { Scheduler, scheduler } from "./scheduler";
|
||||||
import { activateSheet } from "./styles";
|
import { activateSheet } from "./styles";
|
||||||
|
import { Browser, browser } from "../browser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl Component System
|
* Owl Component System
|
||||||
@@ -34,6 +35,7 @@ import { activateSheet } from "./styles";
|
|||||||
*/
|
*/
|
||||||
export interface Env {
|
export interface Env {
|
||||||
qweb: QWeb;
|
qweb: QWeb;
|
||||||
|
browser: Browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MountPosition = "first-child" | "last-child" | "self";
|
export type MountPosition = "first-child" | "last-child" | "self";
|
||||||
@@ -157,6 +159,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
|
|||||||
if (!this.env.qweb) {
|
if (!this.env.qweb) {
|
||||||
this.env.qweb = new QWeb();
|
this.env.qweb = new QWeb();
|
||||||
}
|
}
|
||||||
|
if (!this.env.browser) {
|
||||||
|
this.env.browser = browser;
|
||||||
|
}
|
||||||
this.env.qweb.on("update", this, () => {
|
this.env.qweb.on("update", this, () => {
|
||||||
if (this.__owl__.isMounted) {
|
if (this.__owl__.isMounted) {
|
||||||
this.render(true);
|
this.render(true);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Fiber } from "./fiber";
|
import { Fiber } from "./fiber";
|
||||||
|
import { browser } from "../browser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl Scheduler Class
|
* Owl Scheduler Class
|
||||||
@@ -19,9 +20,9 @@ interface Task {
|
|||||||
export class Scheduler {
|
export class Scheduler {
|
||||||
tasks: Task[] = [];
|
tasks: Task[] = [];
|
||||||
isRunning: boolean = false;
|
isRunning: boolean = false;
|
||||||
requestAnimationFrame: typeof window.requestAnimationFrame;
|
requestAnimationFrame: Window["requestAnimationFrame"];
|
||||||
|
|
||||||
constructor(requestAnimationFrame) {
|
constructor(requestAnimationFrame: Window["requestAnimationFrame"]) {
|
||||||
this.requestAnimationFrame = requestAnimationFrame;
|
this.requestAnimationFrame = requestAnimationFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,5 +110,4 @@ export class Scheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const raf = window.requestAnimationFrame.bind(window);
|
export const scheduler = new Scheduler(browser.requestAnimationFrame);
|
||||||
export const scheduler = new Scheduler(raf);
|
|
||||||
|
|||||||
+5
-3
@@ -10,6 +10,8 @@
|
|||||||
* - debounce
|
* - debounce
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { browser } from "./browser";
|
||||||
|
|
||||||
export function whenReady(fn?: any) {
|
export function whenReady(fn?: any) {
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
if (document.readyState !== "loading") {
|
if (document.readyState !== "loading") {
|
||||||
@@ -44,7 +46,7 @@ export function loadJS(url: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadFile(url: string): Promise<string> {
|
export async function loadFile(url: string): Promise<string> {
|
||||||
const result = await fetch(url);
|
const result = await browser.fetch(url);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new Error("Error while fetching xml templates");
|
throw new Error("Error while fetching xml templates");
|
||||||
}
|
}
|
||||||
@@ -83,8 +85,8 @@ export function debounce(func: Function, wait: number, immediate?: boolean): Fun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const callNow = immediate && !timeout;
|
const callNow = immediate && !timeout;
|
||||||
clearTimeout(timeout);
|
browser.clearTimeout(timeout);
|
||||||
timeout = setTimeout(later, wait);
|
timeout = browser.setTimeout(later, wait);
|
||||||
if (callNow) {
|
if (callNow) {
|
||||||
func.apply(context, args);
|
func.apply(context, args);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,7 @@ import { patch } from "../src/vdom";
|
|||||||
import "../src/qweb/base_directives";
|
import "../src/qweb/base_directives";
|
||||||
import "../src/qweb/extensions";
|
import "../src/qweb/extensions";
|
||||||
import "../src/component/directive";
|
import "../src/component/directive";
|
||||||
|
import { browser } from "../src/browser";
|
||||||
|
|
||||||
// modifies scheduler to make it faster to test components
|
// modifies scheduler to make it faster to test components
|
||||||
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
|
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
|
||||||
@@ -75,7 +76,8 @@ export function makeDeferred(): Deferred {
|
|||||||
|
|
||||||
export function makeTestEnv(): Env {
|
export function makeTestEnv(): Env {
|
||||||
return {
|
return {
|
||||||
qweb: new QWeb()
|
qweb: new QWeb(),
|
||||||
|
browser: browser
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { escape, debounce } from "../src/utils";
|
import { escape, debounce } from "../src/utils";
|
||||||
|
import { browser } from "../src/browser";
|
||||||
|
|
||||||
describe("escape", () => {
|
describe("escape", () => {
|
||||||
test("normal strings", () => {
|
test("normal strings", () => {
|
||||||
@@ -15,6 +16,10 @@ describe("escape", () => {
|
|||||||
describe("debounce", () => {
|
describe("debounce", () => {
|
||||||
test("works as expected", () => {
|
test("works as expected", () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
// need to reset them on browser because they were mocked in window, but not
|
||||||
|
// on browser object
|
||||||
|
browser.setTimeout = window.setTimeout.bind(window);
|
||||||
|
browser.clearTimeout = window.clearTimeout.bind(window);
|
||||||
let n = 0;
|
let n = 0;
|
||||||
let f = debounce(() => n++, 100);
|
let f = debounce(() => n++, 100);
|
||||||
expect(n).toBe(0);
|
expect(n).toBe(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user