bug? template with foreach, instantiated through a foreach is getting cross talk on model values

72 views
Skip to first unread message

Greg Veres

unread,
Aug 22, 2016, 9:30:19 PM8/22/16
to KnockoutJS
I have a page that contains a list of elements. Each of those elements has an array of subelements. 

The UI structure I am using is a collapsable section for each element. Then because there are lots of properties to each element, I am creating tabs within the collapsable sections. In the first of those collapsable tab elements is an array of the subelements. What I am finding is that when I add a second element to the page and I hit the "add subelement" button, I end up getting extra UI widgets on the second element page. 

I have:

Element #1
SubElement Array (4 subelements)

I add a second one:
Element #2
SubElement Array (0 elements)

at this point it all looks good. The knockout template that is displaying the Element #2 sub element array is showing 0 elements. Its all good. 

Then I click on the button that adds an array element to Element #2.SubElement Array. And what happens is that the knockout template now shows me 4 UI elements for #2's subelement array display. And all those 4 UI elements are bound to the same, single Element #2 subelement. So when I start typing a name of that subelement, all 4 UI elements update. 

I have gone back in and changed the number of subelements for #1 and that changes the number of UI elements that knockout creates. So when Element #1 has 3 subelements, the UI for Element #2 shows 3 UI elements, all abound to the same subarray element in Element #2's model. 

I am guessing this is a bug in knockout that it is getting confused about where to get the count for the template's foreach. 

Here is the structure of the code:

    <div data-bind="foreach: courtGroups">


        <div class="row" style="padding-top:5px">

            <a role="button" data-toggle="collapse" href="#a" aria-expanded="false" aria-controls="collapseOne" data-bind="attr:{href:'#collapseOne'+$index() }" onclick="$(this).find('.indicator').toggleClass('glyphicon-chevron-down glyphicon-chevron-right')">

                <div class="page-item-header col-xs-12">

                    <span class="glyphicon glyphicon-chevron-right indicator"></span>

                    &nbsp;

                    <span data-bind="text: Name"></span>

                    <span data-bind="click: $parent.DeleteCourtGroup.bind($parent), clickBubble: false" class="fa fa-trash-o" style="float:right"></span>

                </div>

            </a>

        </div>

        @using (Html.BeginForm())

        {

            <div class="collapse court-group" style="padding-left:15px;padding-right:15px" data-bind="attr:{id:'collapseOne'+$index()}">


                <div class="col-xs-12">

                    <div class="col-xs-12" style="height:10px"></div>

                    <div class="">

                        @*******************************************************************************

                            Tab List

                            ********************************************************************************@

                        <ul class="vcenter nav nav-pills col-xs-12" data-bind="foreach: Tabs">

                            <li class="page-banner-pill active" data-bind="css: { active: IsSelected }">

                                <a href="#" data-bind="click: $parent.SelectedTab"><span data-bind="text: Name"></span></a>

                            </li>

                            <li class="page-banner-separator-pill" data-bind="visible: NeedsSeparator">|</li>

                        </ul>

                        @*******************************************************************************

                            Tab Content

                            ********************************************************************************@

                        <div class="col-xs-12 post-body-text-spacing"></div>

                        <div class="tab-content" data-bind="foreach: Tabs">

                            <div class="row tab-pane" data-bind="template: { name: TemplateName, data: $parent }, css: { active: IsSelected }, attr: { id: TemplateName }"></div>

                        </div>

                    </div>

                </div>

                <div class="col-xs-12"><hr />&nbsp;</div>

                <div class="col-xs-12 centered-pills">

                    <ul class="nav nav-buttons">

                        <li><a class="btn section-button link" data-bind="click: $data.ReloadFromServer.bind($data, @Model.ClubId)" tabindex="0">@CommonResources.Resources.Cancel</a></li>

                        <li><button class="btn section-button" data-bind="click: $data.SaveToServer.bind($data, @Model.ClubId)">@CommonResources.Resources.Save</button></li>

                    </ul>

                </div>

            </div>

        }

    </div> 

Ok this part of the code holds the highest level structure. The first div is doing the foreach over the elements. 
Within each element I create the header that is collapsable and under that I create 3 tab groups. The number of tab groups is controlled by the view model, but there are three of them. This is the Tab list that creates the bootstrap nav portion of the tabs. Then the Tab Content section of the code creates the contents of the tabs. They are in templates so I can have different things in different tabs. I stole this structure from a SO post by https://github.com/rniemeyer.

The template that is having the cross talk is:

        <div>

            <a class="col-xs-12" style="cursor: pointer;padding-left: 0;padding-top: 5px" data-bind='click: AddACourt'>@CommonResources.Resources.CourtAdd</a>


            <table class="table table-striped" style="padding: 5px 5px;">

                <thead>

                    <tr>

                        <th>@CommonResources.Resources.CourtName</th>

                        <th>@CommonResources.Resources.CourtShortName</th>

                        <th>@CommonResources.Resources.CourtOffsetMinutesLabel</th>

                        <th></th>

                        <th></th>

                    </tr>

                </thead>

                <tbody data-bind="foreach: UICourtList">

                    <tr>

                        <td>

                            <span data-bind="text: Name, visible: !isEditing(), click: Edit"></span>

                            <input data-bind="textinput: Name, visible: isEditing(), hasFocus: isEditing()" />

                        </td>

                        <td>

                            <span style="max-width:7em" data-bind="text: ShortName, visible: !isEditing(), click: Edit"></span>

                            <input style="max-width:7em" data-bind="textinput: ShortName, visible: isEditing()" />

                        </td>

                        <td>

                            <span style="max-width:6em" data-bind="text: OffsetMinutes, visible: !isEditing(), click: Edit"></span>

                            <input style="max-width:6em" type="number" step="5" min="0" data-bind="textinput: OffsetMinutes, visible: isEditing(), attr: { max: ($parent.CourtLengthMinutes() - 5)}" />

                        </td>

                        <td style="width:25px">

                            <button class="fa fa-pencil unsytled_button" data-bind="visible: !isEditing(), click: Edit"></button>

                            <button class="fa fa-save unsytled_button" data-bind="visible: isEditing(), click: Save"></button>

                        </td>

                        <td style="width:25px">

                            <button class="fa fa-trash unsytled_button" data-bind="click: $parent.DeleteCourt.bind($parent)"></button>

                        </td>

                    </tr>

                </tbody>

            </table>

        </div>


BTW, this is a cshtml file as I am using MVC ASP.Net. 

It is the foreach: UICourtList that is pulling the count from Element #1 when rendering the contents for Element #2. 
Can anybody see anything obvious that I am doing wrong? I have verified that my model is correct. There is a single element being pushed on to the UICourtList KnockoutObservableArray. 

I will see if I can create a jsfiddle.

Thanks
Greg

Greg Veres

unread,
Aug 22, 2016, 9:31:15 PM8/22/16
to KnockoutJS
I am currently using Knockout 3.4.0

Greg Veres

unread,
Aug 22, 2016, 9:41:47 PM8/22/16
to KnockoutJS
I even used the debugging trick of dumping out $data from the template. 

            <pre data-bind="text: ko.toJSON($data, null, 2)"></pre>

Sure enough, UICourtList is 1 element long but the UI shows 4 elements.  The only way I can see that happening is if there is a bug in the template rendering of knockout. 
I guess the best thing to do is create a jsfiddle of a paired down example right?

Michael Best

unread,
Aug 22, 2016, 9:43:27 PM8/22/16
to KnockoutJS
I see the following

<div class="row tab-pane" data-bind="template: { name: TemplateName, data: $parent }, css: { active: IsSelected }, attr: { id: TemplateName }"></div>


How/where are you defining the template?

-- Michael

Greg Veres

unread,
Aug 22, 2016, 10:05:58 PM8/22/16
to knock...@googlegroups.com
The second part of the code listing is an excerpt from the template. Here is how the template script tag is defined:

<script type="text/html" id="CourtsTab”>
</script>

The excerpt that I included with the foreach: UICourtList is contained within this script tag. It is called with the line in the first code listing:

<div class="tab-content" data-bind="foreach: Tabs">
<div class="row tab-pane" data-bind="template: { name: TemplateName, data: $parent }, css: { active: IsSelected }, attr: { id: TemplateName }"></div>
</div>

each Element has a member of type knockoutObservableArray that provides the details of the template instantiation, it’s name on screen, its template name (CourtsTab in the example above), whether or not it is selected etc.
> --
> You received this message because you are subscribed to a topic in the Google Groups "KnockoutJS" group.
> To unsubscribe from this topic, visit https://groups.google.com/d/topic/knockoutjs/7MJeSOhFDyg/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to knockoutjs+...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Greg Veres

unread,
Aug 22, 2016, 10:10:06 PM8/22/16
to KnockoutJS
Micheal Thank you!!!

You pointed me right at the problem. It turns out the "attr: { id: TemplateName }" was the problem. I had put that in the code in an attempt to get fade in / fade out working as I switched tabs. I wanted to be able to find the tab so I could call fade on it. But it didn't work and I forgot to take out the setting of the Id. 

I think that with the template instance having a non-unique name, knockout was getting confused and finding the length attribute from the wrong div. 

When I removed setting the id, everything started working again. 

Greg

Michael Best

unread,
Aug 23, 2016, 1:45:59 PM8/23/16
to KnockoutJS
Glad you got it figured out! To do a fade-in effect, maybe you can use the afterRender template option.

-- Michael

Greg Veres

unread,
Aug 23, 2016, 1:59:19 PM8/23/16
to knock...@googlegroups.com
Yea that’s a good suggestion. Thanks

Reply all
Reply to author
Forward
0 new messages