mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b63cd4c9d9 | |||
| 953778dc50 | |||
| 4f61d9f1e0 | |||
| bc2c7edff4 | |||
| c8d9c0b50e | |||
| 15b25fd838 |
@@ -102,10 +102,16 @@ Submit a PR!
|
|||||||
|
|
||||||
## Installing/Building
|
## Installing/Building
|
||||||
|
|
||||||
|
Owl can be installed with the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install @odoo/owl
|
||||||
|
```
|
||||||
|
|
||||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||||
|
|
||||||
- [owl-1.0.0-beta4.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta4/owl.js)
|
- [owl-1.0.0-beta5.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta5/owl.js)
|
||||||
- [owl-1.0.0-beta4.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta4/owl.min.js)
|
- [owl-1.0.0-beta5.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta5/owl.min.js)
|
||||||
|
|
||||||
Some npm scripts are available:
|
Some npm scripts are available:
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -56,15 +56,15 @@ useState Link
|
|||||||
config RouteComponent
|
config RouteComponent
|
||||||
mode Router
|
mode Router
|
||||||
core tags
|
core tags
|
||||||
EventBus xml
|
EventBus css
|
||||||
Observer utils
|
Observer xml
|
||||||
hooks debounce
|
hooks utils
|
||||||
onWillStart escape
|
onWillStart debounce
|
||||||
onMounted loadJS
|
onMounted escape
|
||||||
onWillUpdateProps loadFile
|
onWillUpdateProps loadJS
|
||||||
onWillPatch shallowEqual
|
onWillPatch loadFile
|
||||||
onPatched whenReady
|
onPatched shallowEqual
|
||||||
onWillUnmount
|
onWillUnmount whenReady
|
||||||
useContext
|
useContext
|
||||||
useState
|
useState
|
||||||
useRef
|
useRef
|
||||||
|
|||||||
@@ -225,6 +225,10 @@ to be called in the constructor.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **`style`** (string, optional): it should be the return value of the [`css tag](tags.md#css-tag),
|
||||||
|
which is used to inject stylesheet whenever the component is visible on the
|
||||||
|
screen.
|
||||||
|
|
||||||
There is another static property defined on the `Component` class: `current`.
|
There is another static property defined on the `Component` class: `current`.
|
||||||
This property is set to the currently being defined component (in the constructor).
|
This property is set to the currently being defined component (in the constructor).
|
||||||
This is the way [hooks](hooks.md) are able to get a reference to the target
|
This is the way [hooks](hooks.md) are able to get a reference to the target
|
||||||
|
|||||||
+88
-4
@@ -4,16 +4,19 @@
|
|||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [`xml` tag](#xml-tag)
|
- [`xml` tag](#xml-tag)
|
||||||
|
- [`css` tag](#css-tag)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Tags are very small helpers to make it easy to write inline templates. There is
|
Tags are very small helpers intended to make it easy to write inline templates
|
||||||
only one currently available tag: `xml`, but we plan to add other tags later,
|
or styles. There are currently two tags: `css` and `xml`. With these functions,
|
||||||
such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
|
it is possible to write [single file components](../tooling.md#single-file-component).
|
||||||
|
|
||||||
## XML tag
|
## XML tag
|
||||||
|
|
||||||
Without tags, creating a standalone component would look like this:
|
The `xml` tag is certainly the most useful tag. It is used to define an inline
|
||||||
|
QWeb template for a component. Without tags, creating a standalone component
|
||||||
|
would look like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { Component } from 'owl'
|
import { Component } from 'owl'
|
||||||
@@ -52,3 +55,84 @@ class MyComponent extends Component {
|
|||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## CSS tag
|
||||||
|
|
||||||
|
The CSS tag is useful to define a css stylesheet in the javascript file:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div class="my-component">some template</div>
|
||||||
|
`;
|
||||||
|
static css`
|
||||||
|
.my-component {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `css` tag registers internally the css information. Then, whenever the first
|
||||||
|
instance of the component is created, will add a `<style>` tag to the document
|
||||||
|
`<head>`.
|
||||||
|
|
||||||
|
Note that to make it more useful, like other css preprocessors, the `css` tag
|
||||||
|
accepts a small extension of the css specification: css scopes can be nested,
|
||||||
|
and the rules will then be expanded by the `css` helper:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.my-component {
|
||||||
|
display: block;
|
||||||
|
.sub-component h {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
will be formatted as:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.my-component {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.my-component .sub-component h {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, there is no additional processing done by the `css` tag. However, since it
|
||||||
|
is done in javascript at runtime, we actually have more power. For example:
|
||||||
|
|
||||||
|
1. sharing values between javascript and css:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { theme } from "./theme";
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`<div class="my-component">...</div>`;
|
||||||
|
static style = css`
|
||||||
|
.my-component {
|
||||||
|
color: ${theme.MAIN_COLOR};
|
||||||
|
background-color: ${theme.SECONDARY_color};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. scoping rules to the current component:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { generateUUID } from "./utils";
|
||||||
|
|
||||||
|
const uuid = generateUUID();
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`<div data-o-${uuid}="">...</div>`;
|
||||||
|
static style = css`
|
||||||
|
[data-o-${uuid}] {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
+20
-9
@@ -43,32 +43,43 @@ useful to compare various performance metrics on some tasks.
|
|||||||
It is very useful to group code by feature instead of by type of file. It makes
|
It is very useful to group code by feature instead of by type of file. It makes
|
||||||
it easier to scale application to larger size.
|
it easier to scale application to larger size.
|
||||||
|
|
||||||
To do so, Owl currently has a small helper that makes it easy to define a
|
To do so, Owl has two small helpers that make it easy to define a
|
||||||
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
|
template or a stylesheet inside a javascript (or typescript) file: the
|
||||||
helper. With this, a template is automatically registered to [QWeb](reference/qweb_engine.md).
|
[`xml`](reference/tags.md#xml-tag) and [`css`](reference/tags.md#css-tag)
|
||||||
|
helper.
|
||||||
|
|
||||||
This means that the template and the javascript code can be defined in the same
|
This means that the template, the style and the javascript code can be defined in
|
||||||
file. It is not currently possible to add css to the same file, but Owl may
|
the same file. For example:
|
||||||
get a `css` tag helper later.
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { Component } = owl;
|
const { Component } = owl;
|
||||||
const { xml } = owl.tags;
|
const { xml, css } = owl.tags;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// TEMPLATE
|
// TEMPLATE
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
const TEMPLATE = xml/* xml */ `
|
const TEMPLATE = xml/* xml */ `
|
||||||
<div class="main two-columns">
|
<div class="main">
|
||||||
<Sidebar/>
|
<Sidebar/>
|
||||||
<Content />
|
<Content />
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// STYLE
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const STYLE = css/* css */ `
|
||||||
|
.main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// CODE
|
// CODE
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
class MyComponent extends Component {
|
class Main extends Component {
|
||||||
static template = TEMPLATE;
|
static template = TEMPLATE;
|
||||||
|
static style = STYLE;
|
||||||
static components = { Sidebar, Content };
|
static components = { Sidebar, Content };
|
||||||
|
|
||||||
// rest of component...
|
// rest of component...
|
||||||
|
|||||||
+6
-3
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "owl-framework",
|
"name": "@odoo/owl",
|
||||||
"version": "1.0.0-beta4",
|
"version": "1.0.0-beta5",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "src/index.ts",
|
"main": "dist/owl/index.js",
|
||||||
|
"types": "dist/owl/index.d.ts",
|
||||||
|
"prepublish": "npm run build",
|
||||||
|
"files": ["dist/owl/"],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.15.3"
|
"node": ">=10.15.3"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# 🦉 OWL Roadmap 🦉
|
# 🦉 OWL Roadmap 🦉
|
||||||
|
|
||||||
- Current version: 1.0.0-beta4
|
- Current version: 1.0.0-beta5
|
||||||
- Status: mostly stable
|
- Status: mostly stable
|
||||||
|
|
||||||
This roadmap is only an attempt at predicting Owl's future. Everything may
|
This roadmap is only an attempt at predicting Owl's future. Everything may
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "./directive";
|
|||||||
import { Fiber } from "./fiber";
|
import { Fiber } from "./fiber";
|
||||||
import "./props_validation";
|
import "./props_validation";
|
||||||
import { Scheduler, scheduler } from "./scheduler";
|
import { Scheduler, scheduler } from "./scheduler";
|
||||||
|
import { activateSheet } from "./styles";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl Component System
|
* Owl Component System
|
||||||
@@ -199,6 +200,9 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
refs: null,
|
refs: null,
|
||||||
scope: null
|
scope: null
|
||||||
};
|
};
|
||||||
|
if (constr.style) {
|
||||||
|
this.__applyStyles(constr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -580,6 +584,20 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
return fiber;
|
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 {
|
__getTemplate(qweb: QWeb): string {
|
||||||
let p = (<any>this).constructor;
|
let p = (<any>this).constructor;
|
||||||
if (!p.hasOwnProperty("_template")) {
|
if (!p.hasOwnProperty("_template")) {
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ QWeb.addDirective({
|
|||||||
} else if (!name.startsWith("t-")) {
|
} else if (!name.startsWith("t-")) {
|
||||||
if (name !== "class" && name !== "style") {
|
if (name !== "class" && name !== "style") {
|
||||||
// this is a prop!
|
// this is a prop!
|
||||||
props[name] = ctx.formatExpression(value);
|
props[name] = ctx.formatExpression(value) || "undefined";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 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 } = {};
|
||||||
|
|
||||||
|
function processSheet(str: string): string {
|
||||||
|
const tokens = str.split(/(\{|\}|;)/).map(s => s.trim());
|
||||||
|
const selectorStack: string[] = [];
|
||||||
|
const parts: string[] = [];
|
||||||
|
let rules: string[] = [];
|
||||||
|
function generateRules() {
|
||||||
|
if (rules.length) {
|
||||||
|
parts.push(selectorStack.join(" ") + " {");
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
+1
-1
@@ -132,7 +132,7 @@ export function useStore(selector, options: SelectorOptions = {}): any {
|
|||||||
__destroy.call(component, parent);
|
__destroy.call(component, parent);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof result !== "object") {
|
if (typeof result !== "object" || result === null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return new Proxy(result, {
|
return new Proxy(result, {
|
||||||
|
|||||||
+17
@@ -1,4 +1,5 @@
|
|||||||
import { QWeb } from "./qweb/index";
|
import { QWeb } from "./qweb/index";
|
||||||
|
import { registerSheet } from "./component/styles";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl Tags
|
* Owl Tags
|
||||||
@@ -25,3 +26,19 @@ export function xml(strings, ...args) {
|
|||||||
QWeb.registerTemplate(name, value);
|
QWeb.registerTemplate(name, value);
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
||||||
|
* an inline stylesheet with just the following code:
|
||||||
|
* ```js
|
||||||
|
* class A extends Component {
|
||||||
|
* static style = css`.component-a { color: red; }`;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function css(strings, ...args) {
|
||||||
|
const name = `__sheet__${QWeb.nextId++}`;
|
||||||
|
const value = String.raw(strings, ...args);
|
||||||
|
registerSheet(name, value);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,43 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`basic widget properties can handle empty props 1`] = `
|
||||||
|
"function anonymous(context, extra
|
||||||
|
) {
|
||||||
|
// Template name: \\"__template__2\\"
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let scope = Object.create(context);
|
||||||
|
let h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
let vn1 = h('div', p1, c1);
|
||||||
|
// Component 'Child'
|
||||||
|
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||||
|
let props2 = {val:undefined};
|
||||||
|
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||||
|
w2.destroy();
|
||||||
|
w2 = false;
|
||||||
|
}
|
||||||
|
if (w2) {
|
||||||
|
w2.__updateProps(props2, extra.fiber, undefined);
|
||||||
|
let pvnode = w2.__owl__.pvnode;
|
||||||
|
c1.push(pvnode);
|
||||||
|
} else {
|
||||||
|
let componentKey2 = \`Child\`;
|
||||||
|
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
|
||||||
|
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||||
|
w2 = new W2(parent, props2);
|
||||||
|
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||||
|
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||||
|
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
|
||||||
|
c1.push(pvnode);
|
||||||
|
w2.__owl__.pvnode = pvnode;
|
||||||
|
}
|
||||||
|
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`basic widget properties reconciliation alg works for t-foreach in t-foreach 1`] = `
|
exports[`basic widget properties reconciliation alg works for t-foreach in t-foreach 1`] = `
|
||||||
"function anonymous(context, extra
|
"function anonymous(context, extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -147,6 +147,21 @@ describe("basic widget properties", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
|
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can handle empty props", async () => {
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child val=""/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span></span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
test("cannot be clicked on and updated if not in DOM", async () => {
|
test("cannot be clicked on and updated if not in DOM", async () => {
|
||||||
class Counter extends Component<any, any> {
|
class Counter extends Component<any, any> {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ let env: Env;
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = makeTestFixture();
|
fixture = makeTestFixture();
|
||||||
env = makeTestEnv();
|
env = makeTestEnv();
|
||||||
env.qweb.addTemplate("Component<any,any>", "<div></div>");
|
|
||||||
env.qweb.addTemplate(
|
|
||||||
"Counter",
|
|
||||||
`<div><t t-esc="state.counter"/><button t-on-click="inc">Inc</button></div>`
|
|
||||||
);
|
|
||||||
env.qweb.addTemplate("WidgetA", `<div>Hello<t t-component="b"/></div>`);
|
|
||||||
env.qweb.addTemplate("WidgetB", `<div>world</div>`);
|
|
||||||
Component.env = env;
|
Component.env = env;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { Component, Env } from "../../src/component/component";
|
||||||
|
import { xml, css } from "../../src/tags";
|
||||||
|
import { makeTestFixture, makeTestEnv } from "../helpers";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// We create before each test:
|
||||||
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||||
|
// manipulations. Note that it is removed after each test.
|
||||||
|
// - env: an Env, necessary to create new components
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = makeTestEnv();
|
||||||
|
Component.env = env;
|
||||||
|
document.head.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("styles and component", () => {
|
||||||
|
test("can define an inline stylesheet", async () => {
|
||||||
|
class App extends Component<any, any> {
|
||||||
|
static template = xml`<div class="app">text</div>`;
|
||||||
|
static style = css`
|
||||||
|
.app {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
expect(document.head.innerHTML).toBe("");
|
||||||
|
const app = new App();
|
||||||
|
|
||||||
|
expect(document.head.innerHTML).toBe(`<style component=\"App\">.app {
|
||||||
|
color: red;
|
||||||
|
}</style>`);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
const style = getComputedStyle(app.el!);
|
||||||
|
expect(style.color).toBe("red");
|
||||||
|
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("inherited components properly apply css", async () => {
|
||||||
|
class App extends Component<any, any> {
|
||||||
|
static template = xml`<div class="app">text</div>`;
|
||||||
|
static style = css`
|
||||||
|
.app {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
class SubApp extends App {
|
||||||
|
static style = css`
|
||||||
|
.app {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
expect(document.head.innerHTML).toBe("");
|
||||||
|
const app = new SubApp();
|
||||||
|
|
||||||
|
expect(document.head.innerHTML).toBe(`<style component=\"SubApp\">.app {
|
||||||
|
font-weight: bold;
|
||||||
|
}</style><style component=\"App\">.app {
|
||||||
|
color: red;
|
||||||
|
}</style>`);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
const style = getComputedStyle(app.el!);
|
||||||
|
expect(style.color).toBe("red");
|
||||||
|
expect(style.fontWeight).toBe("bold");
|
||||||
|
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get a meaningful error message if css helper is missing", async () => {
|
||||||
|
class App extends Component<any, any> {
|
||||||
|
static template = xml`<div class="app">text</div>`;
|
||||||
|
static style = `.app {color: red;}`;
|
||||||
|
}
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
new App();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(
|
||||||
|
"Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("inline stylesheets are processed", async () => {
|
||||||
|
class App extends Component<any, any> {
|
||||||
|
static template = xml`<div class="app">text</div>`;
|
||||||
|
static style = css`
|
||||||
|
.app {
|
||||||
|
color: red;
|
||||||
|
.some-class {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
new App();
|
||||||
|
|
||||||
|
expect(document.head.querySelector("style")!.innerHTML).toBe(`.app {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.app .some-class {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
.app {
|
||||||
|
display: block;
|
||||||
|
}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -49,10 +49,13 @@ describe("connecting a component to store", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("useStore can observe primitive types and call onUpdate", async () => {
|
test("useStore can observe primitive types and call onUpdate", async () => {
|
||||||
const state = { isBoolean: false };
|
const state = { isBoolean: false, nullValue: null };
|
||||||
const actions = {
|
const actions = {
|
||||||
setTrue({ state }) {
|
setTrue({ state }) {
|
||||||
state.isBoolean = true;
|
state.isBoolean = true;
|
||||||
|
},
|
||||||
|
setNotNull({ state }) {
|
||||||
|
state.nullValue = "ok";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, actions });
|
const store = new Store({ state, actions });
|
||||||
@@ -61,8 +64,10 @@ describe("connecting a component to store", () => {
|
|||||||
static template = xml`
|
static template = xml`
|
||||||
<div>
|
<div>
|
||||||
<span t-if="isBoolean">ok</span>
|
<span t-if="isBoolean">ok</span>
|
||||||
|
<span t-if="nullValue !== null">not null</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
isBoolean: boolean;
|
isBoolean: boolean;
|
||||||
|
nullValue: string;
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.isBoolean = useStore(state => state.isBoolean, {
|
this.isBoolean = useStore(state => state.isBoolean, {
|
||||||
@@ -70,6 +75,11 @@ describe("connecting a component to store", () => {
|
|||||||
this.isBoolean = isBoolean;
|
this.isBoolean = isBoolean;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.nullValue = useStore(state => state.nullValue, {
|
||||||
|
onUpdate: nullValue => {
|
||||||
|
this.nullValue = nullValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +92,10 @@ describe("connecting a component to store", () => {
|
|||||||
store.dispatch("setTrue");
|
store.dispatch("setTrue");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>");
|
||||||
|
|
||||||
|
store.dispatch("setNotNull");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>ok</span><span>not null</span></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("map works on the result of useStore when the resulting array changes for a bigger one", async () => {
|
test("map works on the result of useStore when the resulting array changes for a bigger one", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user