Can we get support for colocating shared JS constants with a group of otherwise colocated JS components?

@chrismccord @josevalim @steffend

Been using colocated JS for a while and I keep on stumbling on the same limitation: there is no way (at least known to me) to colocate JS constants with a group of custom elements operating together as a whole (naturally, each defined is in its own file/script module).

I managed to import them (from one script module into another) but they can only be used from connectedCallback on. They don’t exist yet (synchronously) right after the import statement nor can they be used directly or via a function call in the Lit decorators like @provide, @consume and friends, i.e. they cannot be used as constants.

I know it works by simply moving those constants to a .js file and saving it in assets, but that’s no longer a “physical” colocation and those constants are of no concern to the rest of the app.

Is there any chance this could be somehow added to Phoenix/LiveView?

Thanks

You can do something like this:

defmodule MyAppWeb.SomeComponent do
  @doc false
  def __colocated_constants__(assigns) do
    ~H"""
    <script :type={Phoenix.LiveView.ColocatedJS} name="SharedFoo" key="constants">
      const constants = {
        FOO: "foo",
        BAR: "bar",
      }
      export default constants;
    </script>
    """
  end

  def my_component(assigns) do
    ~H"""
    <script :type={Phoenix.LiveView.ColocatedHook} name=".MyHook">
      import { constants } from "phoenix-colocated/my_app";

      export default {
        mounted() {
          this.el.innerText = JSON.stringify(constants);
        }
      }
    </script>

    <div id="some-id" phx-hook=".MyHook"></div>
    """
  end
end

In this example, the constants are available as constants.SharedFoo:

// constants
{"SharedFoo":{"FOO":"foo","BAR":"bar"}}

PS: No need to ping us individually, I’m subscribed to the Proposals section.

3 Likes

In your example you successfully use the constants from within the mounted() callback. That’s akin to my successfully using it in a custom element’s connectedCallback(), but that’s too late, because I need them available by the time a constant is defined in the other module.

For the sake of simplicity, I’ll show it on your example:

def my_component(assigns) do
    ~H"""
    <script :type={Phoenix.LiveView.ColocatedHook} name=".MyHook">
      import { constants } from "phoenix-colocated/my_app";
  
      const myFoo = constants.FOO; // I need it already here, not in the `mounted()`

      export default {
        mounted() {
          this.el.innerText = JSON.stringify(constants);
        }
      }
    </script>

    <div id="some-id" phx-hook=".MyHook"></div>
    """
  end

Deal.

You can use the constants as soon as they are imported. There’s no magic related to mounted. At the end of the day, colocated JS is just JavaScript files in a folder, treated like any other JS by esbuild.

That may be so when the script modules are in the same file, but it’s certainly not when they are in separate html.heex files.

I see the problem now (note that it would have been much easier if you posted the exact kind of error you’re seeing). Because of the way the JS files are bundled by esbuild, the individual modules are put before the code that initializes the key variables.

The solution to that is to use a different manifest for constants:

  @doc false
  def __colocated_constants__(assigns) do
    ~H"""
    <script :type={Phoenix.LiveView.ColocatedJS} name="SharedFoo" manifest="constants.js">
      const constants = {
        FOO: "foo",
        BAR: "bar",
      }
      export default constants;
    </script>
    """
  end

  def my_component(assigns) do
    ~H"""
    <script :type={Phoenix.LiveView.ColocatedHook} name=".MyHook">
      import constants from "phoenix-colocated/my_app/constants";

      console.log(constants.SharedFoo.FOO);

      export default {
        mounted() {
          this.el.innerText = JSON.stringify(constants.SharedFoo);
        }
      }
    </script>

    <div id="some-id" phx-hook=".MyHook"></div>
    """
  end
2 Likes

Thank you. Haven’t tried this one yet. Will be back if issues.

PS. No A"I" managed to find a solution.

Btw, I wasn’t receiving any build errors - just that the constant was undefined right after the import (which I pointed out in the original post) - being unavailable synchronously.

Yeah it builds just fine, but I assume you got an exception in the browser like

Uncaught TypeError: can't access property "SharedFoo", imp_mnxw443umfxhi4y is undefined

That would already have put me on the right track :slight_smile:

Anyway, please report back if you find any other issues!

1 Like

Yep, it works! Thank you so much for this one. You’ve got a lunch from me if you ever come visit Pula, Croatia (or any other place in the region of Istria).

1 Like