[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:
Bruno Boi
2021-11-03 10:00:15 +01:00
committed by Aaron Bohy
parent d3745e4e5f
commit 348b505e5f
6 changed files with 266 additions and 143 deletions
+1
View File
@@ -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;
+4
View File
@@ -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();
} }
+88
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}`);
}); });
}); });