Packages
Colocated, Scoped, Formatted, Ergonomic. Intermix JS, CSS & Phoenix LiveView components without limits.
Current section
Files
Jump to
Current section
Files
README.md
# Coloco
**Colocated, Scoped, Formatted, Ergonomic. Intermix JS, CSS & Phoenix LiveView
components without limits.**
## Why?
Phoenix LiveView's (1.2) implementation of component-colocated CSS and JS is a
fantastic addition, allowing us to finally separate fully by concern rather than
implementation language as frontend codebases have been doing since components
became the UI architecture pattern de jour. However, colocated CSS and JS in LiveView
1.2 has certain limitations, especially when using a template engine other than HEEx
(eg. [Temple](https://github.com/mhanberg/temple)). LiveView also leaves much of the
implementation of colocated CSS/JS to consumer side plugin code. Coloco fills in these
gaps, providing ergonomic tooling out-of-the-box which integrates well with any
template system.
Coloco aims to provide an excellent dev experience in these areas:
- **Ergonomics:** A set of macros allow flexible expression of JS and CSS code within
Phoenix component files, next to or within the template the code is relevant to.
This comes without any runtime cost.
- **CSS Scoping:** Coloco provides a "low-fi" or "low-magic" form of CSS scoping using
generated `@scope` rules, with fallback strategy for browsers that don't support this.
- Note that "de-scoping" does not always happen automatically. Often you'll want
a CSS scope to end when a component's slot content starts, and in these cases
you'll need to use an element with a `descope_css` class or attr around the slot.
- Coloco does generate CSS to automatically "de-scope" wherever a new CSS scope
begins. So in cases where a CSS-scoped parent component has child sub-components
with their own scope, no manual de-scope is needed; the scopes will not overlap.
- **CSS Post-Processing:** In most production settings, it's highly beneficial to do
some transformation of CSS between source code and what is shipped to the browser.
[Autoprefixer](https://github.com/postcss/autoprefixer) is ubiquitous for auto-adding
variations of rules for browser compatibility reasons. Another good example is CSS
nesting, which is not yet supported in older browsers. [PostCSS](https://postcss.org/)
provides plugin-based handling of these transforms and many others from a rich
ecosystem, but it has rough edges when integrated with Phoenix. Coloco makes PostCSS
setup easy, and out-of-the-box handles bugbears like PostCSS watcher process shutdown
(when installed naively, orphaned processes will outlive the Phoenix server and
accumulate;
[see this issue](https://elixirforum.com/t/extra-watcher-doesnt-get-killed-when-shutting-down-phoenix/2807)).
- **Colocated Code Formatting:** Setting up automatic formatting of colocated code
is possible in LiveView 1.2, which is amazing, but much of the actual implementation
is left to user-side plugin code making initial setup cumbersome. Coloco provides
pre-built plugins which use Prettier to format colocated JS and CSS, with only a few
easy changes to `.formatter.exs` required.
## Installation
Add `coloco` to your list of dependencies in `mix.exs`, then run `mix deps.get`:
```elixir
def deps do
[
{:coloco, "~> 0.1.0"}
]
end
```
## Setup
There are two ways Coloco can be used:
1. Through four small macros: `scope_css`, `descope_css`, `colocate_js`, `colocate_hook`
- **Setup:** Add `import Coloco` to your module, or add it within
the `html_helpers` section of your Phoenix app config to make these macros
available throughout all components (live and otherwise).
2. Directly, by calling the `ScopeCSS` module directly.
- **Setup:** None; just call `Coloco.ScopedCSS.scope` and other functions wherever
you need them.
These two methods are functionally equivalent; macros operate during compilation
so runtime behavior will be identical. Here are kitchen-sink examples of each style
for comparison:
```elixir
# Macro style:
defmodule MyApplication.MyComponent do
use MyApplicationWeb, :live_view
def render(assigns) do
temple do
div class: css_scope() do
p "phx-hook": p_hook(), id: "hooks-need-ids" do
"hello world"
end
div class: descope_css(), do: slot @inner_block
colocate_js(~H"""
alert("hello world from colocated js");
"""js)
end
end
end
def css_scope() do
scope_css(~H"""
p {
color: green;
}
"""css)
end
def p_hook() do
colocate_hook(~H"""
export default {
mounted() {
alert("hello world from colocated hook");
},
};
"""js)
end
end
```
```elixir
# Direct-call / Macroless style:
defmodule MyApplication.MyComponent do
use MyApplicationWeb, :live_view
def render(assigns) do
temple do
div class: css_scope() do
p "hello world"
div class: Coloco.ScopedCSS.descope(__ENV__), do: slot @inner_block
~H"""
<script :type={Phoenix.LiveView.ColocatedJS}>
alert("hello world from colocated js");
</script>
""")
end
end
end
def css_scope() do
Coloco.ScopedCSS.scope(__ENV__, ~H"""
<style :type={Coloco.ScopedCSS}>
p {
color: green;
}
</style>
""")
end
def p_hook() do
hook_name = ".p_hook"
module = __MODULE__ |> to_string() |> String.replace_prefix("Elixir.", "")
hook_name_prefixed_with_module = module <> hook_name
~H"""
<script :type={Phoenix.LiveView.ColocatedHook} name="#{hook_name}">
export default {
mounted() {
alert("hello world from colocated hook");
},
};
</script>
""")
hook_name_prefixed_with_module
end
end
```
## PostCSS (Browser-compatible CSS nesting & auto-prefixing)
Coloco expects the calling application to manage installation of PostCSS and its
plugins, which gives much greater flexibility. First, run these commands in the
`assets` directory:
```sh
cd assets
! [[ -f package.json ]] && echo "{}" >> package.json
npm install --save-dev postcss postcss-import postcss-nesting autoprefixer prettier tailwindcss @tailwindcss/cli @tailwindcss/postcss daisyui
```
Now you'll need to add a PostCSS config file in `assets`. Here's an example using the
plugins installed above; copy this config into `assets/postcss.config.cjs`:
```javascript
const path = require("path")
module.exports = {
plugins: [
require("postcss-import")({ path: process.env.NODE_PATH.split(path.delimiter) }),
// postcss-import should come first (per plugin docs)
require("@tailwindcss/postcss"),
require("postcss-nesting"),
require("autoprefixer"),
]
}
```
Now, make these changes to Phoenix app config to run PostCSS during build and also
watch source files changes to rebuild when the dev server is running:
```diff
# mix.exs
defp aliases do
[
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
--- "assets.build": ["compile", "tailwind example_app", "esbuild example_app"],
+++ "assets.build": ["compile", &Coloco.PostCSS.build/1, "esbuild example_app"],
"assets.deploy": [
--- "tailwind example_app --minify",
"esbuild example_app --minify",
"phx.digest"
],
```
```diff
# config/dev.exs
config :example_application, ExampleApplication.Endpoint
...,
watchers: [
esbuild: {Esbuild, :install_and_run, [:example_app, ~w(--sourcemap=inline --watch)]},
--- tailwind: {Tailwind, :install_and_run, [:example_app, ~w(--watch)]}
+++ postcss: {Coloco.PostCSS, :watcher, []}
]
```
Note that PostCSS runs all Tailwind-related processing via the `@tailwind/postcss`
plugin, so standalone commands to invoke tailwind are no longer necessary and are
removed from the files above.
## Colocated Code Formatting
In order to automatically format JS and CSS colocated code whenever `mix format` is
run (either via CLI, or editor integration) make these changes to your
`.formatter.exs` config file at project root:
```diff
[
import_deps: [:ecto, :ecto_sql, :phoenix, :temple],
subdirectories: ["priv/*/migrations"],
plugins: [
+++ Coloco.Format.PreHTMLFormatterPlugin, # add BEFORE LiveView.HTMLFormatter
Phoenix.LiveView.HTMLFormatter,
+++ Coloco.Format.PostHTMLFormatterPlugin, # add AFTER LiveView.HTMLFormatter
],
inputs: [
"*.{heex,ex,exs}",
"{config,lib,test}/**/*.{heex,ex,exs}",
"priv/*/seeds.exs",
],
+++ tag_formatters: %{
+++ script: Coloco.Format.PrettierTagFormatter,
+++ style: Coloco.Format.PrettierTagFormatter,
+++ },
...
```
The sole purpose of Coloco's `PreHTMLFormatterPlugin` and `PostHTMLFormatterPlugin` is
to manage surrounding `<script>` and `<style>` tags properly so that
`Phoenix.LiveView.HTMLFormatter` can operate normally even when these tags aren't
included in source code. If you prefer, you can avoid using these plugins and use
wrapping tags when you define colocated code instead:
```elixir
scope_css(~H"""
<style>
p {
color: green;
}
</style>
"""css)
```
instead of
```elixir
scope_css(~H"""
p {
color: green;
}
"""css)
```
With `PreHTMLFormatterPlugin` and `PostHTMLFormatterPlugin` included in plugins, both
of these will behave identically and be formatted identically (any wrapping tags you
add will remain in place). Without these plugins, only the first example will work
with the formatter; the second will cause it to error.
## License
MIT