parent
7d93c9d88b
commit
6c50ff1b84
28
.vscode/tasks.json
vendored
Normal file
28
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "dev",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"label": "npm: dev",
|
||||||
|
"detail": "vite dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm",
|
||||||
|
"args": ["run", "new-post", "${input:post-name}"],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"label": "npm: new post",
|
||||||
|
"detail": "new post"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"type": "promptString",
|
||||||
|
"id": "post-name",
|
||||||
|
"description": "Post name",
|
||||||
|
"default": "Incididunt aliqua adipisicing ipsum"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -12,7 +12,8 @@
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"pagefind": "pagefind",
|
"pagefind": "pagefind",
|
||||||
"preview:pagefind": "pagefind --site .svelte-kit/output/prerendered/pages/",
|
"preview:pagefind": "pagefind --site .svelte-kit/output/prerendered/pages/",
|
||||||
"build:pagefind": "pagefind --site build/"
|
"build:pagefind": "pagefind --site build/",
|
||||||
|
"new-post": "node scripts/new-post.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "^1.27.6",
|
"@sveltejs/kit": "^1.27.6",
|
||||||
|
|
23
scripts/new-post.js
Normal file
23
scripts/new-post.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { format, formatISO } from 'date-fns';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const title = process.argv[2] ?? 'New Blog Post';
|
||||||
|
const slug = title.toLowerCase().replaceAll(' ', '-').slice(0, 60);
|
||||||
|
|
||||||
|
const filename = `${format(new Date(), 'yyyy.MM.dd')}.${slug}.md`;
|
||||||
|
|
||||||
|
const contents = `---
|
||||||
|
title: '${title}'
|
||||||
|
date: ${formatISO(new Date())}
|
||||||
|
description: 'Blog post description here'
|
||||||
|
---
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('dirname', __dirname);
|
||||||
|
console.log('filename', filename);
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(__dirname, '..', 'src', 'routes', 'posts', filename), contents);
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
title: Making Customizable Components in Svelte
|
title: 'Making Customizable Components in Svelte'
|
||||||
date: 2024-05-12T17:50:52+0000
|
date: 2024-05-12T17:50:52+0000
|
||||||
tags:
|
tags:
|
||||||
- dev
|
- dev
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
---
|
||||||
|
title: 'Using Shadow DOM to isolate injected browser extension components'
|
||||||
|
date: 2024-05-18T23:08:16-05:00
|
||||||
|
description: 'I spent a weekend building a browser extension with React and TailwindCSS, but injected components can be a lot of trouble. This post explores how Shadow DOM saves the day (and your CSS sanity).'
|
||||||
|
---
|
||||||
|
|
||||||
|
I recently started experimenting with building a browser extension. I'm using
|
||||||
|
React and TailwindCSS --bundling it all with Vite-- creating some UI elements like buttons and injecting them
|
||||||
|
into certain web pages. Setting this up was super easy, I used [a repository template](https://github.com/JohnBra/vite-web-extension)
|
||||||
|
to get a PoC as quickly as possible, and a few minutes later I had something working!
|
||||||
|
|
||||||
|
One issue with injecting components into web pages though is that your
|
||||||
|
components are injected into the regular page DOM. This means that your
|
||||||
|
components can be affected by scripts or CSS on the page, and conversely your
|
||||||
|
CSS can affect existing elements on the page should anything collide.
|
||||||
|
|
||||||
|
This is a bit of a problem when using TailwindCSS. On one hand, utility classes
|
||||||
|
have the same meaning across websites. For example, `w-full` is always going to
|
||||||
|
be `width: 100%;`, because what else would it be? On the other hand, websites
|
||||||
|
_are_ always able to change what CSS these utility classes map to. `rounded`
|
||||||
|
might mean different amounts of rounding on different websites, and
|
||||||
|
`bg-color-primary` is going to map to different colors based on the color
|
||||||
|
palette of the website. And nobody can stop you if you want to change `w-full`
|
||||||
|
to set the width to 10 pixels.
|
||||||
|
|
||||||
|
Avoiding these collisions in the past would have involved prefixing all your IDs
|
||||||
|
and classes so that they are distinct from any ones you might encounter on the
|
||||||
|
page. Even then, Javascript code of the original page can traverse the
|
||||||
|
components you inject on the page and inspect or modify them! Nowadays though,
|
||||||
|
Shadow DOM allows us to easily isolate components on the page. Using Shadow DOM,
|
||||||
|
the CSS of the injected components is isolated from the rest of the page. That
|
||||||
|
isolation works in both directions, the CSS from the original page won't affect
|
||||||
|
the elements in the Shadow DOM, and the CSS injected into the Shadow DOM won't
|
||||||
|
affect the elements outside of it. Plus, Shadow DOM has an option to isolate the
|
||||||
|
injected components from Javascript of the original page so they can't traverse
|
||||||
|
into the Shadow DOM (the original page can still delete or move the root of the
|
||||||
|
Shadow DOM, but can't look inside of it).
|
||||||
|
|
||||||
|
And the code for this is very simple! Shadow DOM might sound a bit scary, but
|
||||||
|
it's actually very easy. Let's start with some basic code to inject a React
|
||||||
|
component into a page:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import styles from "./style.css";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
const injectionTarget = document.querySelector<HTMLDivElement>(
|
||||||
|
"#place-to-inject-my-component",
|
||||||
|
);
|
||||||
|
if (!injectionTarget) {
|
||||||
|
throw new Error("Can't find injection target");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootContainer = document.createElement("div");
|
||||||
|
injectionTarget.appendChild(rootContainer);
|
||||||
|
|
||||||
|
const root = createRoot(rootContainer);
|
||||||
|
root.render(<YourElement />);
|
||||||
|
```
|
||||||
|
|
||||||
|
To use Shadow DOM, we need to create the Shadow DOM and attach it to the page,
|
||||||
|
then inject our React root inside of the Shadow DOM. This is as easy as calling
|
||||||
|
`attachShadow`! Let's modify the example to use the Shadow DOM:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import styles from "./style.css";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
const injectionTarget = document.querySelector<HTMLDivElement>(
|
||||||
|
"#place-to-inject-my-component",
|
||||||
|
);
|
||||||
|
if (!injectionTarget) {
|
||||||
|
throw new Error("Can't find injection target");
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadowDomRoot = document.createElement("div");
|
||||||
|
injectionTarget.appendChild(shadowDomRoot);
|
||||||
|
const shadow = shadowDomRoot.attachShadow({
|
||||||
|
// closed means the contents of the shadow can't be accessed from the page
|
||||||
|
mode: "closed"
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootContainer = document.createElement("div");
|
||||||
|
shadow.appendChild(rootContainer);
|
||||||
|
|
||||||
|
const root = createRoot(rootContainer);
|
||||||
|
root.render(<YourElement />);
|
||||||
|
```
|
||||||
|
|
||||||
|
Nice! We just had to add about 3 lines of code, and now our components lives
|
||||||
|
inside it's own bubble.
|
||||||
|
|
||||||
|
Now, if you are following along with this example and running the code, you
|
||||||
|
might have noticed that your injected component suddenly has no styles applied
|
||||||
|
to it now. Well, that's the CSS isolation the Shadow DOM gives you. How do we
|
||||||
|
get around this? That's not too much work either, we just need to inject the CSS
|
||||||
|
into the page too.
|
||||||
|
|
||||||
|
First, you can use the manifest file to inject CSS into the page declaratively.
|
||||||
|
This is in fact what the repository template I linked above does. But that's not
|
||||||
|
what we want here, because we don't want the CSS to be just injected into the
|
||||||
|
page. We want it injected specifically into the Shadow DOM we just created. So,
|
||||||
|
we'll have to disable this declarative CSS injection.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||||
|
"js": ["src/pages/content/index.tsx"],
|
||||||
|
// Make sure this is empty
|
||||||
|
"css": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we got rid of that, we'll want to import the CSS into our script and
|
||||||
|
inject that along too. The trick here is adding `?inline` to the end of the CSS
|
||||||
|
import so that rather than injecting the CSS into the page, Vite understands that
|
||||||
|
we want the contents of the CSS.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Note the `?inline`
|
||||||
|
import styles from './style.css?inline';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
// ... same as example above
|
||||||
|
|
||||||
|
// Create the style element, put the CSS into it,
|
||||||
|
// and inject it into the Shadow DOM.
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.innerHTML = styles;
|
||||||
|
shadow.appendChild(style);
|
||||||
|
|
||||||
|
// ... same as example above
|
||||||
|
```
|
||||||
|
|
||||||
|
Tada! Your component should now light up with all the fancy CSS --or utility
|
||||||
|
classes-- you added to it. While the Shadow DOM example is slightly more complex
|
||||||
|
than the most basic option I demonstrated first, the advantages you get in CSS
|
||||||
|
and JS isolation are worth it considering it only takes a few lines of text.
|
Loading…
Reference in a new issue