mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] tags: reintroduce inline 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 style = 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>.
Original commit in Owl v1: 953778dc5
This commit is contained in:
@@ -6,6 +6,7 @@ import type { ComponentNode } from "./component_node";
|
|||||||
|
|
||||||
export class Component {
|
export class Component {
|
||||||
static template: string = "";
|
static template: string = "";
|
||||||
|
static style: string = "";
|
||||||
|
|
||||||
props: any;
|
props: any;
|
||||||
env: any;
|
env: any;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "./fibers";
|
} from "./fibers";
|
||||||
import { applyDefaultProps } from "./props_validation";
|
import { applyDefaultProps } from "./props_validation";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
|
import { applyStyles } from "./style";
|
||||||
|
|
||||||
export function component(
|
export function component(
|
||||||
name: string | typeof Component,
|
name: string | typeof Component,
|
||||||
@@ -89,6 +90,9 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
|
|||||||
applyDefaultProps(props, C);
|
applyDefaultProps(props, C);
|
||||||
this.component = new C(props, app.env, this) as any;
|
this.component = new C(props, app.env, this) as any;
|
||||||
this.renderFn = app.getTemplate(C.template).bind(null, this.component, this);
|
this.renderFn = app.getTemplate(C.template).bind(null, this.component, this);
|
||||||
|
if (C.style) {
|
||||||
|
applyStyles(C);
|
||||||
|
}
|
||||||
this.component.setup();
|
this.component.setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Component } from "./component";
|
||||||
|
|
||||||
|
export const globalStylesheets: { [key: string]: HTMLStyleElement } = {};
|
||||||
|
|
||||||
|
export function registerSheet(id: string, css: string) {
|
||||||
|
const sheet = document.createElement("style");
|
||||||
|
sheet.innerHTML = processSheet(css);
|
||||||
|
globalStylesheets[id] = sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the stylesheets defined by the component. Note that we need to make
|
||||||
|
* sure all inherited stylesheets are applied as well, in a reverse order to
|
||||||
|
* ensure that <style/> will be applied to the DOM in the order they are
|
||||||
|
* included in the document. We then delete the `style` key from the constructor
|
||||||
|
* to make sure we do not apply it again.
|
||||||
|
*/
|
||||||
|
export function applyStyles(ComponentClass: typeof Component) {
|
||||||
|
const toApply: [string, string][] = [];
|
||||||
|
while (ComponentClass && ComponentClass.style) {
|
||||||
|
if (ComponentClass.hasOwnProperty("style")) {
|
||||||
|
toApply.push([ComponentClass.style, ComponentClass.name]);
|
||||||
|
delete (ComponentClass as any).style;
|
||||||
|
}
|
||||||
|
ComponentClass = Object.getPrototypeOf(ComponentClass);
|
||||||
|
}
|
||||||
|
while (toApply.length) {
|
||||||
|
const [styleId, componentName] = toApply.pop()!;
|
||||||
|
activateSheet(styleId, componentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateSheet(id: string, name: string) {
|
||||||
|
const sheet = globalStylesheets[id];
|
||||||
|
if (!sheet) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid css stylesheet for component '${name}'. Did you forget to use the 'css' tag helper?`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sheet.dataset.component = name;
|
||||||
|
document.head.appendChild(sheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
+1
-1
@@ -54,7 +54,7 @@ export function useComponent(): Component {
|
|||||||
export { status } from "./component/status";
|
export { status } from "./component/status";
|
||||||
export { Portal } from "./misc/portal";
|
export { Portal } from "./misc/portal";
|
||||||
export { Memo } from "./misc/memo";
|
export { Memo } from "./misc/memo";
|
||||||
export { xml } from "./tags";
|
export { css, xml } from "./tags";
|
||||||
export { useState } from "./reactivity";
|
export { useState } from "./reactivity";
|
||||||
export { useRef } from "./refs";
|
export { useRef } from "./refs";
|
||||||
export { EventBus } from "./event_bus";
|
export { EventBus } from "./event_bus";
|
||||||
|
|||||||
+14
@@ -1,3 +1,4 @@
|
|||||||
|
import { registerSheet } from "./component/style";
|
||||||
import { globalTemplates } from "./qweb/template_helpers";
|
import { globalTemplates } from "./qweb/template_helpers";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -12,3 +13,16 @@ export function xml(strings: TemplateStringsArray, ...args: any[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
xml.nextId = 1;
|
xml.nextId = 1;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Global stylesheets
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function css(strings: TemplateStringsArray, ...args: any[]) {
|
||||||
|
const name = `__sheet__${css.nextId++}`;
|
||||||
|
const value = String.raw(strings, ...args);
|
||||||
|
registerSheet(name, value);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
css.nextId = 1;
|
||||||
|
|||||||
+158
-142
@@ -1,159 +1,175 @@
|
|||||||
// import { Component, Env } from "../../src/component/component";
|
import { Component, css, mount, xml } from "../../src";
|
||||||
// import { processSheet } from "../../src/component/styles";
|
import { makeTestFixture } from "../helpers";
|
||||||
// import { xml, css } from "../../src/tags";
|
|
||||||
// import { makeTestFixture, makeTestEnv } from "../helpers";
|
|
||||||
|
|
||||||
// //------------------------------------------------------------------------------
|
let fixture: HTMLElement;
|
||||||
// // Setup and helpers
|
beforeEach(() => {
|
||||||
// //------------------------------------------------------------------------------
|
fixture = makeTestFixture();
|
||||||
|
document.head.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
// // We create before each test:
|
describe("styles and component", () => {
|
||||||
// // - 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.skip("styles and component", () => {
|
|
||||||
test("can define an inline stylesheet", async () => {
|
test("can define an inline stylesheet", async () => {
|
||||||
// class App extends Component {
|
class App extends Component {
|
||||||
// static template = xml`<div class="app">text</div>`;
|
static template = xml`<div class="app">text</div>`;
|
||||||
// static style = css`
|
static style = css`
|
||||||
// .app {
|
.app {
|
||||||
// color: red;
|
color: red;
|
||||||
// }
|
}
|
||||||
// `;
|
`;
|
||||||
// }
|
}
|
||||||
// expect(document.head.innerHTML).toBe("");
|
expect(document.head.innerHTML).toBe("");
|
||||||
// const app = new App();
|
const app = await mount(App, fixture);
|
||||||
// expect(document.head.innerHTML).toBe(`<style component=\"App\">.app {
|
expect(document.head.innerHTML).toBe(`<style data-component=\"App\">.app {
|
||||||
// color: red;
|
color: red;
|
||||||
// }</style>`);
|
}</style>`);
|
||||||
// await app.mount(fixture);
|
const style = getComputedStyle(app.el as HTMLElement);
|
||||||
// const style = getComputedStyle(app.el!);
|
expect(style.color).toBe("red");
|
||||||
// expect(style.color).toBe("red");
|
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
|
||||||
// expect(fixture.innerHTML).toBe('<div class="app">text</div>');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("inherited components properly apply css", async () => {
|
test("inherited components properly apply css", async () => {
|
||||||
// class App extends Component {
|
class App extends Component {
|
||||||
// static template = xml`<div class="app">text</div>`;
|
static template = xml`<div class="app">text</div>`;
|
||||||
// static style = css`
|
static style = css`
|
||||||
// .app {
|
.app {
|
||||||
// color: red;
|
color: red;
|
||||||
// }
|
}
|
||||||
// `;
|
`;
|
||||||
// }
|
}
|
||||||
// class SubApp extends App {
|
class SubApp extends App {
|
||||||
// static style = css`
|
static style = css`
|
||||||
// .app {
|
.app {
|
||||||
// font-weight: bold;
|
font-weight: bold;
|
||||||
// }
|
}
|
||||||
// `;
|
`;
|
||||||
// }
|
}
|
||||||
// expect(document.head.innerHTML).toBe("");
|
expect(document.head.innerHTML).toBe("");
|
||||||
// const app = new SubApp();
|
const app = await mount(SubApp, fixture);
|
||||||
// expect(document.head.innerHTML).toBe(`<style component=\"SubApp\">.app {
|
expect(document.head.innerHTML).toBe(`<style data-component=\"App\">.app {
|
||||||
// font-weight: bold;
|
color: red;
|
||||||
// }</style><style component=\"App\">.app {
|
}</style><style data-component=\"SubApp\">.app {
|
||||||
// color: red;
|
font-weight: bold;
|
||||||
// }</style>`);
|
}</style>`);
|
||||||
// await app.mount(fixture);
|
const style = getComputedStyle(app.el as HTMLElement);
|
||||||
// const style = getComputedStyle(app.el!);
|
expect(style.color).toBe("red");
|
||||||
// expect(style.color).toBe("red");
|
expect(style.fontWeight).toBe("bold");
|
||||||
// expect(style.fontWeight).toBe("bold");
|
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
|
||||||
// expect(fixture.innerHTML).toBe('<div class="app">text</div>');
|
});
|
||||||
|
|
||||||
|
test("inherited components properly apply css, part 2", async () => {
|
||||||
|
class App extends Component {
|
||||||
|
static template = xml`<div class="app"/>`;
|
||||||
|
static style = css`
|
||||||
|
.app {
|
||||||
|
color: tomato;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
class BetterApp extends App {}
|
||||||
|
class EvenBetterApp extends BetterApp {
|
||||||
|
static style = css`
|
||||||
|
.app {
|
||||||
|
background-color: papayawhip;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
expect(document.head.innerHTML).toBe("");
|
||||||
|
await mount(EvenBetterApp, fixture);
|
||||||
|
expect(document.head.innerHTML).toBe(`<style data-component=\"App\">.app {
|
||||||
|
color: tomato;
|
||||||
|
}</style><style data-component=\"EvenBetterApp\">.app {
|
||||||
|
background-color: papayawhip;
|
||||||
|
}</style>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("get a meaningful error message if css helper is missing", async () => {
|
test("get a meaningful error message if css helper is missing", async () => {
|
||||||
// class App extends Component {
|
class App extends Component {
|
||||||
// static template = xml`<div class="app">text</div>`;
|
static template = xml`<div class="app">text</div>`;
|
||||||
// static style = `.app {color: red;}`;
|
static style = `.app {color: red;}`;
|
||||||
// }
|
}
|
||||||
// let error;
|
let error;
|
||||||
// try {
|
try {
|
||||||
// new App();
|
await mount(App, fixture);
|
||||||
// } catch (e) {
|
} catch (e) {
|
||||||
// error = e;
|
error = e;
|
||||||
// }
|
}
|
||||||
// expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
// expect(error.message).toBe(
|
expect(error.message).toBe(
|
||||||
// "Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?"
|
"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 {
|
test("inline stylesheets are processed", async () => {
|
||||||
// static template = xml`<div class="app">text</div>`;
|
class App extends Component {
|
||||||
// static style = css`
|
static template = xml`<div class="app">text</div>`;
|
||||||
// .app {
|
static style = css`
|
||||||
// color: red;
|
.app {
|
||||||
// .some-class {
|
color: red;
|
||||||
// font-weight: bold;
|
.some-class {
|
||||||
// width: 40px;
|
font-weight: bold;
|
||||||
// }
|
width: 40px;
|
||||||
// display: block;
|
}
|
||||||
// }
|
display: block;
|
||||||
// `;
|
}
|
||||||
// }
|
`;
|
||||||
// new App();
|
}
|
||||||
// expect(document.head.querySelector("style")!.innerHTML).toBe(`.app {
|
await mount(App, fixture);
|
||||||
// color: red;
|
expect(document.head.querySelector("style")!.innerHTML).toBe(`.app {
|
||||||
// }
|
color: red;
|
||||||
// .app .some-class {
|
}
|
||||||
// font-weight: bold;
|
.app .some-class {
|
||||||
// width: 40px;
|
font-weight: bold;
|
||||||
// }
|
width: 40px;
|
||||||
// .app {
|
}
|
||||||
// display: block;
|
.app {
|
||||||
// }`);
|
display: block;
|
||||||
|
}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("properly handle rules with commas", async () => {
|
test("properly handle rules with commas", async () => {
|
||||||
// const sheet = processSheet(`.parent-a, .parent-b {
|
class App extends Component {
|
||||||
// .child-a, .child-b {
|
static template = xml`<div/>`;
|
||||||
// color: red;
|
static style = css`
|
||||||
// }
|
.parent-a,
|
||||||
// }`);
|
.parent-b {
|
||||||
// expect(sheet)
|
.child-a,
|
||||||
// .toBe(`.parent-a .child-a, .parent-a .child-b, .parent-b .child-a, .parent-b .child-b {
|
.child-b {
|
||||||
// color: red;
|
color: red;
|
||||||
// }`);
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
await mount(App, fixture);
|
||||||
|
expect(document.head.querySelector("style")!.innerHTML)
|
||||||
|
.toBe(`.parent-a .child-a, .parent-a .child-b, .parent-b .child-a, .parent-b .child-b {
|
||||||
|
color: red;
|
||||||
|
}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handle & selector", async () => {
|
test("handle & selector", async () => {
|
||||||
// let sheet = processSheet(`.btn {
|
class App extends Component {
|
||||||
// &.danger {
|
static template = xml`<div/>`;
|
||||||
// color: red;
|
static style = css`
|
||||||
// }
|
.btn {
|
||||||
// }`);
|
&.danger {
|
||||||
// expect(sheet).toBe(`.btn.danger {
|
color: red;
|
||||||
// color: red;
|
}
|
||||||
// }`);
|
}
|
||||||
// sheet = processSheet(`.some-class {
|
.some-class {
|
||||||
// &.btn {
|
&.btn {
|
||||||
// .other-class ~ & {
|
.other-class ~ & {
|
||||||
// color: red;
|
color: red;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }`);
|
}
|
||||||
// expect(sheet).toBe(`.other-class ~ .some-class.btn {
|
`;
|
||||||
// color: red;
|
}
|
||||||
// }`);
|
await mount(App, fixture);
|
||||||
|
expect(document.head.querySelector("style")!.innerHTML).toBe(`.btn.danger {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.other-class ~ .some-class.btn {
|
||||||
|
color: red;
|
||||||
|
}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user