Skip to content

Commit 6ec5b99

Browse files
nicholasriceEisenbergEffectjanechu
authored
add CSS partial tagged template literal (#4711)
* add CSS partial tagged template literal to better leverage partial CSS fragments w/ CSS Directives * Update packages/web-components/fast-element/docs/guide/leveraging-css.md Co-authored-by: Rob Eisenberg <[email protected]> * refactor to use style collection logic from css and to support all return types of CSSDirective * open API to ComposableStyles * add test for ElementStyles interpolation * Change files * update docs to reflect opening API * update api-report * Update packages/web-components/fast-element/docs/guide/leveraging-css.md Co-authored-by: Jane Chu <[email protected]> Co-authored-by: nicholasrice <[email protected]> Co-authored-by: Rob Eisenberg <[email protected]> Co-authored-by: Jane Chu <[email protected]>
1 parent 07eaad7 commit 6ec5b99

File tree

6 files changed

+236
-13
lines changed

6 files changed

+236
-13
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add cssPartial template function to facilitate partial CSS abstractions.",
4+
"packageName": "@microsoft/fast-element",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/web-components/fast-element/docs/api-report.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ export class CSSDirective {
193193
createCSS(): ComposableStyles;
194194
}
195195

196+
// @public
197+
export function cssPartial(strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]): CSSDirective;
198+
196199
// @public
197200
export function customElement(nameOrDef: string | PartialFASTElementDefinition): (type: Function) => void;
198201

packages/web-components/fast-element/docs/guide/leveraging-css.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,71 @@ Rather than simply concatenating CSS strings, the `css` helper understands that
128128
You can also pass a CSS `string` or a [CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet) instance directly to the element definition, or even a mixed array of `string`, `CSSStyleSheet`, or `ElementStyles`.
129129
:::
130130

131+
### Partial CSS
132+
There are times when you may want to create reusable blocks of *partial* CSS, where the abstraction is not valid CSS in and of itself, such as groups of CSS properties or a complex value. To do that, you can use the `cssPartial` tagged template literal:
133+
134+
```ts
135+
import { css, cssPartial } from "@microsoft/fast-element";
136+
137+
const partial = cssPartial`color: red;`;
138+
const styles = css`:host{ ${partial} }`;
139+
```
140+
141+
`cssPartial` can also compose all structures that `css` can compose, providing even greater flexibility.
142+
143+
## CSSDirective
144+
The `CSSDirective` allows binding behavior to an element via `ElementStyles`. To create a `CSSDirective`, import and extend `CSSDirective` from `@microsoft/fast-element`:
145+
146+
```ts
147+
import { CSSDirective } from "@microsoft/fast-element"
148+
149+
class RandomWidth extends CSSDirective {}
150+
```
151+
152+
A CSS directive has two key methods that you can leverage to add dynamic behavior via CSS:
153+
154+
### createCSS
155+
`CSSDirective` has a `createCSS()` method that returns a string to be interpolated into an `ElementStyles`:
156+
157+
```ts
158+
class RandomWidth extends CSSDirective {
159+
createCSS() {
160+
return "width: var(--random-width);"
161+
}
162+
}
163+
```
164+
165+
### createBehavior
166+
The `createBehavior()` method can be used to create a `Behavior` that is bound to the element using the `CSSDirective`:
167+
168+
169+
```ts
170+
class RandomWidth extends CSSDirective {
171+
private property = "--random-width";
172+
createCSS() {
173+
return `width: var(${this.property});`
174+
}
175+
176+
createBehavior() {
177+
return {
178+
bind(el) {
179+
el.style.setProperty(this.property, Math.random() * 100)
180+
}
181+
unbind(el) {
182+
el.style.removeProperty(this.property);
183+
}
184+
}
185+
}
186+
}
187+
```
188+
189+
### Usage in ElementStyles
190+
The `CSSDirective` can then be used in an `ElementStyles`, where the CSS string from `createCSS()` will be interpolated into the stylesheet, and the behavior returned from `createBehavior()` will get bound to the element using the stylesheet:
191+
192+
```ts
193+
const styles = css`:host {${new RandomWidth()}}`;
194+
```
195+
131196
## Shadow DOM styling
132197

133198
You may have noticed the `:host` selector we used in our `name-tag` styles. This selector allows us to apply styles directly to our custom element. Here are a few things to consider always configuring for your host element:

packages/web-components/fast-element/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export {
1515
ComposableStyles,
1616
StyleTarget,
1717
} from "./styles/element-styles";
18-
export { css } from "./styles/css";
18+
export { css, cssPartial } from "./styles/css";
1919
export { CSSDirective } from "./styles/css-directive";
2020
export * from "./templating/view";
2121
export * from "./observation/observable";

packages/web-components/fast-element/src/styles/css.ts

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1+
import type { FASTElement } from "../components/fast-element";
12
import type { Behavior } from "../observation/behavior";
23
import { CSSDirective } from "./css-directive";
34
import { ComposableStyles, ElementStyles } from "./element-styles";
45

5-
/**
6-
* Transforms a template literal string into styles.
7-
* @param strings - The string fragments that are interpolated with the values.
8-
* @param values - The values that are interpolated with the string fragments.
9-
* @remarks
10-
* The css helper supports interpolation of strings and ElementStyle instances.
11-
* @public
12-
*/
13-
export function css(
6+
function collectStyles(
147
strings: TemplateStringsArray,
15-
...values: (ComposableStyles | CSSDirective)[]
16-
): ElementStyles {
8+
values: (ComposableStyles | CSSDirective)[]
9+
): { styles: ComposableStyles[]; behaviors: Behavior[] } {
1710
const styles: ComposableStyles[] = [];
1811
let cssString = "";
1912
const behaviors: Behavior[] = [];
@@ -49,6 +42,26 @@ export function css(
4942
styles.push(cssString);
5043
}
5144

45+
return {
46+
styles,
47+
behaviors,
48+
};
49+
}
50+
51+
/**
52+
* Transforms a template literal string into styles.
53+
* @param strings - The string fragments that are interpolated with the values.
54+
* @param values - The values that are interpolated with the string fragments.
55+
* @remarks
56+
* The css helper supports interpolation of strings and ElementStyle instances.
57+
* @public
58+
*/
59+
export function css(
60+
strings: TemplateStringsArray,
61+
...values: (ComposableStyles | CSSDirective)[]
62+
): ElementStyles {
63+
const { styles, behaviors } = collectStyles(strings, values);
64+
5265
const elementStyles = ElementStyles.create(styles);
5366

5467
if (behaviors.length) {
@@ -57,3 +70,76 @@ export function css(
5770

5871
return elementStyles;
5972
}
73+
74+
class CSSPartial extends CSSDirective implements Behavior {
75+
private css: string = "";
76+
private styles?: ElementStyles;
77+
constructor(styles: ComposableStyles[], private behaviors: Behavior[]) {
78+
super();
79+
80+
const stylesheets: ReadonlyArray<Exclude<
81+
ComposableStyles,
82+
string
83+
>> = styles.reduce(
84+
(
85+
accumulated: Exclude<ComposableStyles, string>[],
86+
current: ComposableStyles
87+
) => {
88+
if (typeof current === "string") {
89+
this.css += current;
90+
} else {
91+
accumulated.push(current);
92+
}
93+
return accumulated;
94+
},
95+
[]
96+
);
97+
98+
if (stylesheets.length) {
99+
this.styles = ElementStyles.create(stylesheets);
100+
}
101+
}
102+
103+
createBehavior(): Behavior {
104+
return this;
105+
}
106+
107+
createCSS(): string {
108+
return this.css;
109+
}
110+
111+
bind(el: FASTElement): void {
112+
if (this.styles) {
113+
el.$fastController.addStyles(this.styles);
114+
}
115+
116+
if (this.behaviors.length) {
117+
el.$fastController.addBehaviors(this.behaviors);
118+
}
119+
}
120+
121+
unbind(el: FASTElement): void {
122+
if (this.styles) {
123+
el.$fastController.removeStyles(this.styles);
124+
}
125+
126+
if (this.behaviors.length) {
127+
el.$fastController.removeBehaviors(this.behaviors);
128+
}
129+
}
130+
}
131+
132+
/**
133+
* Transforms a template literal string into partial CSS.
134+
* @param strings - The string fragments that are interpolated with the values.
135+
* @param values - The values that are interpolated with the string fragments.
136+
* @public
137+
*/
138+
export function cssPartial(
139+
strings: TemplateStringsArray,
140+
...values: (ComposableStyles | CSSDirective)[]
141+
): CSSDirective {
142+
const { styles, behaviors } = collectStyles(strings, values);
143+
144+
return new CSSPartial(styles, behaviors);
145+
}

packages/web-components/fast-element/src/styles/styles.spec.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
} from "./element-styles";
88
import { DOM } from "../dom";
99
import { CSSDirective } from "./css-directive";
10-
import { css } from "./css";
10+
import { css, cssPartial } from "./css";
11+
import type { Behavior } from "../observation/behavior";
12+
import { defaultExecutionContext } from "../observation/observable";
1113

1214
if (DOM.supportsAdoptedStyleSheets) {
1315
describe("AdoptedStyleSheetsStyles", () => {
@@ -244,4 +246,64 @@ describe("css", () => {
244246
expect(styles.behaviors?.includes(behavior)).to.equal(true)
245247
});
246248
})
249+
});
250+
251+
describe("cssPartial", () => {
252+
it("should have a createCSS method that is the CSS string interpolated with the createCSS product of any CSSDirectives", () => {
253+
class myDirective extends CSSDirective {
254+
createCSS() { return "red" };
255+
createBehavior() { return undefined; }
256+
}
257+
258+
const partial = cssPartial`color: ${new myDirective}`;
259+
expect (partial.createCSS()).to.equal("color: red");
260+
});
261+
262+
it("Should add behaviors from interpolated CSS directives when bound to an element", () => {
263+
const behavior = {
264+
bind() {},
265+
unbind() {},
266+
}
267+
268+
const behavior2 = {...behavior};
269+
270+
class directive extends CSSDirective {
271+
createCSS() { return "" };
272+
createBehavior() { return behavior; }
273+
}
274+
class directive2 extends CSSDirective {
275+
createCSS() { return "" };
276+
createBehavior() { return behavior2; }
277+
}
278+
279+
const partial = cssPartial`${new directive}${new directive2}`;
280+
const el = {
281+
$fastController: {
282+
addBehaviors(behaviors: Behavior[]) {
283+
expect(behaviors[0]).to.equal(behavior);
284+
expect(behaviors[1]).to.equal(behavior2);
285+
}
286+
}
287+
}
288+
289+
partial.createBehavior()?.bind(el, defaultExecutionContext)
290+
});
291+
292+
it("should add any ElementStyles interpolated into the template function when bound to an element", () => {
293+
const styles = css`:host {color: blue; }`;
294+
const partial = cssPartial`${styles}`;
295+
let called = false;
296+
const el = {
297+
$fastController: {
298+
addStyles(style: ElementStyles) {
299+
expect(style.styles.includes(styles)).to.be.true;
300+
called = true;
301+
}
302+
}
303+
}
304+
305+
partial.createBehavior()?.bind(el, defaultExecutionContext)
306+
307+
expect(called).to.be.true;
308+
})
247309
})

0 commit comments

Comments
 (0)