How can I preserve a bootstrap dropdown state on Re-Render

1,285 views
Skip to first unread message

Rui Gonçalves

unread,
Feb 4, 2013, 8:12:25 AM2/4/13
to meteo...@googlegroups.com
Hi

I'm using bootstrap in my current project, and I'm hitting the wall with this simple problem.

I have a table and for each row a dropdown button that shows a dropdown window that has a form and below this form a list of "tasks".

So when I open the dropdown "window" to add a task in the form, then the submit event adds a new task into the collection.
Imidiatly after the dropdown is closed, because the row re-renders the open class of the dropdown is removed.

I tried playing around with preserve, but it seams it does not preserves class attributes.
Then I tried {{#isolate}} to only rerender the task list and leave all other things untouched, with no luck.
Then in desperate mode I tried also wrapping {{#constant}} on the parent template that contains the form, but sparks throws some errors, and all gets rerendered.

I relay loved if Spaks could leve untouched some parts of the template so UI states like dropdowns where left alone after rerenders.

For now I solved with a template function that saves the open state on a Session variable, but this definitely not the way to go as it adds extra garbage to the code and extra complexity.

Is there any way to solve this problem without the use of Session or functions?

Thanks for your replies

David Greenspan

unread,
Feb 4, 2013, 3:55:52 PM2/4/13
to meteo...@googlegroups.com
Hi Rui,

Can you show code?  There should be a way, but it's hard to know without seeing the code.  I'm also curious what the work-around looks like.

Thanks,
David



--
You received this message because you are subscribed to the Google Groups "meteor-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to meteor-talk...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Rui Gonçalves

unread,
Feb 5, 2013, 2:26:20 PM2/5/13
to meteo...@googlegroups.com
Here is a simple example that demonstrates the problem

tasks.html
----
<body>
  <div class="container">
    {{> dropdown}}
  </diV>
</body>

<template name="dropdown">
    <div class="btn-group">
      <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">Tasks ({{tasksCount}})<span class="caret"></span></a>
      <ul class="dropdown-menu">
        <li><input type="text" name="name" /></li>
        {{> dropdown_items}}
      </ul>
    </div>
</template>

<template name="dropdown_items">
      {{#each tasks}}
        <li>{{name}}</li>
      {{/each}}
</template>

tasks.js
----
Tasks = new Meteor.Collection("tasks");

if (Meteor.isClient){
    Template.dropdown.tasksCount = function(){
        return Tasks.find().count();
    };

    Template.dropdown.events({
        "change input" : function(ev){
            Tasks.insert({name : ev.target.value});
        }
    });


    Template.dropdown_items.tasks = function(){
        return Tasks.find();
    };
}

The {{tasksCount}} emulates my main problem, the dropdown closes after this value is invalidated.
My question is if it is possible isolate the dropdown temaplate to keep the dropdown open when tasksCount is invalidated?

The workaround adds an {{open}} state to the dropdown class and keeps the state in a Session variable.

David Greenspan

unread,
Feb 5, 2013, 8:27:36 PM2/5/13
to meteo...@googlegroups.com
In this case, you want an `{{#isolate}}` around the `<a class="btn dropdown-toggle" ...`.  Since that's where the invalidation is happening, #isolate confines it to that element.

In the code you pasted, I also found that clicking on the input field would hide the dropdown, but it was just Bootstrap behavior.  Binding a click handler on the input field and calling stopPropagation prevents it.

Hope this helps,
David


Rui Gonçalves

unread,
Feb 6, 2013, 6:14:21 AM2/6/13
to meteo...@googlegroups.com
Hi David

Huge thanks for your great help, that solved the entire issues..
I was using {{#isolate}} the wrong way, and isolating what did not needed to be isolated.
The docs should mention that {{#isolate}} must refer to elements and perhaps an example to illustrate the usage.

Here's the code on how it turned out

-----
<head>
  <title>Dropdown Test</title>
</head>


<body>
  <div class="container">
    {{> dropdown}}
  </div>
</body>

<template name="dropdown">
    {{#isolate}}<p>{{tasksCount}}</p>{{/isolate}}
    <div class="btn-group">
      {{#isolate}}<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">Tasks ({{tasksCount}})<span class="caret"></span></a>{{/isolate}}

      <ul class="dropdown-menu">
        <li><input type="text" name="name" /></li>
        {{> dropdown_items}}
      </ul>
    </div>
</template>

<template name="dropdown_items">
      {{#each tasks}}
        <li>{{name}}</li>
      {{/each}}
</template>

---------------------


Tasks = new Meteor.Collection("tasks");

if (Meteor.isClient){
    Template.dropdown.tasksCount = function(){
        return Tasks.find().count();
    };

    Template.dropdown.events({
        "click input" : function(ev){
            ev.stopPropagation();
        },

Rui Gonçalves

unread,
Feb 6, 2013, 1:28:10 PM2/6/13
to meteo...@googlegroups.com
Hi David

Been playing around with #isolate and it seams that in my application it fails, isolate does not play nice inside #each or #with blocks.

I updated the example to illustrate the problem, this time I was unable to get {{#isolate}} to work

----------------------------
<head>
  <title>Dropdown Test2</title>

</head>

<body>
  <div class="container">
    {{> main}}
  </div>
</body>

<template name="main">
    <table>
      <tbody>
        {{#each persons}}
        <tr>
          <td>{{> dropdown}}</td>
        </tr>
        {{/each}}
      </tbody>
    </table>
</template>



<template name="dropdown">
    <div class="btn-group">
      <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">Tasks<span class="caret"></span></a>

      <ul class="dropdown-menu">
        <li><input type="text" name="name" /></li>
        {{#isolate}}
        {{#each tasks}}
        <li>{{this}}</li>
        {{/each}}
        {{/isolate}}
      </ul>
    </div>
</template>

----------------

Persons = new Meteor.Collection("persons");

if (Meteor.isClient){
    Template.main.persons = function(){
        return Persons.find();

    };

    Template.dropdown.events({
        "click input" : function(ev){
            ev.stopPropagation();
        },
        "change input" : function(ev){
            Persons.update(this._id, {$push : { tasks : ev.target.value} });
        }
    });
} else {
    if (Persons.find().count() === 0){
        Persons.insert({name : "Alice", tasks : []});
        Persons.insert({name : "Bob", tasks : []});
    }

}

----------------------------------------------

Thanks again for your help

Rui Gonçalves

unread,
Feb 7, 2013, 6:53:32 AM2/7/13
to meteo...@googlegroups.com
I'm still trying to get my head around this, and here is were I'm now, please correct me if I'm wrong.

It seams that putting a #with or #each in the parent template makes it impossible to isolate changes on the child template.
The parent #with block invalidates all its children templates, #isolation or preserve is ignored because all the child block is being invalidated by its parent.

Still, I was hopping there was a way to preserve changes to the dropdown div. If I could preserve the class attribute from being overriden i could maintain the dropdown open.
But marking it as a {{#constant}} makes all its children unchangeable and if i add a new task, it does not appear in the tasks list.
I tried preserving the dropdown div in the hope it didn't override the class attribute but it does.
Isolating as I mentioned before did nothing.

Any ideas??

Thanks again

David Greenspan

unread,
Feb 7, 2013, 1:42:24 PM2/7/13
to meteo...@googlegroups.com
Hi Rui,

You're right, the problem now is the class attribute on the div that is being overwritten.

Bootstrap adds class "open" to the div, and when the DOM is re-rendered, the class is lost.

I don't have a good workaround for preserving classes added by third-party libraries, sorry.

-- David



Rui Gonçalves

unread,
Feb 8, 2013, 8:02:15 AM2/8/13
to meteo...@googlegroups.com
:(

I'll paste my workaround later, I did it using Session and keeping the current open element id, but this is an ugly ugly hack.
Basiclly it'll render 2 times the same thing besides of the added complexity.

Perhaps I'm not seeing the full picture, but shouldn't Sparks only update the reactive areas inside the template that actually changed?

In this case there was no real need to replace the row that changed but only the added <li> should be inserted.
If this was the behavior the open state was kept unaltered.

I'm sill a big fan of Meteor, but I needed to get this out of my chest :)

Rui Gonçalves

unread,
Feb 8, 2013, 2:01:28 PM2/8/13
to meteo...@googlegroups.com
As promised

-------------------------------
<head>
  <title>Dropdown Test</title>

</head>

<body>
  <div class="container">
    {{> main}}
  </div>
</body>

<template name="main">
    <table>
      <tbody>
      {{#each persons}}
        <tr>
          <td>{{> dropdown}}</td>
        </tr>
      {{/each}}
      </tbody>
    </table>
</template>

<template name="dropdown">
    <div class="btn-group {{#if isOpen}}open{{/if}}">

      <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">Tasks<span class="caret"></span></a>
        <ul class="dropdown-menu">
          <li><input type="text" name="name" /></li>
          {{#each tasks}}
          <li>{{this}}</li>
          {{/each}}
        </ul>
    </div>
</template>

-----------------------------


Persons = new Meteor.Collection("persons");

if (Meteor.isClient){
    Template.main.persons = function(){
        return Persons.find();
    };

    Template.dropdown.isOpen = function(){
        return Session.equals("open_dropdown", this._id);
    };

    Template.dropdown.events({
        "click div.btn-group" : function(ev,n){
            Session.set("open_dropdown", this._id);
        },
        "click input" : function(ev,n){

            ev.stopPropagation();
        },
        "change input" : function(ev){
            Persons.update(this._id, {$push : { tasks : ev.target.value} });
        }
    });

    Meteor.startup(function(){
        $(document).on('click.dropdown.data-api touchstart.dropdown.data-api',
            function(e){
                Session.set("open_dropdown", undefined);

            });
    });

} else {
    if (Persons.find().count() === 0){
        Persons.insert({name : "Alice", tasks : []});
        Persons.insert({name : "Bob", tasks : []});
    }
}

Thanks for your help

David Greenspan

unread,
Feb 10, 2013, 7:52:08 PM2/10/13
to meteo...@googlegroups.com
Hi Rui,

What's going on here is Meteor doesn't know exactly what has changed.  It knows when a person document has changed, based on the outer #each, so it recalculates all the relevant DOM.  Then, Spark tries to figure out what has changed in the DOM, but all it knows is what the current DOM looks like and what the new rendering looks like.  So if an extra class like "open" has been added in the DOM, then to rerender the template which lacks that class is actually considered a change!

Be careful by the way, I don't think block helpers are supported inside attributes, in this line: <div class="btn-group {{#if isOpen}}open{{/if}}">.

The story here is that we hope things like dropdowns will be pure declarative Meteor in the future.  If we add better integration with libraries like Bootstrap, it may be post-1.0.

-- David



George Kellerman

unread,
Apr 20, 2013, 5:03:42 PM4/20/13
to meteo...@googlegroups.com
Is there a more commonly workaround pattern for this issue or another way to solve it?

I am running into the same thing with the use of bootstrap-tabs. The active tab is given a class but when changing stuff in a tab the template re-renders and the default tab is selected rather than the current tab because the class is back to the default.

Steve Dossick

unread,
Apr 20, 2013, 5:23:14 PM4/20/13
to meteo...@googlegroups.com
Hi George

Without seeing some sample code it's hard to be specific, but the way that I've handled this in the past is to {{#isolate}} the modal contents (including the tabs) and be really careful about changing Session or other reactive sources that could cause a recompute outside of the current tab (which is {{#isolate}}'ed).

Does that make sense?

-s



George Kellerman

unread,
Apr 20, 2013, 5:39:20 PM4/20/13
to meteo...@googlegroups.com
Steve,

The tab code is as follow:

    <user>
        <ul class="nav nav-tabs" id="testTabs">
          <li class="active"><a href="#owned" data-toggle="tab">Owned</a></li>
          <li><a href="#starred" data-toggle="tab">Starred</a></li>
          <li><a href="#Recent" data-toggle="tab">Recent</a></li>
        </ul>
        <div id="usernav">
            <div class="tab-content">          
                <div id="owned" class="tab-pane" class='active'>

                    <ul>
                        {{#each owned}}
                            <li><a href="/{{title}}">{{title}}</a></li>
                        {{/each}}
                    </ul>
                </div> 
                <div id="starred" class="tab-pane" >
                    <ul>
                        {{#each starred}}
                            <li><a href="/{{title}}">{{title}}</a></li>
                        {{/each}}
                    </ul>
                </div>
            </div>
        </div>
    </user>

The Owned tab and tab-contents are defaulted as active in the template. The issue I run across is when having the Starred tab open and doing an action that causes the reactive data in the contents of the Starred tab to change. This causes the whole template to re-rendered which in turns causes the active tab and tab-contents to revert back to Owned. I tried wrapping each <li> under the first <ul> element with {{#isolate}} to preserve the tab state, and also wrapped the #owned and #starred divs (and childeren) with {{#isolate}} as well but that didn't seem to work. I'll do some more playing around with this but it seems like their should be either a much better way or at least a best practices for this particular issue.

Steve Dossick

unread,
Apr 20, 2013, 6:00:06 PM4/20/13
to meteo...@googlegroups.com
Both tabs are calling the {{title}} helper, so if title is changing underneath and is reactive (i.e. it's coming from a collection or a Session.get), both tabs will get re-rendered even if they are {{#isolated}}.  To my understanding, isolation doesn't turn off reactivity, it just establishes a "bounding box" ... if a change to a reactive source that's used within the bounding box occurs, only the box is re-rendered and not the rest of the template.

{{#isolate}} just gives you the same "bounding box" as using a sub-template.  If both templates are currently rendered and their reactive data sources change, both will update.

-s

-s

George Kellerman

unread,
Apr 20, 2013, 6:12:49 PM4/20/13
to meteo...@googlegroups.com
I don't think the issue here is the title so much as it is the {{#each owned}} and {{#each starred}} although I could be wrong. The titles are not changing but the number of them are (there are more titles returned from the collection for {{starred}} after the article is starred)

Either way what do you think a good {{#isolate}} structure for this would be given that the "active" class is dynamically changed between the <li> items for the tabs and the divs with <div class"tab-pane">?

What do you think of Rui's solution with storing and extracting session variables to many the active/open class helpers?

Steve Dossick

unread,
Apr 20, 2013, 6:23:28 PM4/20/13
to meteo...@googlegroups.com
I think if you set up each tab in its own template (or #isolate region), and then wrap the tab contents in something like {{#if tab1showing}} ... {{/if}}, and then set a session variable when the tab is selected by the user, you should be ok.  The idea is basically to remove the underlying reactive dependencies on each tab when it's not showing.

Does that make sense?

-s


George Kellerman

unread,
Apr 20, 2013, 6:25:39 PM4/20/13
to meteo...@googlegroups.com
yes thanks! I'll try it out and report back.
Reply all
Reply to author
Forward
0 new messages