Phoenix with LiveView - Javascript in html.leex Templates

Hey Folks,

Finishing my first Phoenix/LiveView web app and about to deploy. I’ve got to say I’m really impressed with the DOM patching and LiveView in general. Really excited to see how this tech evolves. I had a few questions regarding Javascript and best practices.

For the most part, LiveView has eliminated the majority of my client side code but there were a few places where I had to write Javascript. I’ve got a reusable LiveComponent that acts as a top navigation on certain pages. The titles are implemented with an SVG animation that utilizes the snapsvg-cjs npm module. I’ve installed the module, and added the animation code to the bottom of app.js:

import Snap from "snapsvg-cjs";

function play(title) {
  var l = Snap('#logo');
  l.clear();

  setTimeout( function() {
    var logoTitle = title;
    var logoRandom = '';
    var logoTitleContainer = l.text(0, '98%', '');
    var possible = "-+*/|}{[]~\\\":;?/.><=+-_)(*&^%$#@!)}";
    logoTitleContainer.attr({
      fontSize: 150,
      fontFamily: 'Dosis',
      fontWeight: '600'
    });

    function generateRandomTitle(i, logoRandom) {
      setTimeout( function() {
        logoTitleContainer.attr({ text: logoRandom });
      }, i*70 );
    }

    for( var i=0; i < logoTitle.length+1; i++ ) {
      logoRandom = logoTitle.substr(0, i);
      for( var j=i; j < logoTitle.length; j++ ) { 
        logoRandom += possible.charAt(Math.floor(Math.random() * possible.length)); 
      }
      generateRandomTitle(i, logoRandom);
      logoRandom = '';
    }

  }, 500 );
}

I then pass the function to the .html.leex template via window.navAnimation = play('Test Title') and in a <script> tag inside the template, I assign it to the window.onload event. I’ve seen a lot of conversation surrounding Javascript and using npm modules with webpack and inside .html.leex templates, but all solutions discussed feel just as hacky as the way I’ve done it here. So with that said I have a few questions:

  1. First and foremost, is there a way to do text animations without javascript? I.e. is there a preexisting LiveView or Phoenix functionality that can do what I’m trying to do without passing things through window or even using javascript at all?
  2. If not, is there a cleaner more modular way to do this with javascript?
  3. If this is the cleanest/best practice/common way to do this, I can’t seem to pass in a method parameter inside the .html.leex template. Trying to set the title of the play(title) function inside the template results in this client side error: window.onload = window.navAnimation('Test Title');Uncaught TypeError: window.play is not a function. What’s wrong with my syntax, please explain?

Thanks,
Scott

Have you read about LiveView Hooks?

This might be what you are looking for.

2 Likes

I’ll take a look and let you know, thanks.

@shamanime that’s exactly what I’m looking for! I’m having an issues getting the payload though, was wondering if you could lend a hand.

I set up the event push within the update/2 method inside the LiveComponent:

  def update(assigns, socket) do
    Logger.debug "Assigns title: #{assigns.title}"
    {:ok, push_event(socket, "title", %{title: assigns.title})}
  end

This produces the expected value for assigns.title. I then handle the event in the client code in app.js:

let Hooks = {}
Hooks.AnimateTitle = {
  mounted() {
    this.handleEvent("title", ({title}) => animateTitle(title))

    function animateTitle(title) {
      // logic to perform animation
    }
  }
}

I know that the event’s being handled because when I stub the title out in the event handler this.handleEvent("title", animateTitle("Test Title Stub")) the animation works as expected and is called when the event is pushed.

What am I missing here?

Thanks,
Scott

Hey!

I created the repo shamanime/live_view_hooks and added 3 examples for you to browse.

The first one is similar to your code.
The second one sends a message to the LiveComponent to fetch the new title when it mounts.
The third one doesn’t exchange messages between the Component and the Hook. The Hook will read the needed value from the HTML.

This commit has all the code, hopefully it helps you find your way!

3 Likes

Thanks @shamanime! Your solution helped to debug my code and provided as a working reference.

So FYI anyone experiencing issues with hooks and Javascript callbacks not working, specifically when using the following notation this.handleEvent("beginTitleAnimation", ({title}) => this.animateTitle(title)). Make sure that both the HTML component inside the LiveComponent <p id="id-for-html-element" phx-hook="HookYouAreCalling"=>The HTML component triggering the hook</p> and the LiveComponent its self <%= live_component @socket, LiveComponentWithMarkup, id: "id-for-live-component"%> both have IDs. The IDs don’t need to be the same but my hooks didn’t behave correctly if both IDs weren’t there.

Scott