kccarter

kccarter

Can you minimize Javascript inside a heex template?

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.

First 10 of 11 Posts Switch mode

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?

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
silverdr
Using Phoenix.LiveView.TagEngine as an EEx.Engine is deprecated! To compile HEEx, use Phoenix.LiveView.TagEngine.compile/2 instead. Sta...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
michallepicki
I am using Oban and occasionally, shortly after a deployment, a handful of jobs can fail because of dependency on other parts of the syst...
New

Other Trending Topics Top

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
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New

We're in Beta

About us Mission Statement