Template event (click) does not fire after document changed


#1

I’m using Blaze template and have encountered a strange problem:

HTML

<template name="test">
  <div class="start"></div>
  <div>
    <button type="button" onclick="btn1Clk()">
          Btn 1
    </button>
  </div>
  <div>
    <button type="button" class="js-test-btn-2">
          Btn 2
    </button>
  </div>
  <div class="stop"></div>
  <div>
    <button type="button" class="js-test-btn-3">
          Btn 3
    </button>
  </div>
</template>

JS

Template.test.events({
    'click .js-test-btn-2': function(event){
        alert("btn-2");
    },
    'click .js-test-btn-3': function(event){
        $(".start").nextUntil(".stop").wrapAll("<div style='background-color: blue;'></div>");
    },
});

btn1Clk = function(){
    alert("btn-1 ");
}

After btn-3 has been clicked, the click event for btn-2 (template event) no longer worked, while btn-1 (old style js event) still worked. I would be very grateful if somebody tell me why …

By the way, is there a place similar to codepen.io or jsfiddle.net that works for templates?


#2

I guess it is because your additional wrapper destroys the event handlers Blaze has setup.

In general, your shouldn’t use jquery to manipulate the DOM, especially when it gets to DOM nodes that contain Blaze nodes. Instead, use Blaze’s conditional tags to achieve the same result.

In your sample, I would add the additional wrapper DIV on first render and just make the style (or class) conditional.

PS: onClick handlers shouldn’t be used either. But I guess this was just for demonstration purposes.


#3

Thanks waldgeist for your answer.

Yes, the onClick() handler was just for demonstration. I also guessed Blaze handlers were destroyed during jquery DOM manipulation APIs, but I was not sure if it was just technically impossible or a Blaze bug?

I did tried Blaze conditional tags. In my first attempt, injecting uncompleted/partial html fragment by Blaze conditional tags didn’t work:

{{#if stylingBegin}}
<div style='bla'> <!-- open div -->
{{/if}}
  <btn1/>
  <btn2/>
{{#if stylingEnd}}
</div> <!-- close div -->
{{/if}}
<btn3/>

While it makes sense that jQuery DOM manipulating APIs should not allow uncompleted DOM nodes to be inserted into doc, I think it’s kind of pity that Blaze couldn’t handle the above. I guess Blaze could have a staging area to store the doc fragment related to the template being processed, and a template can be seen as a closure for that doc fragment where the confirmation that the doc fragment is completed or not can be made in the final processing step for that template.

For this example it can still work like this:

{{#if styling}}
<div style='bla'> <!-- open div -->
  <btn1/>
  <btn2/>
</div> <!-- close div -->
{{else}}
<btn1/>
<btn2/>
{{/if}}
<btn3/>

… where I may further sub-template the repeated block <btn1/> <btn2/> to make it more manageable. The same attempt however does not work for the following case, which is the real case that I am on:
I defined the following template:

{{#each ListItems}}
  {{name}}
{{/each}}

to display a collection named ‘ListItems’ in this format:
list
In the desired view, certain adjacent items should be displayed in a box. I have the condition for the first item to be in the first box, the last item to be in the first box, the first item to be in the second box, the last item to be in the second box, and so on, where the number of items in boxes vary, and boxes are mixed with no-boxed items in the view as showed.

The following did not work for me because the problem of uncompleted html fragment mentioned above:

{{#each listItems}}
  {{#if boxStart}}
    <div class='well'>
  {{/if}}
      {{name}}
  {{#if boxEnd}}
    </div>
  {{/if}}
{{/each}}

My best try so far is:

{{#each listItems}}
    <div id='{{boxStart}}'></div>
      {{name}}
    <div class='{{boxEnd}}'></div>
{{/each}}
{{deferStyling}}
Template.myTemplate.helpers({
  boxStart(){
        if ( someCond ){
            return "box-start-"+this._id;
        } else {
            return "";
        }
    },
    boxEnd(){
        if ( someCond ) {
            return "box-stop";
        } else {
            return "";
        }
    },
   deferStyling(){
        $('[id^="box-start-"]').each(function(idx){
            $(this).nextUntil(".box-stop").wrapAll("<div class='well'/>");
        }
   },
 });

I now got the view, but the events created for the items that are displayed inside the boxes do not work any more as showed in the first example.

What did I miss along the way? Is there a different trick to achieve this?


#4

You wouldn’t normally do this:

{{#if styling}}
<div style='bla'> <!-- open div -->
  <btn1/>
  <btn2/>
</div> <!-- close div -->
{{else}}
<btn1/>
<btn2/>
{{/if}}
<btn3/>

It’s much cleaner to do something like this:

<div class="{{style}}">
  <btn1/>
  <btn2/>
</div>
<btn3/>

and use a template helper to deliver the style:

Template.btns.helpers({
  style() {
    const styledClass = ... // some code to determine the class name
    return styledClass ? 'exciting' : 'boring';
  }
});

Where styledClass would be determined based on a reactive variable, so it would change dynamically.


#5

Thanks robfallows for your suggestion.
Yes, it’s the best for this construction.

I am still looking for a solution for the case that the number of elements that shall be styled is only known at run-time to display this view:

Where the template might look like this (or not???):

{{#each ListItems}}
  {{#if stylingBegin}}
      ???
  {{/if}}
    {{this}}
  {{#if stylingEnd}}
      ???
  {{/if}}
{{/each}}

when stylingStart and stylingEnd do not return true for the same {{this}}.


#6

The canonical way to address this in Blaze is to use nested templates:

<template name="list">
  <div class="list">
    {{#each doc in ListItems}}
      {{> showItem item=doc}}
    {{/each}}
  <div>
</template>

<template name="showItem">
  {{#with item}}
    <div class="{{style}}">
      {{anItemField}} {{anotherItemField}}
    </div>
  {{/with}}
</template>

Then in your showItem template helpers this.item gives you access to the complete document (item) currently being rendered - this.item is different for each template instance. That means that the style helper is specific to that one item.


#7

Thanks for your suggestion. But my problem was a bit different. It was not about styling fields of each items differently, it was about styling group of adjacent items differently, i.e some template that expands to:

<div class="style-for-item"> item-1 </div>
<div class="style-for-group">
    <div class="style-for-item"> item-2 </div>
    <div class="style-for-item"> item-3 </div>
    <div class="style-for-item"> item-4 </div>
</div>
<div class="style-for-item"> item-5 </div>
<div class="style-for-group"> 
    <div class="style-for-item"> item-6 </div>
    <div class="style-for-item"> item-7 </div>
</div>

Especially, “style-for-group” is a block level style, like a well (frame), which requires a div tag wraps around all the relevant items and not around each item.

Now I guess iterating through each element is absolutely not a way to achieve what I want, unless Blaze supported uncompleted html fragment (open div tag without closing and vice versa) which it doesn’t. Maybe I need to iterate each group of items:

{{#each groupOfItems}}
<div class="{{style_for_group}}">
    {{#each item in groupOfItems}}
        <div class="{{style_for_item}}">{{item}}</div>
    {{/each}}
</div>
{{/each}}

Then for the above example, my helper function groupOfItems() should return:
[item-1] for the first iteration
[item-2, item-3, item-4] for the second iteration
[item-5] for the third iteration
[item-6, item-7] for the fourth iteration

And say my ListItems is a mini mongo db collection or a reactive array whatever, I don’t know how the helper function looks like …


#8

I think you’re on the right lines now :slight_smile:.

Without knowing your data - what the docs in your collection look like - I can’t really help.


#9
var ListItems = new Meteor.Collection(null);

for (var i=1; i<=7; i++) {
    var type = (i==1 || i==5) ? "standalone" : "group";
    ListItems.insert({
        idx: i,
        name: "item-" + i,
        type: type,
    });
}

Then I want to display just document.name, ordered by idx, grouped format for adjacent items if their type is ‘group’. The actual collection is huge.