Search code examples
erlangnitrogen

Nitrogen wf:wire() does not work for dynamic content


I have 2 modules:

test2

  • Module create div element with a button id = wf:temp_id()
  • wire #alert{} event to it.
  • return these HTML elements so they could be used by other modules.

test

  • Module create page content with a button myBtn and postback message {btn_clicked,myBtn}
  • #alert{"myBtn clicked"} event to it,
  • add empty div with id: id_insert_here - to be used as a placeholder for the dynamic element
  • call test2:get_content("Static"), to add content from the module test2.

Then in event function, it use #wf:insert_after to add content from the test2:get_content("Dynamic").

Expected behavior:

  1. User click on myBtn,
  2. Alert message "myBtn clicked" appeared
  3. New element "Dynamic content" appeared This part is working.

  4. User click on button in "Static" section that was generated from the test2:get_content/1 function,

  5. Message "Btn_TEMP1" clicked appeared. So far everything is working as expected.

Now: 6. User click on button in "Dynamic" secion that was generated using test2:get_content/1 function, and added to the page using wf:insert_after() call.

  1. Expected behavior: Message "btn_TEMP2" clicked appeared.

Realized behavior - nothing happened.

wf:wire does not work.

Q: How to enure that wf:wire() works fo the elements that are added using wf:insert_XXX function?

%% -*- mode: nitrogen -*-
-module (test).
-compile(export_all).
-include_lib("../deps/nitrogen_core/include/wf.hrl").

main() -> 
    #template { file="./priv/templates/bare.html",
       bindings=[{'PageTitle',<<"Test">>}]}.

title() -> 
    #template{bindings = L} = main(),
    proplists:get_value('PageTitle',L).

body() ->
    Body = [
       #panel{class="w3-panel w3-green",
              body = 
                    [#p{text = "some text"},
                     #button {
                        id=myBtn,
                        text="myBtn",
                        postback={btn_clicked,myBtn}
                    },
                    #panel{id = id_insert_here}
                    ]}
     ],
    wf:wire(myBtn, #event{ type= click, actions = [ #alert{text="MyBtn clicked"} ]}),
    Body,
    Content = test2:get_content("Pre-compiled"),
    Body2 = [Body,Content],
    Body2.

event({btn_clicked,myBtn} = Event) ->
    io:format("Event: ~p",[Event]),
    Content = test2:get_content("Dynamic"),
    wf:insert_after(id_insert_here,Content);

event(Event) ->
    io:format(" Unexpected Event: ~p ",[Event]),
    ok.
%% end of module test

Module test2:

-module(test2).
-compile(export_all).
-include_lib("../deps/nitrogen_core/include/wf.hrl").

get_content(Title) ->
    MyId = wf:temp_id(),
    BtnText = io_lib:format("Btn_~p",[MyId]),
    Body = [
    #panel{class="w3-panel w3-yellow",
           body = 
                [#p{text = Title},
                 #button {
                    id=MyId,
                    text=BtnText
                    }]
        }],
    Msg = io_lib:format("Btn: ~p clicked",[MyId]),
    wf:wire(MyId, #event{ type= click, actions = [ #alert{text= Msg} ]}),
    Body.

Solution

  • Great Question, and one that needs a proper answer in the nitrogen docs.

    The short answer is that in test2:get_content, you should just replace wf:wire with wf:defer.

    The reasoning for it is a little longer, but it goes like this:

    In both situations (both static and dynamic), when test2:get_content is called it does the following in this order:

    1. Build some Nitrogen elements and store them in variables.
    2. Wire an action to one of the elements defined in (1)
    3. Return the previously made elements.

    In a static request (or rather, in nitrogen parlance, a first_request, since in Nitrogen, a "static request" refers to purely static content like requesting image files or whatever), this is perfectly fine. Static content is returned, placed into the page, and then any wired actions are inserted into the [[[script]]] section at the bottom of the template.

    In a dynamic request, however, the order of operations becomes important.

    The test2:get_content function you've defined does the same thing, but the function is evaluated and the content is getting returned to the wf:insert_after function, which then inserts the elements into the DOM.

    The bug resides subtly in the above paragraph, but I'll fit in the overall context to make it more obvious. With the dynamic request:

    1. The call to wf:insert_after (or wf:insert_before, or wf:update, or wf:replace or any of those) needs to test2:get_content to be evaluated first. So evaluate test2:get_content()
    2. Build some Nitrogen elements and store them in variables.
    3. Wire an action to one of the elements defined in (2)
    4. Return the previously made elements.
    5. wf:insert_after takes those returned elements and wires them to the page.

    What happens here is that because the wf:insert_after is effectively happening last, the wf:wire to the element goes nowhere. That is, the element wf:wire is trying to be attached to doesn't exist in the DOM yet.

    It's like this pseudocode:

    $('#my_new_element').click(function(){do_something}));
    $('#body').insert_bottom("<button id=my_new_element>click</button>");
    

    Using wf:defer in place of wf:wire ensures the wiring happens after the insert_XXX function evaluates.