mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] component/tags: add inline css tag
This add an important feature: defining completely standalone owl components, with the template/style and javascript code together. closes #284
This commit is contained in:
+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,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")) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
+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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe("basic widget properties", () => {
|
|||||||
const parent = new Parent();
|
const parent = new Parent();
|
||||||
await parent.mount(fixture);
|
await parent.mount(fixture);
|
||||||
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
|
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
|
||||||
expect(fixture.innerHTML).toBe( "<div><span></span></div>");
|
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 () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user