Page-Specific JavaScript with LiveView and esbuild Phoenix 1.6.0

Hello everyone,

Can someone help how to do Page-Specific JavaScript with LiveView and esbuild

I have found directions for the old webpack way of packing … but non for the new 1.6 release with esbuild

Thanks! :smiley:

p.s. a side question - can we also take advantage from the esbuild splitting

1 Like

I think you can run the build command for each entrypoint separately, it would result in multiple bundles in your output directory.

mix esbuild js/app1.js --bundle --target=es2016 --outdir=../priv/static/assets --minify
mix esbuild js/app2.js --bundle --target=es2016 --outdir=../priv/static/assets --minify

Splitting can also be used by providing the necessary flags to the build command.

mix esbuild default --format=esm --splitting --minify

  ../priv/static/assets/app.js                 79.2kb
  ../priv/static/assets/uPlot.esm-VJBKNXIX.js  40.3kb
  ../priv/static/assets/chunk-GXEDC2R5.js       1.1kb

⚡ Done in 11ms

Thank you @ruslandoga
i’ll definitely try it !

can you show how you dynamically import the bundle afterwards inside a hook for specific view. i failed miserably resolving the path to the bundle (error: Could not resolve"path"…)

Not sure what you mean by importing a bundle (do you manually bundle the files before importing them in a hook?), but for importing plain deps from node_modules I perform the same steps as I did for webpack:

In app.js it’s hooks as usual:

import { ChartHook } from "./hooks";

// ... csrfToken = ...

let liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },
  hooks: { ChartHook },

And in hooks/index.js I do async import which I await for in asynced mounted:

async function makeChart(el) {
  const { default: uPlot } = await import("uplot");
  // opts = ... data = ...
  return new uPlot(opts, data, el);

export const ChartHook = {
  async mounted() {
    this.chart = await makeChart(this.el);

  destroyed() {
    if (this.chart) this.chart.destroy();

This means that I don’t really specify a view / bundle here but rather the necessary code is loaded only when that hook is used on the page.

<div id="chart" phx-hook="ChartHook" phx-update="ignore"></div>

yes, exactly - this is what i was trying to do. bundling the view specific js into separate bundle, and loading the file only for a specific view

Could you please post some code (both js and html) for me to understand your use-case better?

something like this

export const LoadEditor = {
  async mounted() {
  destroyed() {
    if (this.quill) this.quill.destroy();

var toolbarOptions = [
  ["bold", "italic", "underline", "strike"], // toggled buttons
  ["blockquote", "code-block"],

  [{ header: 1 }, { header: 2 }], // custom button values
  [{ list: "ordered" }, { list: "bullet" }],
  [{ script: "sub" }, { script: "super" }], // superscript/subscript
  [{ indent: "-1" }, { indent: "+1" }], // outdent/indent
  [{ direction: "rtl" }], // text direction

  [{ size: ["small", false, "large", "huge"] }], // custom dropdown
  [{ header: [1, 2, 3, 4, 5, 6, false] }],

  [{ color: [] }, { background: [] }], // dropdown with defaults from theme
  [{ font: [] }],
  [{ align: [] }],

  ["clean"], // remove formatting button

const codeInput = document.getElementById("quill-editor");
const startQuillEditor = (el) => {

   //how to point to the local assets bundle - OR must refer to the https://domain/assets/folder
//also the example can't load properly the css in this way  - Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/css"


  ]).then(([quillcss, quill]) => {
    const quill = new Quill(el, {
      theme: "snow",
      debug: false,
      modules: {
        toolbar: {
          toolbar: toolbarOptions,

p.s. the idea is to bundle the quill.js with an additional esbuild step, and loading it only for this particular view

inside the config.exs

config :esbuild,
args: ~w(js/app.js js/quill.js --bundle --target=es2016 --outdir=../priv/static/assets),

I don’t see anything stopping you from the approach I outlined above. “Just” async import("./quill") in your hook and limit entrypoint to app.js in esbuild command.

probably i’m missing something :frowning:

but when i do that - the code gets packed inside app.js and i don’t want that to happen

export const quillEditor = {
  mounted() {
    import('quill').then(({ default: Quill }) => { 
      const editorInstance = new Quill(this.el, {
        placeholder: 'Start writing...',
        modules: {
          toolbar: toolbarOptions
        theme: 'snow'

If you do something like what ruslandoga did… or similar to how I did it above, I think it’ll load as a module when you run phoenix digest. You should see your app.js size be smaller on page load without quill on it… then it’ll grab the quill specific bundle later.

i tried the approach and unfortunately the scripts are bundled inside the app.js

from 57K initially, after refactoring - implementing this approach of loading few js components (and after digest) the app.js.gz skyrocketed to 507K

finally solved it

non of the above will work if you don’t do the following:

from esbuild

Note that when code splitting is disabled (i.e. the default behavior), an import('path') expression behaves similar to Promise.resolve(require('path')) and still bundles the imported file into the entry point bundle. No additional chunks are generated in this case.

so you must first configure the esbuild inside config.exs like so:

~w(js/app.js css/quill.css --chunk-names=chunks/[name]-[hash] --bundle --outdir=../priv/static/assets --splitting --format=esm --target=es2016),

then in your template you must change the app.js type like so (type=“module” otherwise you will get errors that you can’t use imports outside a module …):

<script defer phx-track-static type="module" src={Routes.static_path(@conn, "/assets/js/app.js")}></script>

now the code will load from the generated chunks