Webpack asset imports directly in Elixir code. For example in Phoenix controllers/views/templates or LiveView's.
- Only load assets that are used for current render.
- Helps to avoid assets over, under, or double fetching.
- Optimal asset packaging and cache reuse with Webpack code splitting.
- Supports Phoenix LiveView's by loading assets dynamically.
- Supports mix dependencies that are using
asset_import
.
The package can be installed by adding asset_import
to your list of dependencies in mix.exs
:
def deps do
[
{:asset_import, "~> 0.4.10"}
]
end
/* assets/css/forms.css */
form {
/* a lot of form styling, or import bootstrap `_forms.scss` etc */
}
// assets/js/login_form.js
import "../css/forms.css"
// some login form specific javascript here
Anywhere in your views, templates, or LiveView renders:
<div>
<%= if @logged_in do %>
<div>My profile..</div>
<% else %>
<!--
You can use <% asset_import @conn, "js/login_form" %> (without =),
if you are not using this template in LiveView. It will save some bytes from your html.
-->
<%= asset_import @conn, "js/login_form" %>
<!--
Inside LiveView use @socket instead @conn
-->
<%= asset_import @socket, "js/login_form" %>
<form>Login form..</form>
<% end %>
</div>
Usage with LiveView hooks that guarantees that code is always loaded before hook usage:
<div>
<%= if @logged_in do %>
<div>My profile..</div>
<% else %>
<form data-hook="MyHook" phx-hook="AssetHook" data-assets="<%= asset_hook(@socket, "js/myHook") %>">
Login form..
</form>
<% end %>
</div>
Assets js/login_form.js
and css/forms.css
are only loaded when user is not logged in.
Typical asset_import
config:
# config/config.exs
config :asset_import, MyAppWeb.Assets,
assets_base_url: "/",
assets_path: Path.expand("assets"),
manifest_path: Path.expand("priv/static/manifest.json"),
entrypoints_path: Path.expand("assets/entrypoints.json")
Replace phoenix watcher from webpack
to nodemon
:
# config/dev.exs
config :my_app, MyAppWeb.Endpoint,
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [node: ["node_modules/nodemon/bin/nodemon.js", cd: Path.expand("../assets", __DIR__)]]
Disable entrypoints file generation in test.
# config/test.exs
config :asset_import, entrypoints_path: :disabled
defmodule MyAppWeb.Assets do
use AssetImport,
assets_path: "assets" # optional, defaults to "assets"
use AwesomeUiComponents.Assets # add dependency assets
end
defmodule MyAppWeb do
..
def view do
quote do
..
use MyAppWeb.Assets
..
end
end
..
end
defmodule MyAppWeb.LayoutView do
use MyAppWeb, :view
import MyAppWeb.Assets.Files
end
<!-- Content rendering has to execute before scripts and styles -->
<% body = render "body.html", assigns %>
<html>
<head>
<!-- Styles for current page (render blocking) -->
<%= asset_styles() %>
<!-- Optional: Preload unused styles and scripts -->
<%= preload_asset_styles() %>
<%= preload_asset_scripts() %>
</head>
<body>
<%= body %>
<!-- Scripts for current page -->
<%= asset_scripts() %>
</body>
</html>
Or
<!-- Content rendering has to execute before scripts and styles -->
<% body = render "body.html", assigns %>
<html>
<head>
<!-- Styles for current page (render blocking) -->
<%= for path <- asset_style_files() do %>
<link rel="stylesheet" href="<%= path %>" />
<% end %>
<!-- Optional: Preload unused styles and scripts -->
<%= for path <- unused_asset_style_files() do %>
<link rel="preload" href="<%= path %>" as="style">
<% end %>
<%= for path <- unused_asset_script_files() do %>
<link rel="preload" href="<%= path %>" as="script">
<% end %>
</head>
<body>
<%= body %>
<!-- Scripts for current page -->
<%= for path <- asset_script_files() do %>
<script type="text/javascript" src="<%= path %>"></script>
<% end %>
</body>
</html>
import LiveSocket from "phoenix_live_view"
import Socket from "phoenix"
import AssetImport from "asset_import_hook"
let liveSocket = new LiveSocket("/live", Socket, { AssetImport })
liveSocket.connect()
Copy example_assets/*
to your project assets or adjust existing files manually:
-
// assets/nodemon.json { "watch": ["entrypoints.json", "webpack.config.js"], "exec": "node_modules/webpack/bin/webpack.js --color --mode development --watch-stdin" }
-
// assets/package.json { .. "dependencies": { .. "asset_import_hook": "0.4.10" // only when LiveView is used .. }, "devDependencies": { .. "nodemon": "1.19.2", "uglifyjs-webpack-plugin": "2.2.0", .. } }
-
// assets/webpack.config.js .. 8: const entrypoints = require('./entrypoints.json') || {}; 9: 10: if (Object.keys(entrypoints).length === 0) { 11: console.log('No entrypoints'); 12: process.exit(); 13: return; 14: } .. 23: runtimeChunk: 'single', 24: chunkIds: 'natural', 25: concatenateModules: true, 26: splitChunks: { 27: chunks: 'all', 28: minChunks: 1, 29: minSize: 0 30: } 31: }, 32: entry: entrypoints, 33: output: { 34: filename: 'js/[id]-[contenthash].js', 35: chunkFilename: 'js/[id]-[contenthash].js', 36: path: path.resolve(__dirname, '../priv/static') 37: }, 38: plugins: [ 39: new MiniCssExtractPlugin({ 40: filename: 'css/[id]-[contenthash].css', 41: chunkFilename: 'css/[id]-[contenthash].css', 42: }), 43: new ManifestPlugin({ fileName: 'manifest.json' }), ..