guides/sandboxing.md

# Sandboxing components

In `PhxLiveStorybook` your components live within the storybook, so they share
some context with the storybook: **styling** and **scripts**.

While the original Storybook for React only [relies on iframes](https://storybook.js.org/docs/react/configure/story-rendering), we find them quite slow and don't want them to be the default choice.

This guide will explain:

- what JS context do your components share with the storybook?
- how is the storybook styled, to prevent most styling clashes?
- how should you provide the style of your components with scoped styles?
- how to, as a last resort, enable iframe rendering?

## 1. What JS context do your components share with the storybook?

`PhxLiveStorybook` runs with Phoenix LiveView and therefore requires its `LiveSocket`.
This LiveSocket is the same used by your components: you just need to inject it with your
own `Hooks`, `Params` and `Uploaders`.

To do so, create a JS file that will declare your `Hooks`, `Params` and `Uploaders` and set them in
`window.storybook`. This script will be loaded immediately before the storybook's script.

```javascript
// assets/js/my_components.js
import * as Hooks from "./hooks";
import * as Params from "./params";
import * as Uploaders from "./uploaders";
(function () {
  window.storybook = { Hooks, Params, Uploaders };
})();
```

Then set the `js_path: "/assets/js/components.js"` option to the storybook within your `config.exs`
file.

You can also use this script to inject whatever content you want into document `HEAD`, such as
external scripts.

The `Params` will be available in page stories as `connect_params` assign.
There is currently no way to access them in component or live component stories.

## 2. How is the storybook styled?

`PhxLiveStorybook` is using [TailwindCSS](https://tailwindcss.com) with
[preflight](https://tailwindcss.com/docs/preflight) (which means all default HTML styles from your
browser are removed) and a [custom prefix](https://tailwindcss.com/docs/configuration#prefix):
`lsb-` (which means that instead of using `bg-blue-400` the storybook uses `lsb-bg-blue-400`).

Only elements with the `.lsb` class are preflighted, in order to let your component styling as-is.

So unless your components use `lsb` or `lsb-` prefixed classes there should be no styling leak from
the storybook to you components.

## 3. How should you provide the style of your components?

You need to inject your component's stylesheets into the storybook. Just (like for JS), set the
`css_path: "/assets/css/components.css"` option in `config.exs`.

The previous part (2.) was about storybook styles not leaking into your components. This part is
about the opposite: don't accidentally mess up Storybook styling with your styles.

All containers rendering your components in the storybook (`stories`, `playground`, `pages` ...)
carry the `.lsb-sandbox` CSS class and a **custom sandboxing class of your choice**.

You can leverage this to scope your styles with this class. Here is how you can do it with
`TailwindCSS`:

- configure `phx_live_storybook` with a custom `sandbox_class`:

```elixir
# lib/my_app_web/storybook.ex
defmodule MyAppWeb.Storybook do
  use PhxLiveStorybook,
    ...
    sandbox_class: "my-app-sandbox",
```

- use Tailwind [important selector strategy](https://tailwindcss.com/docs/configuration#selector-strategy)
  with this class. It will prefix all your tailwind classes increasing their specificity, hence
  their priority.

```javascript
// assets/tailwind.config.js
module.exports = {
  // ...
  important: ".my-app-sandbox",
};
```

- nest your custom styles under Tailwind `@layer utilities`. This way, your styling will also
  benefit from sandboxing.

```css
/* assets/css/components.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
  /* this style will be generated as .my-app-sandbox :not(i) { ... } */
  /* we use :not(i) instead of * because we don't want to override FontAwesome icons styles */
  /* (some icons are indeed rendered with your own sandboxed styles) */
  :not(i) {
    font-family: "MyComponentsFont";
    @apply text-slate-600;
  }

  /* this style will be generated as .my-app-sandbox h1 { ... } */
  h1 {
    @apply text-2xl font-bold text-slate-700 mt-2 mb-6;
  }

  /* this style will be generated as .my-app-sandbox h2 { ... } */
  h2 {
    @apply text-xl font-bold text-slate-700 mt-2 mb-4;
  }
}
```

## 4. Enabling iframe rendering

As a last resort, if for whatever reason you cannot make your component live within the storybook
(an example would be that your component needs to bind listeners on `document`), it is possible to
enable iframe rendering, component per component.

Just add the `iframe` option to it.

```elixir
# storybook/components/button.exs
defmodule MyAppWeb.Storybook.Components.Button do
 alias MyAppWeb.Components.Button
 use PhxLiveStorybook.Story, :component

 def function, do: &Button.button/1
 def container, do: :iframe

 # ...
end
```