简体   繁体   中英

Durandal Widget with multiple views

I am working on a SPA using Durandal, and I have created a widget for displaying a particular page component. Following the Durandal Documentation , the widget is in app/widgets/my-widget and is composed of viewmodel.js and view.html .

Now, I want to add a different view to the same widget - in effect, to have a "Basic" view and an "Advanced" view, with a flag in the ViewModel for which one will be used.

I do not want to create two different widgets, because the ViewModel is exactly the same and I want to avoid unnecessary code duplication. I also do not want to put both versions of the view into view.html and just display one or the other based on the flag in the ViewModel, because that will quickly become a nightmare to maintain as features are added to the widget later on.

I would think that one answer is to do some kind of View Composition , where the name of the view to be composed is returned from the ViewModel based on the flag, but I'm not really sure how to do that.

Does anyone have a way to do this? Is there a different way I should be approaching this problem?


UPDATE Here is an example of what I want to do. There is a widget workitem that is used to display "Work Items". In the ViewModel, a Work Item has a property status which can take the values ready , in-progress , complete , or invalid . Depending on the status of a Work Item, the information that needs to be displayed about it (and therefore its View) is completely different. What I want to do is set up something like this:

-widgets
|
|-workitem
|  |-viewmodel.js
|  |-view-ready.html
|  |-view-in-progress.html
|  |-view-complete.html
|  |-view-invalid.html

... and then automatically select one of these views based on the property in the ViewModel.

The approach we take to multi-view widgets is to simply use the if or the visible binding, and bind it to an observable on the viewmodel that holds the view type (eg 'compact', 'expanded', 'grouped', etc.).

For our datepicker widget, our high-level view is this (showing only two of the views):

</div>
    <div data-bind="visible: viewMode() === 'months'">
        <div data-bind="compose: {model: 'widgets/datepicker/datepickerMonths', view: 'widgets/datepicker/datepickerMonths', activate: true, activationData: messageChannel }">    </div>
    </div>
    <div data-bind="visible: viewMode() === 'years'">
        <div data-bind="compose: {model: 'widgets/datepicker/datepickerYears', view: 'widgets/datepicker/datepickerYears', activate: true, activationData: messageChannel }">    </div>
    </div>
</div>

The viewMode observable allows us to switch between views.

We use the visible binding in this case for two reasons:

  1. Performance: The layouts remain loaded in the DOM, just hidden;
  2. The jQuery clickoutside plugin is confused by the if binding (the if binding kicks the DOM out from under the user's mouse pointer, so to speak, and leads the clickoutside plugin to believe the user has clicked outside).

With the approach above, your widget's single view.html file can reference multiple, dynamically composable views.

I have provided a link to my public SkyDrive (now OneDrive) folder of our datepicker, written entirely to leverage Durandal's composition and widget systems, along with KnockoutJS: video of datepicker as I switch from one view to another . The switching of views occurs using the technique outlined above.

Characteristics of the datepicker:

  • datepicker is a multi-view widget
  • it composes an input field and our popover widget (in other words, a widget inside of a widget)
  • The high-level view dynamically composes three different views, switching between them as the user interacts
  • it relies on postal.js as the client-side message bus for routing selections between views, for coordinating with the parent view, and for updating the input control (you could use Durandal's built-in pub/sub, if you like)
  • it uses jwerty.js for keyboard management

Widgets views can't be easily switched like normal views by using area as they are always of type partial .

In order to change them you most probably have to overwrite widget.convertKindToModulePath and widget.convertKindToViewPath

Here's an example from https://github.com/BlueSpire/Durandal/issues/217

var oldConvert = widget.convertKindToModulePath;
widget.convertKindToModulePath: function(kind) {
    if (typeof(kind) == 'function') {
        return widget.mapKindToModuleId(kind());
    }
    return oldConvert(kind);
}

Update Widget can be constructed by using eg observables or computeds eg in your view:

<!-- ko widget: getWidgetSettings() -->
<!-- /ko -->

getWidgetSettings could be a ko.computed (depended on status) that instead of {kind: 'workitem'} returns something like {kind: { id : 'workitem', status: 'statusId'}} .

Now you'd need to adjust widget.convertKindToModulePath and widget.convertKindToViewPath accordingly as OOTB Durandal expects kind to be of type string but now it's an object .

Something along the following should get you started:

var oldconvertKindToModulePath = widget.convertKindToModulePath;
widget.convertKindToModulePath = function( kind ) {
    if ( typeof(kind) == 'object' ) {
        return  'widgets/' + kind.id + '/viewmodel';
    }
    return oldconvertKindToModulePath(kind);
};

var oldconvertKindToViewPath = widget.convertKindToViewPath;
widget.convertKindToViewPath = function( kind ) {
    if ( typeof(kind) == 'object' ) {
        return 'widgets/' + kind.id + '/' + statusViewMap[statusId] ;
    }
    return oldconvertKindToViewPath(kind);
};

As an alternative to implementing this as a widget you might consider implementing this as standard module and leveraging the getView method. Here's an example

http://dfiddle.github.io/dFiddle-2.0/#view-composition/getView

define(['knockout'], function(ko) {

  var roles = ['default', 'role1', 'role2'];
  var role = ko.observable('default');

  var getView = ko.computed(function(){
        var roleViewMap = {
          'default': 'viewComposition/getView/index.html',
          role1: 'viewComposition/getView/role1.html',
          role2: 'viewComposition/getView/role2.html'
        };

        this.role = (role() || 'default');

        return roleViewMap[this.role];
  });


  return {
    showCodeUrl: true,
    roles: roles,
    role: role,
    getView: getView,
    propertyOne: 'This is a databound property from the root context.',
    propertyTwo: 'This property demonstrates that binding contexts flow through composed views.'
  };


});

The example is using a singleton, which wouldn't fulfill the requirement having multiple independent workitems, so you'd have to rewrite it using the constructor pattern.

Feel free to fork and I'm taking pull requests :).

As you have suggested you can also do this using the composition and returning the appropriate view. Maybe something like the following (using a view location strategy ):

localViewStrategy - sure you can come up with a better name for this :)

define(['durandal/system', 'durandal/viewLocator'], function (system, viewLocator) {
    return function(settings) {
            var moduleId = system.getModuleId(settings.model);
            var viewName = settings.viewName;
            var path = moduleId.substring(0, moduleId.lastIndexOf('/') + 1) + viewName + '.html';

        return viewLocator.locateView(path, settings.area , settings.parent);
    };
});

Widget View Model

define(function(){
    var ctor = function() {
        //assuming this is your default view?
        this.view = ko.observable('view-ready');
    };

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

        if(settings.view !== null){
            this.view(settings.view);
        }
    }
    return ctor;
});

view.html

<span data-bind="compose: { model: $data, strategy: 'path/to/strategy/localViewStrategy', 'viewName' : view, activate: false, preserveContext: true, cacheViews : false }">

</span>

Not sure if this is the best way to go about it though!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM