Reactivity in hologram or, why do my buttons don't do anything

Hello there,

I am trying to understand reactivity in hologram.

I’ve create a simple component calculator.ex that I am calling like this <Calculator cid="calculator" value="20" />

This is my component definition:

defmodule Calculator do
  use Hologram.Component

  prop :value, :integer

  def template do
    ~HOLO"""
    <button class="btn" $click="plus">Plus</button>
    <button class="btn" $click="minus">Minus</button>

    <p>The value is {@value}</p>
    """
  end

  def init(props, component) do
    put_state(component, :value, props.value)
  end

  def action(:plus, _params, component) do
    put_state(component, :value, component.state.value + 1)
  end

  def action(:minus, _params, component) do
    put_state(component, :value, component.state.value - 1)
  end
end

However, the displayed value doesn’t change when I hit any of the buttons.

What am I doing wrong?

I noticed that you initialized the value as a string, but you’re treating it as an integer using the plus/minus operators.

The runtime prop type validation isn’t working yet, which might be misleading you into thinking the value gets automatically converted to an integer. Is that what’s happening?

1 Like

Hello Bart,

I have two GET errors when I launch mix phx.server

[info] GET /hologram/runtime-c50354c9a5cd66be8e0af163accbc9ab.js
[debug] ** (Phoenix.Router.NoRouteError) no route found for GET /hologram/runtime-c50354c9a5cd66be8e0af163accbc9ab.js (HologramTutorialWeb.Router)
    (hologram_tutorial 0.1.0) deps/phoenix/lib/phoenix/router.ex:465: HologramTutorialWeb.Router.call/2
    (hologram_tutorial 0.1.0) lib/hologram_tutorial_web/endpoint.ex:1: HologramTutorialWeb.Endpoint.plug_builder_call/2
    (hologram_tutorial 0.1.0) deps/plug/lib/plug/debugger.ex:155: HologramTutorialWeb.Endpoint."call (overridable 3)"/2
    (hologram_tutorial 0.1.0) lib/hologram_tutorial_web/endpoint.ex:1: HologramTutorialWeb.Endpoint.call/2
    (phoenix 1.8.1) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
    (bandit 1.8.0) lib/bandit/pipeline.ex:131: Bandit.Pipeline.call_plug!/2
    (bandit 1.8.0) lib/bandit/pipeline.ex:42: Bandit.Pipeline.run/5
    (bandit 1.8.0) lib/bandit/http1/handler.ex:13: Bandit.HTTP1.Handler.handle_data/3
    (bandit 1.8.0) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
    (bandit 1.8.0) lib/bandit/delegating_handler.ex:8: Bandit.DelegatingHandler.handle_info/2
    (stdlib 5.2.3.3) gen_server.erl:1095: :gen_server.try_handle_info/3
    (stdlib 5.2.3.3) gen_server.erl:1183: :gen_server.handle_msg/6
    (stdlib 5.2.3.3) proc_lib.erl:241: :proc_lib.init_p_do_apply/3

[info] GET /hologram/page-f4d5cc35aef68e063cf284312c06e078.js
[debug] ** (Phoenix.Router.NoRouteError) no route found for GET /hologram/page-f4d5cc35aef68e063cf284312c06e078.js (HologramTutorialWeb.Router)
    (hologram_tutorial 0.1.0) deps/phoenix/lib/phoenix/router.ex:465: HologramTutorialWeb.Router.call/2
    (hologram_tutorial 0.1.0) lib/hologram_tutorial_web/endpoint.ex:1: HologramTutorialWeb.Endpoint.plug_builder_call/2
    (hologram_tutorial 0.1.0) deps/plug/lib/plug/debugger.ex:155: HologramTutorialWeb.Endpoint."call (overridable 3)"/2
    (hologram_tutorial 0.1.0) lib/hologram_tutorial_web/endpoint.ex:1: HologramTutorialWeb.Endpoint.call/2
    (phoenix 1.8.1) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
    (bandit 1.8.0) lib/bandit/pipeline.ex:131: Bandit.Pipeline.call_plug!/2
    (bandit 1.8.0) lib/bandit/pipeline.ex:42: Bandit.Pipeline.run/5
    (bandit 1.8.0) lib/bandit/http1/handler.ex:13: Bandit.HTTP1.Handler.handle_data/3
    (bandit 1.8.0) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
    (bandit 1.8.0) lib/bandit/delegating_handler.ex:8: Bandit.DelegatingHandler.handle_continue/2
    (stdlib 5.2.3.3) gen_server.erl:1085: :gen_server.try_handle_continue/3
    (stdlib 5.2.3.3) gen_server.erl:995: :gen_server.loop/7
    (stdlib 5.2.3.3) proc_lib.erl:241: :proc_lib.init_p_do_apply/3

The same errors are visible in the browser console.

The double quotes around the 20 are automatically added by Zed in my home_page.holo. When I remove them (I have to move the template into home_page.ex to do that because Zed doesn’t pick up the HTML inside the HOLO Sigil), I get a new error:

Compiling 1 file (.ex)

== Compilation error in file app/pages/home_page.ex ==
** (Hologram.TemplateSyntaxError) 

Reason:
Unknown reason.

Hint:
Please report that you received this message here: https://github.com/bartblast/hologram/issues
and include a markup snippet that will allow us to reproduce the issue.

="calculator" value=20 />
                    ^

status = :attribute_assignment

token = {:string, "20"}

context = %Hologram.Template.Parser.Context{attribute_name: "value", attribute_value: [], attributes: [{"cid", [text: "calculator"]}], block_name: nil, delimiter_stack: [], node_type: :attribute, prev_status: :attribute_name, processed_tags: [text: "\n", end_tag: "h1", text: "Home Page", start_tag: {"h1", []}], processed_tokens: [symbol: "=", string: "value", whitespace: " ", symbol: "\"", string: "calculator", symbol: "\"", symbol: "=", string: "cid", whitespace: " ", string: "Calculator", symbol: "<", whitespace: "\n", symbol: ">", string: "h1", symbol: "</", string: "Page", whitespace: " ", string: "Home", symbol: ">", string: "h1", symbol: "<"], raw?: false, script?: false, tag_name: "Calculator", token_buffer: []}

    (hologram 0.6.3) lib/hologram/template/parser.ex:1064: Hologram.Template.Parser.raise_error/5
    (hologram 0.6.3) lib/hologram/template.ex:22: Hologram.Template.dom_ast/1
    (hologram 0.6.3) lib/hologram/template.ex:40: Hologram.Template.build_holo_sigil_ast/1
    (hologram 0.6.3) expanding macro: Hologram.Template.sigil_HOLO/2
    (hologram_tutorial 0.1.0) app/pages/home_page.ex:9: HologramTutorial.HomePage.template/0
[watch] build started (change: "../_build/dev/phoenix-colocated/hologram_tutorial/index.js")
[watch] build finished

Hi @Starlet9334,

You need to place the Hologram router plug before the Phoenix router plug in your endpoint module.

To pass an integer as a prop value, wrap it in curly braces to treat it as an expression (similar to React and other frontend frameworks).
<Calculator cid="calculator" value={20} />

Thank you! It still doesn’t work, even after wrapping the value in curly braces.

I do have the hologram router before the Phoenix one:

defmodule HologramTutorialWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :hologram_tutorial

  # The session will be stored in the cookie and signed,
  # this means its contents can be read but not tampered with.
  # Set :encryption_salt if you would also like to encrypt it.
  @session_options [
    store: :cookie,
    key: "_hologram_tutorial_key",
    signing_salt: "n/6zNTc1",
    same_site: "Lax"
  ]

  socket "/live", Phoenix.LiveView.Socket,
    websocket: [connect_info: [session: @session_options]],
    longpoll: [connect_info: [session: @session_options]]

  # Serve at "/" the static files from "priv/static" directory.
  #
  # When code reloading is disabled (e.g., in production),
  # the `gzip` option is enabled to serve compressed
  # static files generated by running `phx.digest`.
  plug Plug.Static,
    at: "/",
    from: :hologram_tutorial,
    gzip: not code_reloading?,
    only: HologramTutorialWeb.static_paths(),
    only: ["hologram" | HologramTutorialWeb.static_paths()]

  # Code reloading can be explicitly enabled under the
  # :code_reloader configuration of your endpoint.
  if code_reloading? do
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
    plug Phoenix.LiveReloader
    plug Phoenix.CodeReloader
    plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hologram_tutorial
  end

  plug Phoenix.LiveDashboard.RequestLogger,
    param_key: "request_logger",
    cookie_key: "request_logger"

  plug Plug.RequestId
  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()

  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, @session_options
  plug Hologram.Router
  plug HologramTutorialWeb.Router
end

Mind posting the repo link? Would be much easier to help if I can see the whole project.

I could replicate the error. I just created a basic project and added the component.
If I add the init/3 function to the component, it seems to work:

  def init(props, component, server) do
    component = put_state(component, :value, props.value)

    {component, server}
  end

but with only init/2 I have the same error as @Starlet9334

1 Like

Thanks, I’ll look at it!

Btw, this:

def init(props, component, server) do
  component = put_state(component, :value, props.value)

  {component, server}
end

Can be just this:

def init(props, component, _server) do
  put_state(component, :value, props.value)
end

you don’t have to return server struct if it’s not modified :slight_smile:

3 Likes

Just to clarify - are you able to replicate the routing-related error specifically, or a different issue?

To ensure we’re all on the same page, could either @Starlet9334 or @phcurado provide a minimal reproduction repository? You’re welcome to fork the skeleton app as a starting point: Hologram Skeleton

That way I can reproduce the exact issue you’re both experiencing and work on a solution.

Here you go: Starlet9334/hologram-tutorial - Codeberg.org

Thank you!

There a few problems in the code:

1)
You’ve got duplicate :only option in the Plug.Static config:

plug Plug.Static,
  at: "/",
  from: :hologram_tutorial,
  gzip: not code_reloading?,
  only: HologramTutorialWeb.static_paths(),
  only: ["hologram" | HologramTutorialWeb.static_paths()]

should be:
only: ["hologram" | HologramTutorialWeb.static_paths()]

2)
You can’t use Phoenix routing-related stuff (like ~p sigil) in Hologram templates.
instead:
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
use:
<link rel="stylesheet" href={asset_path("assets/css/app.css")} />

3)
Don’t use Phoenix CSRF tokens inside Hologram layouts.
remove:
<meta name="csrf-token" content={get_csrf_token()} />
CSRF protection is managed automatically by Hologram, you don’t have to think about it at all.

4)
You’re layout is broken, you need to use html as the root element, and nest header and footer elements inside the body element.

instead:

<head>
  <title>{@page_title}</title>
  <Hologram.UI.Runtime />
  <link rel="stylesheet" href={asset_path("assets/css/app.css")} />
</head>
<header>Header with menu</header>
<body>
  <slot />
</body>
<footer>Footer</footer>

use:

<!DOCTYPE html>
<html>
  <head>
    <title>{@page_title}</title>
    <Hologram.UI.Runtime />
    <link rel="stylesheet" href={asset_path("assets/css/app.css")} />
  </head>
  <body>
    <header>Header with menu</header>
    <slot />
    <footer>Footer</footer>
  </body>
</html>

5)
When initializing Hologram components server-side (e.g. when using the given component in a page template - since pages are always loaded from the server) you need to use init/3 instead of init/2.


For #5 the DX could be better and Hologram could allow to use init/2 in cases where there is no init/3 defined - added that to the backlog.

2 Likes

This was the solution. I changed

def init(props, component) do
    put_state(component, :value, props.value)
  end

to

def init(props, component, _server) do
    put_state(component, :value, props.value)
  end

and the buttons now work as expected.

Thank you for the quick help, on a Sunday no less!

1 Like

Since fixing #5 alone solved everything, the repo you shared was probably different from your original code. No worries! Just want to make sure it’s clear - for you and anyone else reading this with similar issues: each of those 5 issues will cause Hologram to break in different ways, so if any are in your actual codebase, they’ll need addressing.

4 Likes