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

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() {
    startQuillEditor(this.el);
  },
  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) => {
  Promise.all([

   //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"

    import("https://cdn.quilljs.com/1.3.6/quill.snow.css"),
    import("https://cdn.quilljs.com/1.3.6/quill.js"),

  ]).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 }) => { 
      import('quill/dist/quill.snow.css');
      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.