Using JS onclick function from assets directory

Hi all,

I’m quite new to Phoenix liveview and certain aspects are still confusing to me.
I’m testing out tailwind on Phoenix liveview.

What I have here is an html calling the searchHandler function from the script tag and it works without a problem.

<div class="px-6 h-full flex items-center justify-center border-l border-gray-300 text-gray-400 flex items-center">
  <input type="text" class="bg-transparent focus:outline-none text-xs w-0 transition duration-150 ease-in-out" placeholder="Type something..." />
  <svg onclick="searchHandler(this)" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search cursor-pointer" width="28" height="28" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <path stroke="none" d="M0 0h24v24H0z" />
    <circle cx="10" cy="10" r="7" />
    <line x1="21" y1="21" x2="15" y2="15" />
  </svg>
</div>


<script>

  function searchHandler(element) {
    let Input = element.parentElement.getElementsByTagName("input")[0];
    Input.classList.toggle("w-0");
  }

</script>

I was wondering if it’s possible to move the function from inside script tag into the app.js inside the assets directory and have the html execute the onlick function of the JS from the assets directory.

If it is possible, what are the necessary steps to achieve this goal?

It’s certainly possible. What have you already tried?

Yes but be aware, your live view will re-render the dom if the state change. So every changes made by JS will be undone.

that’s kinda the thing. I have no idea how to start. I tried reading online about the solution to start but JS interoperability is quite confusing for me.
I was hoping to get a solution for this question, so I might be able to understand how a custom JS can interact with phoenix liveview.

Have you seen AlpineJS? Combine Phoenix LiveView with Alpine.js - Tutorials and screencasts for Elixir, Phoenix and LiveView

1 Like

yes, look at Alpine. At first it seems like overhead to use a framework if you need just some JS but

  • AlipineJS is made for systems like LiveView - with some magic (“onBeforeElUpdated”, see below) it just works
  • it takes you by the hand, for me, also having “no idea where to start” it was a great help.

this is all you have to do:

petal/assets$ npm install alpinejs

add some magic to app.js

petal/assets$ git diff HEAD js/app.js
 ...
 import "../css/app.css"
+import Alpine from "alpinejs";
 ...
-let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })
+let liveSocket = new LiveSocket("/live", Socket, {
+   params: { _csrf_token: csrfToken },
+   dom: {
+   	onBeforeElUpdated(from, to) {
+       	if (from.__x) {
+           	Alpine.clone(from.__x, to);
+            }
+       },
+   },
+}
+)

but also look at the tutorial slouchpie has posted and for alpine itself I think this is a good start:

and just one more hint, first thing I was stuck on: when you need to work on the x-data just define a function inside that object.

2 Likes

Instead of reaching for something like alpine, you should instead be looking into Hooks. Basically the issue you’re having is that the onclick handler is being registered the first time that you load the page but when the page updates it is cleared. To solve this you need a hook to basically add the onclick behaviour every time that the dom element changes. You can read more about this here: JavaScript interoperability — Phoenix LiveView v0.15.4

I heard using hooks would be the solution to my problem but the thing is, getting it to actually work is quite confusing for me. I was wondering if it’s possible for someone to demonstrate how it would work using the example that I have. It would clarify the necessary steps required to solve this problem.

What I’ve done:

I’ve added hooks into let liveSocket
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})

Created a hook callback object

let Hooks = {}
Hooks.searchHandler() {
 mounted() {
    let Input = element.parentElement.getElementsByTagName("input")[0];
    Input.classList.toggle("w-0");
 } 
}

Replace onlick with phx-hook

<svg phx-hook="searchHandler(this)" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search cursor-pointer" width="28" height="28" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <path stroke="none" d="M0 0h24v24H0z" />
    <circle cx="10" cy="10" r="7" />
    <line x1="21" y1="21" x2="15" y2="15" />
  </svg>

I know this is horribly wrong but this is my understand from read the documents and reading other sources online. I’m really new at this and I need an example of this solution.

So you’re actually really close which is why I had asked earlier what you had tried. So what you are missing here is that the hook is supposed to be an object, not a function as you have currently declared it. This object has several lifecycle hooks it can implement, as well as some properties automatically available to it. You declare this hook object like so

let Hooks = {}
Hooks.SearchHandler = {
  mounted() {
    // Code to run when the dom element is mounted
 } 
}

so as you can see it’s pretty close to what you had. Then, in your markup you only need to pass the name of the hook, not call it as you attempted to do. So the full markup would look like

<svg phx-hook="SearchHandler" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search cursor-pointer" width="28" height="28" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" />
  <circle cx="10" cy="10" r="7" />
  <line x1="21" y1="21" x2="15" y2="15" />
</svg>

Then we need to fill out the mounted function. Since your original implementation registered an onclick handler, let’s do that as well

mounted() {
  this.el.addEventListener("click", () => {
    let Input = this.el.parentElement.getElementsByTagName("input")[0];
    Input.classList.toggle("w-0");
  })
}

Here you can see that we have access to this.el which refers to the element that you registered the hook on. If you check the docs you can see the full list of attributes as well as callbacks you can use. This should be enough to replicate the functionality you had previously. I have not tested it, but it should get you started at least.

2 Likes

I really appreciate your time in viewing my issue.

It still doesn’t seem to work for me. Right now I’m not sure if I’m doing something wrong or I’m missing something but following the steps you’ve provided should make things easier.

Okay, I’m happy to try and help you some more but I’ll need more information than “it doesn’t work”. What are you seeing? If you add some console log statements to the hook, are they called? Is it called when you expect it to be called? Steps like these will help us help you, but it will also help you to see what’s going on.

1 Like

The problem is that nothing is showing, not the JS function or any kind of error. Is there a way where I can get the terminal to show me something?

What I’ve done so far.

Declared hook object:

let Hooks = {}
mounted() {
  this.el.addEventListener("click", () => {
    let Input = this.el.parentElement.getElementsByTagName("input")[0];
    Input.classList.toggle("w-0");
  })
}

Added hooks into let liveSocket:

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

Corrected phx-hook callback:

<svg phx-hook="SearchHandler" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search cursor-pointer" width="28" height="28" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" />
  <circle cx="10" cy="10" r="7" />
  <line x1="21" y1="21" x2="15" y2="15" />
</svg>

I have no idea how to verify what the problem is, that’s why I replied with “it doesn’t work”. I don’t know whether or not I’m missing something or I did something wrong. Besides these three changes, I haven’t touched anything.

You have not declared the hooks object correctly based on the code that you shared here. You need to create an object and assign it to the key SearchHandler in the Hooks object. Like so:

let Hooks = {}
Hooks.SearchHandler = {
  mounted() {
    this.el.addEventListener("click", () => {
      let Input = this.el.parentElement.getElementsByTagName("input")[0];
      Input.classList.toggle("w-0");
    })
  } 
}

As for verifying what went wrong in the future, as I suggested you can add some log statements to make sure that the code is being run. If you added a log call to the mounted function and it didn’t print you can be sure that the mounted function is never called. Then you can work on figuring out why it’s not running.

1 Like

Oops, I made a typo on my last comment, I meant to type…

Declared hook object:

let Hooks = {}
Hooks.SearchHandler = {
  mounted() {
    this.el.addEventListener("click", () => {
      let Input = this.el.parentElement.getElementsByTagName("input")[0];
      Input.classList.toggle("w-0");
    })
  } 
}

as for using console.log to debug the function; it seems like the mount is not being called or even recognized at all. Literally has nothing in the console log.

What file is this code in? What happens if you put a console log before the hooks are declared?

I have the function inside the…

assets/js/app.js

let Hooks = {}
Hooks.SearchHandler = {
  mounted() {
    this.el.addEventListener("click", () => {
      let Input = this.el.parentElement.getElementsByTagName("input")[0];
      Input.classList.toggle("w-0");
    })
  } 
}

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

as for your question: What happens if you put a console log before the hooks are declared?

I don’t think I understand that question. Can you give me an example of what I should do?

Yeah by that I meant just literally before this line, like so

console.log('app.js is running') 
let Hooks = {} 

I also just reread the docs and noticed that you need a unique ID on the element which has the hook. So just a normal HTML ID attribute which must be unique on the page.

Yep, in the console log returned the string ‘app.js is running’, so I’m guessing the problem is not in the file but the function itself.

And when you say a unique ID, do you mean something like this…

<svg id="search-handler" phx-hook="SearchHandler" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search cursor-pointer" width="28" height="28" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" />
  <circle cx="10" cy="10" r="7" />
  <line x1="21" y1="21" x2="15" y2="15" />
</svg>

Am I supposed to defined the unique ID somewhere?

Nope just like that. Do you have more than one of these svgs on the page?