kccarter

kccarter

I’m experimenting with different ways to incorporate a bit of Javascript into some of my pages. (Not using LiveView.)

Consider the following heex template:

<div>
  <button>MyButton</button>
</div>

<script>
  // Some Javascript goes in here....
</script>

Is there anywhere in the “build process” that I could minimize the Javascript that is within the script tags?

We’re aware we can put code in app.js, and even import other files into app.js, but we’re just curious if there’s a way to inline it like we have. For some pages, we’re only sprinkling a very small amount of Javascript and would rather keep that Javascript code as close to where it’s being used.

Discussions headed down the path of whether the script tag could be a functional component that, at compile time, could take the string and “call out” to something like esbuild for minification, though we have no idea if that’s possible within the Elixir build steps.

The goal here is to see if there’s any merrit in a design pattern that is mostly heex, but with a bit of JS “inlined” in the heex templates, rather than having to fallback to using Javascript files and imports / included.

Showing Posts 1 to 10

sodapopcan

sodapopcan

I personally can’t think of any sane way to do this.

You can use esbuild to minify like so:

echo "const foo = bar -> bar * 2" | esbuild --minify

You would have to use System.shell to execute this and do the proper escaping and be very careful as that function is prone to injections.

However you can’t use render_slot outside of of ~H. I believe you can do this in Surface since it supports macro components (hopefully HEEx gets them one day as I would love a <.markdown /> component). But I don’t think there is any sane way to do this in HEEx.

I take no issue with your colocated script tag experiment but I do have to be “that guy” and ask: why do you care about minification if you are only sprinkling in a little bit of JS here and there? I can’t imagine it making a meaningful difference. Minification gets the big wins when the majority of your app is written in client-side JS along with its pit of dispair node_modules directory.

kccarter

kccarter OP

Yeah, I knew that would brought up… :wink:

You’re right that it’s likely to be irrelevant with regards to size. More of an “obfuscation” type of thing to prevent internal comments from potentially leaking out into public code.

regex.sh

regex.sh

Hmm, it should be possible.
Write a task that can be run like “mix test” or “mix ecto.setup”
That task will scan all files for some custom pattern (you can put some text in a script tag like x-inline-minify) and will take that code, minify it, and replace it.

kccarter

kccarter OP

What if we slightly relaxed our requirement of it being entirely inline and instead did something like a “Javascript sidecar” file.

Template:

form.html.heex

<div>
  <button>Hello World</button>
</div>

<.inline_js 'form.js' />

Sidecar:

form.js

function helloWorld() { 
  console.log("Hello World");
}

And then have some custom variant of embed_templates that looks for the JS sidecar files and creates functional components out of them?

And yes, this is getting a bit into the weeds, so consider this more a discussion of what’s possible and not necessarily what’s best-practice.

sodapopcan

sodapopcan

And I knew that you knew that I knew that you knew that I… wait what? :upside_down_face:

Hey, that’s a good reason!

I’m too tired to suggest any of the truly awful ideas I have around this :sweat_smile: @regex.sh’s suggestion isn’t bad, though. It sounds more and more like you may just want Surface. It already has collocated JS hooks.

kccarter

kccarter OP

Inspired by this blog post showing how to compile Markdown formatted blog posts into a Module, we came up with the following for minifying Javascript. Curious if there are obvious ways we could improve this as we’re not Elixir experts.

Minification is done by the Terser library and installed as a node module inside our assets folder.

Our HTML Module:

defmodule DemoWeb.RecordsHTML do
  use DemoWeb, :html

  @inline_js Path.expand(__DIR__)
             |> Path.join("templates_html/**/*.js")
             |> Path.wildcard()
             |> Enum.into(%{}, fn file ->
               {output, _} =
                 System.cmd(
                   "npx",
                   ["terser", "--toplevel", "--compress", "--mangle", "--", file],
                   cd: Path.join(File.cwd!(), "assets")
                 )

               {Path.basename(file), output}
             end)

  embed_templates "templates_html/*"

  @doc """
  embed_js
  """
  attr :filename, :string, doc: "The filename of the Javascript file to embed."

  def embed_js(assigns) do
    assigns = assign(assigns, files: @inline_js)

    ~H"""
    <script>
      <%= raw(Map.fetch!(@files, @filename)) %>
    </script>
    """
  end
end

A HEEX template:

<div>
  <div>Something that needs JS</div>
</div>

<.embed_js filename="form.js" />

Embedding JS with the script tag directly in HEEX template was problematic. Basic code formatting wasn’t working properly, for example. Using a JS “sidecar” file could be an acceptable trade-off.

Questions:

  • Are there ways we could convert this to a Macro along the lines of embed_templates?
  • Can we clean up the existing block of code for @inline_js in anyway?

Naturally, we might want to embed local Javascript files from other Modules.

ibarch

ibarch

Since you’ve started writing JS in a separate file anyways, have you considered wrapping your heex template into a custom element? You’ll get not only minification, but caching for free (well, at the cost of an extra initial HTTP request).

kccarter

kccarter OP

Not entirely sure I follow you. Our rendered HTML is standard heex templates that may include various function components.

Our primary desire for JS integration is to either “progressively enhance” some pages, or to write very lightweight “reactive” components.

kccarter

kccarter OP

For anyone coming across this in the future, we had fun building a working prototype that came close to achieving our original idea with the caveat that the JavaScript was in a sidecar file.

But in the end, it became obvious that the obscurity of such an implementation just wasn’t worth it.

Our JavaScript is now where it should be and our heex templates now just invoke a single line of inline JavaScript to pass a DOM element to an “enhancing” function, somewhat akin to how JS frameworks render a root component into a given element.

ibarch

ibarch

Here’s an example to illustrate what I was suggesting in my previous comment.

Create a heex component wrapped into a custom element <my-button>:

attr :name, :string, required: true

def my_button(assigns) do
  ~H"""
  <my-button name={@name}>
    <button>Click me</button>
    <p>Will change</p>
  </my-button>
  """
end

Define this element somewhere in assets/scripts/custom_elements/my_button.js:

class MyButton extends HTMLElement {
	get name() {
		return this.getAttribute("name");
	}

	connectedCallback() {
		this.button = this.querySelector("button");
		this.paragraph = this.querySelector("p");

		this.button.addEventListener("click", () => {
			this.paragraph.textContent = `Hello ${this.name}`;
		});
	}
}

customElements.define("my-button", MyButton);

Load this script in layouts/root.html.heex:

<script type="module" src={~p"/scripts/custom_elements/my_button.js"}></script>

If you want to make your components truly isolated Declarative Shadow DOM is now widely supported.

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
apz
I’m new to elixir and just tried to install the elixirLS extension for VScode(ium) and it is throwing some errors that I would like help ...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews