Skip to content

Commit

Permalink
feat(css): add support for injecting css into custom elements
Browse files Browse the repository at this point in the history
  • Loading branch information
Sven Tschui committed Feb 26, 2023
1 parent 2aad552 commit 94b41d9
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 4 deletions.
20 changes: 20 additions & 0 deletions docs/config/shared-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,26 @@ export default defineConfig({
})
```

## css.inject

- **Type:** `string | ((node: Element) => void)`

A (stringified) function with the signature `(node: Element) => void` that is used to inject CSS style tags when importing CSS in JS.

If passed a function it will be stringified and can therefore not rely on any variables on the outer scopes.

Note this does not affect `<link >` tags added to `index.html`.

```js
export default defineConfig({
css: {
inject: (node) => {
document.body.querySelector('custom-element').shadowRoot.appendChild(node)
},
},
})
```

## css.devSourcemap

- **Experimental**
Expand Down
15 changes: 12 additions & 3 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,12 @@ const sheetsMap = new Map<string, HTMLStyleElement>()
// because after build it will be a single css file
let lastInsertedStyle: HTMLStyleElement | undefined

export function updateStyle(id: string, content: string): void {
export function updateStyle(
id: string,
content: string,
inject?: (style: HTMLStyleElement) => void,
): void {
console.log('updateStyle', id, content)
let style = sheetsMap.get(id)
if (!style) {
style = document.createElement('style')
Expand All @@ -346,7 +351,11 @@ export function updateStyle(id: string, content: string): void {
style.textContent = content

if (!lastInsertedStyle) {
document.head.appendChild(style)
if (inject) {
inject(style)
} else {
document.head.appendChild(style)
}

// reset lastInsertedStyle after async
// because dynamically imported css will be splitted into a different file
Expand All @@ -366,7 +375,7 @@ export function updateStyle(id: string, content: string): void {
export function removeStyle(id: string): void {
const style = sheetsMap.get(id)
if (style) {
document.head.removeChild(style)
style.remove()
sheetsMap.delete(id)
}
}
Expand Down
10 changes: 9 additions & 1 deletion packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export interface CSSOptions {
* @experimental
*/
devSourcemap?: boolean
/**
* Stringified function with the signature `(node: Element) => void`
* that is used to inject stylesheets.
*
* By default styles are appended to the document.head.
*/
inject?: string | ((node: Element) => void)
}

export interface CSSModulesOptions {
Expand Down Expand Up @@ -401,7 +408,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
)}`,
`const __vite__id = ${JSON.stringify(id)}`,
`const __vite__css = ${JSON.stringify(cssContent)}`,
`__vite__updateStyle(__vite__id, __vite__css)`,
`const __vite__inject_css = ${config.css?.inject}`,
`__vite__updateStyle(__vite__id, __vite__css, __vite__inject_css)`,
// css modules exports change on edit so it can't self accept
`${
modulesCode ||
Expand Down
24 changes: 24 additions & 0 deletions playground/css-inject/__tests__/css-inject.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, test } from 'vitest'
import { editFile, getColor, page, untilUpdated } from '~utils'

test('css inject', async () => {
const linkedOutside = await page.$('.linked.outside')
const linkedInside = await page.$('.linked.inside')
const importedOutside = await page.$('.imported.outside')
const importedInside = await page.$('.imported.inside')

expect(await getColor(linkedOutside)).toBe('red')
expect(await getColor(linkedInside)).toBe('black')
expect(await getColor(importedOutside)).toBe('black')
expect(await getColor(importedInside)).toBe('red')

editFile('linked.css', (code) => code.replace('color: red', 'color: blue'))

await untilUpdated(() => getColor(linkedOutside), 'blue')
expect(await getColor(linkedInside)).toBe('black')

editFile('imported.css', (code) => code.replace('color: red', 'color: blue'))

await untilUpdated(() => getColor(importedInside), 'blue')
expect(await getColor(importedOutside)).toBe('black')
})
3 changes: 3 additions & 0 deletions playground/css-inject/imported.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.imported {
color: red;
}
37 changes: 37 additions & 0 deletions playground/css-inject/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script></script>
<link rel="stylesheet" href="./linked.css" />

<div class="wrapper">
<h1>CSS Inject</h1>

<p class="linked outside">&lt;linked&gt;</p>

<p class="imported outside">&lt;imported&gt;</p>

<custom-element></custom-element>
</div>

<script type="module">
customElements.define(
'custom-element',
class CustomElement extends HTMLElement {
constructor() {
super()
console.log('ctor')
this.attachShadow({ mode: 'open' })
}

connectedCallback() {
console.log('connected')

this.shadowRoot.innerHTML = `
<p class="linked inside">&lt;linked&gt;</p>
<p class="imported inside">&lt;imported&gt;</p>
`

import('./imported.css')
}
},
)
</script>
3 changes: 3 additions & 0 deletions playground/css-inject/linked.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.linked {
color: red;
}
11 changes: 11 additions & 0 deletions playground/css-inject/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@vitejs/test-css-sourcemap",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
"preview": "vite preview"
}
}
21 changes: 21 additions & 0 deletions playground/css-inject/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const MagicString = require('magic-string')

/**
* @type {import('vite').UserConfig}
*/
module.exports = {
resolve: {
alias: {
'@': __dirname,
},
},
css: {
devSourcemap: true,
inject: (node) => {
document.body.querySelector('custom-element').shadowRoot.appendChild(node)
},
},
build: {
sourcemap: true,
},
}

0 comments on commit 94b41d9

Please sign in to comment.