1

This Durandal 2 knockout markup generates tabs with appropriate content but no tab is marked active.

  <div id="editor" class="html-editor form-control" contenteditable="true" data-bind="html: Content"></div>
  <label>Questions</label>
  <div class="question-container navbar">
    <ul class="nav nav-tabs" id="Questions" data-bind="foreach: Questions">
      <li role="presentation">
        <a data-toggle="tab" data-bind="text: Caption, attr: { href: '#' + QuestionId }"></a>
      </li>
    </ul>
    <div class="tab-content Questions" data-bind="foreach: Questions">
      <div class="tab-pane well" data-bind="attr: { id: QuestionId }">
        <div class="html-editor form-control" contenteditable="true" data-bind="html: Question"></div>
        <div data-bind="foreach: Answers">
          <div class="input-group">
            <textarea class="form-control" data-bind="value: Caption"></textarea>
            <span class="input-group-addon">
              <input type="radio" aria-label="Correct answer" data-bind="checked:isCorrect" />
            </span>
          </div>
        </div>
      </div>
    </div>
 </div>&nbsp;

In static markup one would identify the initially active tab by marking it with CSS class active.

How would one do that when all the tabs are generated?

This is Durandal 2 so I could handle attached and manipulate the DOM using jQuery.

What is the recommended approach?

Peter Wone
  • 17,965
  • 12
  • 82
  • 134
  • Just one idea but try to use the "show.bs.tab" event only one time, to make your logic (not sure if it can work for your case [idea]) http://getbootstrap.com/javascript/#tabs-events – BENARD Patrick Mar 03 '15 at 21:53
  • Elegant. Alas, that won't fire until *some* tab is activated. But you've given me an idea. I need a way for the user to add new questions. If I make that a tab in static mark-up, it will activate and I can use your idea! – Peter Wone Mar 03 '15 at 21:57
  • I'm stupid, can't you intercet the template ready and use "$('#myTab').tab('show')" ?? http://getbootstrap.com/javascript/#tabs-usage (french sorry it's difficult to share my things sometimes) – BENARD Patrick Mar 03 '15 at 22:07
  • Unbind it? I thought you were suggesting I use one-shot binding: `$.one("show.bs.tab", fn);` which is why I thought it so elegant. – Peter Wone Mar 03 '15 at 22:07
  • Where myTab is the tab issue from your logic ? – BENARD Patrick Mar 03 '15 at 22:09
  • 1
    Oh you are right, I guess I just learnt a new selector! Write that as an answer so I can accept it. Thanks, Patrick. Vous pouvez écrire en Français si vous le souhaitez. J'ai lu Français assez bien, je ne suis pas si bon à parler. – Peter Wone Mar 03 '15 at 22:10
  • Vous écrivez bien le français ^^ It's same for me, I write better english than I speak ( Je crois parler anglais, mais mes interlocuteurs m'assurent que non :( ). – BENARD Patrick Mar 03 '15 at 22:37

2 Answers2

1

After chatting in comments here is finally the chosen way

In bootstrap doc : http://getbootstrap.com/javascript/#tabs-usage

$('#myTab a:first').tab('show') 

Where myTab is the identifier of the UL containing the tabs.

In this case:

<ul class="nav nav-tabs" id="Questions" data-bind="foreach: Questions">
  <li role="presentation">
    <a data-toggle="tab" data-bind="attr: { href: '#' + QuestionId }">
      <span data-bind="text: Caption"></span>
      <span class="glyph-button" data-bind="click: deleteQuestion">
        <i class="fa fa-trash"></i>
      </span>
    </a>
  </li>
</ul>

And the code from the Durandal activate handler. Most of this code populates data for binding; it is included to aid comprehension of the mark-up. In the last few lines the miracle of tab activation occurs.

  for (var i = 0; i < result.InductionQAs.length; i++) {
    var question = {
      deleteQuestion: deleteQuestion,
      QuestionId: result.InductionQAs[i].InductionQAID,
      Question: ko.observable(result.InductionQAs[i].Question),
      Answers: ko.observableArray()
    };
    question.Caption = ko.computed(function () {
      var q = this.Question();
      var threshold = 20;
      return (q.length <= threshold) ? q : q.substring(0, threshold - 2) + "...";
    }, question);
    var answers = result.InductionQAs[i].Answers.split("|");
    for (var j = 0; j < answers.length; j++) {
      var a = answers[j];
      question.Answers.push({
        Caption: (a[0] == "*") ? a.substring(1) : a,
        isCorrect: a[0] == "*"
      });
    }
    vm.Questions.push(question);
  }
  setTimeout(function () {
    $("#Questions a:first").tab("show");
  }, 20);

setTimeout allows binding to finish modifying the DOM before we try to manipulate it.

Peter Wone
  • 17,965
  • 12
  • 82
  • 134
BENARD Patrick
  • 30,363
  • 16
  • 99
  • 105
1

If at all possible you should avoid doing direct DOM manipulation in your view model. If you just want the first tab to be selected you could use the following (untested):

<label>Questions</label>
  <div class="question-container navbar">
    <ul class="nav nav-tabs" id="Questions" data-bind="foreach: Questions">
      <li role="presentation">
        <a data-toggle="tab" data-bind="text: Caption, attr: { href: '#' + QuestionId }, css: {'active':  $index() === 1 }"></a>
      </li>
    </ul>
    <div class="tab-content Questions" data-bind="foreach: Questions">
      <div class="tab-pane well" data-bind="attr: { id: QuestionId }, css: {'active': $index() === 1 }">
        <div class="html-editor form-control" contenteditable="true" data-bind="html: Question"></div>
        <div data-bind="foreach: Answers">
          <div class="input-group">
            <textarea class="form-control" data-bind="value: Caption"></textarea>
            <span class="input-group-addon">
              <input type="radio" aria-label="Correct answer" data-bind="checked:isCorrect" />
            </span>
          </div>
        </div>
      </div>
    </div>
 </div>

Alternatively you could create a property on the Question object to determine if it should be active and use the same css binding as above to bind to that.

  • Thank, I didn't know about $index. Give me declarative any day of the week. – Peter Wone Mar 04 '15 at 11:26
  • It's zero-based, not one-based, and you have to do this for both the active tab and the active tab pane, but otherwise this is excellent advice that I have tested and found to work very well. – Peter Wone Mar 07 '15 at 09:55