admin管理员组

文章数量:1022442

I'm building a wizard widget with Durandal, and I'd like to use it like so:

        <div data-bind="wizard: options">
            <!-- Step 1 -->
            <span data-part="step-header-1">
                Step 1
            </span>
            <div data-part="step-content-1">
                step content here
            </div>

            <!-- Step 2 -->
            <span data-part="step-header-2">
                Step 2
            </span>
            <div data-part="step-content-2">
                step content here
            </div>
        </div>

This is the actual widget (cut down for brevity):

<div class="wizard-container">

    <ul class="steps" data-bind="foreach: steps">
        <li>
            <span data-bind="html: heading"></span>
        </li>
    </ul>

    <!-- ko foreach: steps -->
    <div class="wizard-step" data-bind="css: { active: isActive }">
        <div data-bind="html: content">

        </div>
    </div>
    <!-- /ko -->

</div>

I've sort of gotten it working, using jQuery to grab the data-parts, assign the data-part's inner HTML to a property on my step model, and then use the html-binding to bind the content to each step. This works on the DOM side of things, but doing it this way means that my step content won't get data-bound.. I am pretty sure it's because I use the html binding, which does not bind the content.

Is there a way to do this with Durandal widgets, without separating each step into a new view?

I'm building a wizard widget with Durandal, and I'd like to use it like so:

        <div data-bind="wizard: options">
            <!-- Step 1 -->
            <span data-part="step-header-1">
                Step 1
            </span>
            <div data-part="step-content-1">
                step content here
            </div>

            <!-- Step 2 -->
            <span data-part="step-header-2">
                Step 2
            </span>
            <div data-part="step-content-2">
                step content here
            </div>
        </div>

This is the actual widget (cut down for brevity):

<div class="wizard-container">

    <ul class="steps" data-bind="foreach: steps">
        <li>
            <span data-bind="html: heading"></span>
        </li>
    </ul>

    <!-- ko foreach: steps -->
    <div class="wizard-step" data-bind="css: { active: isActive }">
        <div data-bind="html: content">

        </div>
    </div>
    <!-- /ko -->

</div>

I've sort of gotten it working, using jQuery to grab the data-parts, assign the data-part's inner HTML to a property on my step model, and then use the html-binding to bind the content to each step. This works on the DOM side of things, but doing it this way means that my step content won't get data-bound.. I am pretty sure it's because I use the html binding, which does not bind the content.

Is there a way to do this with Durandal widgets, without separating each step into a new view?

Share Improve this question asked Sep 18, 2013 at 13:07 JeffJeff 12.2k14 gold badges85 silver badges155 bronze badges 11
  • Do you want them all showing at the same time, or change depending on which step you are on? – PW Kad Commented Sep 18, 2013 at 14:02
  • @PWKad all of that is working already, like I said I cut down the code for brevity - the only issue is that any data-bind in my step-content won't be bound, since the entire content is being loaded using Knockout's HTML-binding, which does not apply bindings to the HTML is renders using that binding. – Jeff Commented Sep 18, 2013 at 17:35
  • But the point is are all of the steps rendered at the same time (ie are you using individual objects for viewing each inside the widget) or does the widget represent each step along the way? The reason I ask is it depends on how you want to render your content (are you using a singleton or what?) – PW Kad Commented Sep 18, 2013 at 17:37
  • @PWKad I am not sure I understand your question, but I will try to answer anyways: The steps are determined by the data-part markup inside the widget's container. The widget itself should only provide the wizard's UI experience, whatever goes on inside the actual steps is none of it's business. The widget only provides the user interface as well as the navigation mechanism (I use Knockout to make life easier for this as well). Sometime I'd like to add in some step validation hooks, but that's later. :) – Jeff Commented Sep 18, 2013 at 17:44
  • @PWKad So every usage of the wizard will contain an initial set of steps (specified as data-part's, as shown in the question), but it would be cool to be able to add steps dynamically as well. – Jeff Commented Sep 18, 2013 at 17:45
 |  Show 6 more ments

2 Answers 2

Reset to default 5

Here's an implementation that uses a traditional Durandal master/detail approach in bination with a Tab widget. The tab widget only implements the tabbing functionality, while the Master controls what's pushed into it and the Detail controls the behavior/layout of itself.

Master

Viewmodel

define(['./tab', 'plugins/widget', 'knockout'], function (Tab, widget, ko) {

    return {
        tabs: ko.observableArray([
            new Tab('Durandal', 'A ...', true),
            new Tab('UnityDatabinding', 'A ...'),
            new Tab('Caliburn.Micro', 'C ...')
        ]),
        addNewTab: function() {
            this.tabs.push(new Tab('New Tab ', 'A test tab.'));
        }
    };
});

View

<div>
    <h1>Tabs sample</h1>
    <!-- ko widget : {kind: 'tabs', items : tabs} -->
    <!-- /ko -->

    <button class="btn" data-bind="click: addNewTab">Add</button>
</div>

Detail

Viewmodel

define(['durandal/events', 'knockout'], function(events, ko) {
    return function(name, content, isActive) {
        this.isActive = ko.observable(isActive || false);
        this.name = name;
        this.content = content;
    };
});

view

<div>
    <div data-bind="html: description"></div>
</div>

Tab widget

Viewmodel

define(['durandal/position', 'jquery'], function(position, $) {

    var ctor = function() { };

    ctor.prototype.activate = function(settings) {
        this.settings = settings;
    };

    ctor.prototype.detached = function() {
        console.log('bootstrap/widget/viewmodel: detached', arguments, this);
    };

    ctor.prototype.toggle = function(model, event){
        this.deactivateAll();
        model.isActive(true);

    };

    ctor.prototype.deactivateAll = function(){

        $.each(this.settings.items(), function(idx, tab){
            tab.isActive(false);
        });
    };



    return ctor;
});

View

<div class="tabs">
    <ul class="nav nav-tabs" data-bind="foreach: { data: settings.items }">
        <li data-bind="css: {active: isActive}">
            <a data-bind="text: name, click: $parent.toggle.bind($parent)"></a>
        </li>
    </ul>

    <div class="tab-content" data-bind="foreach: { data: settings.items}">
        <div class="tab-pane"  data-bind="html: content, css: {active: isActive}"></div>
    </div>
</div>

Live version available at: http://dfiddle.github.io/dFiddle-2.0/#extras/default. Feel free to fork.

As I suspected, the problem with my bindings not applying, was due to the fact that I used the html binding to set the step content. When Knockout sets the HTML, it does not apply bindings to it.

I wrote my own HTML binding handler, that wraps the HTML and inserts it as a DOM-node - Knockout will hapily apply bindings to this.

(function(window, $, ko) {
    var setHtml = function (element, valueAccessor) {
        var $elem = $(element);
        var unwrapped = ko.utils.unwrapObservable(valueAccessor());
        var $content = $(unwrapped);
        $elem.children().remove().end().append($content);
    };
    ko.bindingHandlers.htmlAsDom = {
        init: setHtml,
        update: setHtml
    };
}(window, jQuery, ko));

Please note, this only works when the binding value is wrapped as a node - e.g within a div tag. If not, it won't render it.

I'm building a wizard widget with Durandal, and I'd like to use it like so:

        <div data-bind="wizard: options">
            <!-- Step 1 -->
            <span data-part="step-header-1">
                Step 1
            </span>
            <div data-part="step-content-1">
                step content here
            </div>

            <!-- Step 2 -->
            <span data-part="step-header-2">
                Step 2
            </span>
            <div data-part="step-content-2">
                step content here
            </div>
        </div>

This is the actual widget (cut down for brevity):

<div class="wizard-container">

    <ul class="steps" data-bind="foreach: steps">
        <li>
            <span data-bind="html: heading"></span>
        </li>
    </ul>

    <!-- ko foreach: steps -->
    <div class="wizard-step" data-bind="css: { active: isActive }">
        <div data-bind="html: content">

        </div>
    </div>
    <!-- /ko -->

</div>

I've sort of gotten it working, using jQuery to grab the data-parts, assign the data-part's inner HTML to a property on my step model, and then use the html-binding to bind the content to each step. This works on the DOM side of things, but doing it this way means that my step content won't get data-bound.. I am pretty sure it's because I use the html binding, which does not bind the content.

Is there a way to do this with Durandal widgets, without separating each step into a new view?

I'm building a wizard widget with Durandal, and I'd like to use it like so:

        <div data-bind="wizard: options">
            <!-- Step 1 -->
            <span data-part="step-header-1">
                Step 1
            </span>
            <div data-part="step-content-1">
                step content here
            </div>

            <!-- Step 2 -->
            <span data-part="step-header-2">
                Step 2
            </span>
            <div data-part="step-content-2">
                step content here
            </div>
        </div>

This is the actual widget (cut down for brevity):

<div class="wizard-container">

    <ul class="steps" data-bind="foreach: steps">
        <li>
            <span data-bind="html: heading"></span>
        </li>
    </ul>

    <!-- ko foreach: steps -->
    <div class="wizard-step" data-bind="css: { active: isActive }">
        <div data-bind="html: content">

        </div>
    </div>
    <!-- /ko -->

</div>

I've sort of gotten it working, using jQuery to grab the data-parts, assign the data-part's inner HTML to a property on my step model, and then use the html-binding to bind the content to each step. This works on the DOM side of things, but doing it this way means that my step content won't get data-bound.. I am pretty sure it's because I use the html binding, which does not bind the content.

Is there a way to do this with Durandal widgets, without separating each step into a new view?

Share Improve this question asked Sep 18, 2013 at 13:07 JeffJeff 12.2k14 gold badges85 silver badges155 bronze badges 11
  • Do you want them all showing at the same time, or change depending on which step you are on? – PW Kad Commented Sep 18, 2013 at 14:02
  • @PWKad all of that is working already, like I said I cut down the code for brevity - the only issue is that any data-bind in my step-content won't be bound, since the entire content is being loaded using Knockout's HTML-binding, which does not apply bindings to the HTML is renders using that binding. – Jeff Commented Sep 18, 2013 at 17:35
  • But the point is are all of the steps rendered at the same time (ie are you using individual objects for viewing each inside the widget) or does the widget represent each step along the way? The reason I ask is it depends on how you want to render your content (are you using a singleton or what?) – PW Kad Commented Sep 18, 2013 at 17:37
  • @PWKad I am not sure I understand your question, but I will try to answer anyways: The steps are determined by the data-part markup inside the widget's container. The widget itself should only provide the wizard's UI experience, whatever goes on inside the actual steps is none of it's business. The widget only provides the user interface as well as the navigation mechanism (I use Knockout to make life easier for this as well). Sometime I'd like to add in some step validation hooks, but that's later. :) – Jeff Commented Sep 18, 2013 at 17:44
  • @PWKad So every usage of the wizard will contain an initial set of steps (specified as data-part's, as shown in the question), but it would be cool to be able to add steps dynamically as well. – Jeff Commented Sep 18, 2013 at 17:45
 |  Show 6 more ments

2 Answers 2

Reset to default 5

Here's an implementation that uses a traditional Durandal master/detail approach in bination with a Tab widget. The tab widget only implements the tabbing functionality, while the Master controls what's pushed into it and the Detail controls the behavior/layout of itself.

Master

Viewmodel

define(['./tab', 'plugins/widget', 'knockout'], function (Tab, widget, ko) {

    return {
        tabs: ko.observableArray([
            new Tab('Durandal', 'A ...', true),
            new Tab('UnityDatabinding', 'A ...'),
            new Tab('Caliburn.Micro', 'C ...')
        ]),
        addNewTab: function() {
            this.tabs.push(new Tab('New Tab ', 'A test tab.'));
        }
    };
});

View

<div>
    <h1>Tabs sample</h1>
    <!-- ko widget : {kind: 'tabs', items : tabs} -->
    <!-- /ko -->

    <button class="btn" data-bind="click: addNewTab">Add</button>
</div>

Detail

Viewmodel

define(['durandal/events', 'knockout'], function(events, ko) {
    return function(name, content, isActive) {
        this.isActive = ko.observable(isActive || false);
        this.name = name;
        this.content = content;
    };
});

view

<div>
    <div data-bind="html: description"></div>
</div>

Tab widget

Viewmodel

define(['durandal/position', 'jquery'], function(position, $) {

    var ctor = function() { };

    ctor.prototype.activate = function(settings) {
        this.settings = settings;
    };

    ctor.prototype.detached = function() {
        console.log('bootstrap/widget/viewmodel: detached', arguments, this);
    };

    ctor.prototype.toggle = function(model, event){
        this.deactivateAll();
        model.isActive(true);

    };

    ctor.prototype.deactivateAll = function(){

        $.each(this.settings.items(), function(idx, tab){
            tab.isActive(false);
        });
    };



    return ctor;
});

View

<div class="tabs">
    <ul class="nav nav-tabs" data-bind="foreach: { data: settings.items }">
        <li data-bind="css: {active: isActive}">
            <a data-bind="text: name, click: $parent.toggle.bind($parent)"></a>
        </li>
    </ul>

    <div class="tab-content" data-bind="foreach: { data: settings.items}">
        <div class="tab-pane"  data-bind="html: content, css: {active: isActive}"></div>
    </div>
</div>

Live version available at: http://dfiddle.github.io/dFiddle-2.0/#extras/default. Feel free to fork.

As I suspected, the problem with my bindings not applying, was due to the fact that I used the html binding to set the step content. When Knockout sets the HTML, it does not apply bindings to it.

I wrote my own HTML binding handler, that wraps the HTML and inserts it as a DOM-node - Knockout will hapily apply bindings to this.

(function(window, $, ko) {
    var setHtml = function (element, valueAccessor) {
        var $elem = $(element);
        var unwrapped = ko.utils.unwrapObservable(valueAccessor());
        var $content = $(unwrapped);
        $elem.children().remove().end().append($content);
    };
    ko.bindingHandlers.htmlAsDom = {
        init: setHtml,
        update: setHtml
    };
}(window, jQuery, ko));

Please note, this only works when the binding value is wrapped as a node - e.g within a div tag. If not, it won't render it.

本文标签: javascriptDurandal widgetsdynamic templated partsStack Overflow