简体   繁体   English

如何在敲门js中实现可观察的“桥梁”?

[英]How to implement observable “bridge” in knockout.js?

I wrote a duration picker control in Knockout.js (snippet below, also on jsFiddle ): 我在Knockout.js中编写了一个持续时间选择器控件(以下代码段, 也位于jsFiddle上 ):

 $(function() { ko.bindingHandlers.clickOutside = { init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { var callback = ko.utils.unwrapObservable(valueAccessor()); var clickHandler = function (e) { if (!($.contains(element, e.target) || element === e.target)) { callback(); } }; $('html').on('click', clickHandler); ko.utils.domNodeDisposal.addDisposeCallback(element, function () { $('html').off('click', clickHandler); }); } }; ko.components.register('durationInput', { viewModel: function (params) { var self = this; if (!ko.isObservable(params.value)) { throw "value param should be an observable!"; } this.value = params.value; var match = /^([0-9]{1,2}):([0-5]?[0-9]):([0-5]?[0-9])$/.exec(this.value()); this.hours = ko.observable(match != null ? match[1] : "00"); this.minutes = ko.observable(match != null ? match[2] : "00"); this.seconds = ko.observable(match != null ? match[3] : "00"); this.label = params.label; this.id = params.id; this.popupVisible = ko.observable(false); this.inputClick = function () { self.popupVisible(!self.popupVisible()); }; this.clickOutside = function () { self.popupVisible(false); }; this.evalValue = function () { var hrs = self.hours(); while (hrs.length < 2) hrs = "0" + hrs; var mins = self.minutes(); while (mins.length < 2) mins = "0" + mins; var secs = self.seconds(); while (secs.length < 2) secs = "0" + secs; self.value(hrs + ':' + mins + ':' + secs); }; this.hours.subscribe(this.evalValue); this.minutes.subscribe(this.evalValue); this.seconds.subscribe(this.evalValue); }, template: '<div class="form-group" data-bind="clickOutside: clickOutside">\\ <label data-bind="text: label, attr: { for: id }" />\\ <div class="input-group">\\ <input class="form-control duration-picker-input" type="text" data-bind="value: value, click: inputClick" readonly>\\ <div class="panel panel-default duration-picker-popup" data-bind="visible: popupVisible">\\ <div class="panel-body">\\ <div class="inline-block">\\ <div class="form-group">\\ <label data-bind="attr: { for: id + \\'-hours\\' }">Hours</label>\\ <input data-bind="textInput: hours, attr: { id: id + \\'-hours\\' }" class="form-control" type="number" min="0" max="99" />\\ </div>\\ </div>\\ <div class="inline-block">\\ <div class="form-group">\\ <label data-bind="attr: { for: id + \\'-minutes\\' }">Minutes</label>\\ <input data-bind="textInput: minutes, attr: { id: id + \\'-minutes\\' }" class="form-control" type="number" min="0" max="59" />\\ </div>\\ </div>\\ <div class="inline-block">\\ <div class="form-group">\\ <label data-bind="attr: { for: id + \\'-seconds\\' }">Seconds</label>\\ <input data-bind="textInput: seconds, attr: { id: id + \\'-seconds\\' }" class="form-control" type="number" min="0" max="59" />\\ </div>\\ </div>\\ </div>\\ </div>\\ </input>\\ <span class="input-group-addon" data-bind="click: inputClick"><span class="hover-action glyphicon glyphicon-chevron-down"></span></span>\\ </div>\\ </div>' }); var viewmodel = function() { this.time = ko.observable("12:34:56"); }; ko.applyBindings(new viewmodel()); }); 
 .hover-action { color: #bbb; cursor: pointer; } .hover-action:hover { color: #333; cursor: pointer; } .inline-block { display: inline-block; } .duration-picker-input { position: relative; } .duration-picker-popup { z-index: 100; position: absolute; top: 100%; right: 0; } 
 <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/paper/bootstrap.min.css"> <div style="width: 400px"> <!--ko component: { name: "durationInput", params: { id: "time", value: time, label: "Total time" } } --> <!-- /ko --> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> 

The problem is, that it does not react to changes of value observable, which is being passed via parameters. 问题是,它对通过参数传递的可观察到的值变化没有反应。 However, I have no idea how to implement it without falling into an infinite loop. 但是,我不知道如何在不陷入无限循环的情况下实现它。 Currently, I have: 目前,我有:

(initialization)
       |
Current value is being read from observable passed via params
       |
Hours, minutes and seconds values are generated basing on current value

And then: 接着:

(Hours, minutes or seconds change)
       |
New value is generated as hours:minutes:seconds
       |
New value is set to observable passed via params

The problem is that if I react to changes in passed observable and re-generate hours, minutes and seconds, I will cause their subscribers to be notified, so the value will be regenerated, so it will change, so hours, minutes and seconds will be regenerated, so their subscribers will be notified again... and so on. 问题是,如果我对观察到的变化做出反应并重新生成小时,分钟和秒,我将通知其订阅者,因此将重新生成该值,因此它将更改,因此小时,分钟和秒将发生变化。重新生成,因此将再次通知其订户...等等。

How can I implement so-called two-way bridge from single observable to three others? 如何实现从单一可观察到另外三个的所谓双向桥梁? Something like a multi-binding in WPF. WPF中的多重绑定之类的东西。

You should be using a pure Computed observable rather than composing a data item via subscriptions. 您应该使用纯计算的可观察值,而不是通过订阅来构成数据项。

In any case, subscribers should not be notified if the value doesn't actually change, so a change to the composite value that translates into a change to individual values, which then re-assigns the composite value to the same value as it already has should stop the subscription cycle. 无论如何,如果该值实际上没有改变,则不应通知订户,因此对复合值的更改会转换为对单个值的更改,然后再将复合值重新分配为与现有值相同的值应该停止订阅周期。

Sample implementation: 示例实施:

ko.components.register('durationInput', {
    viewModel: function (params) {

        var self = this;

        if (!ko.isObservable(params.value)) {
            throw "value param should be an observable!";            
        }

        this.value = params.value;
        this.label = params.label;
        this.id = params.id;
        this.popupVisible = ko.observable(false);

        this.hours = ko.observable();
        this.minutes = ko.observable();
        this.seconds = ko.observable();

        this.valueEvaluator = ko.pureComputed({
            read: function () {

                var hrs = self.hours() || "";
                while (hrs.length < 2)
                    hrs = "0" + hrs;

                var mins = self.minutes() || "";
                while (mins.length < 2)
                    mins = "0" + mins;

                var secs = self.seconds() || "";
                while (secs.length < 2)
                    secs = "0" + secs;

                self.value(hrs + ':' + mins + ':' + secs);
            },
            write: function (value) {

                var match = /^([0-9]{1,2}):([0-5]?[0-9]):([0-5]?[0-9])$/.exec(self.value());
                self.hours(match != null ? match[1] : "00");
                self.minutes(match != null ? match[2] : "00");
                self.seconds(match != null ? match[3] : "00");
            }
        });

        // Init
        this.valueEvaluator(this.value());

        this.value.subscribe(function (newValue) {

            if (self.valueEvaluator() != newValue)
                self.valueEvaluator(newValue);
        });

        this.valueEvaluator.subscribe(function (newValue) {

            if (self.value() != newValue)
                self.value(newValue);
        });

        this.inputClick = function () {

            self.popupVisible(!self.popupVisible());
        };

        this.clickOutside = function () {

            self.popupVisible(false);
        };
    },
    template: '<div class="form-group" data-bind="clickOutside: clickOutside">\
        <label data-bind="text: label, attr: { for: id }" />\
        <div class="input-group">\
            <input class="form-control duration-picker-input" type="text" data-bind="value: value, click: inputClick" readonly>\
                <div class="panel panel-default duration-picker-popup" data-bind="visible: popupVisible">\
                    <div class="panel-body">\
                        <div class="inline-block">\
                            <div class="form-group">\
                                <label data-bind="attr: { for: id + \'-hours\' }">Hours</label>\
                                <input data-bind="textInput: hours, attr: { id: id + \'-hours\' }" class="form-control" type="number" min="0" max="99" />\
                            </div>\
                        </div>\
                        <div class="inline-block">\
                            <div class="form-group">\
                                <label data-bind="attr: { for: id + \'-minutes\' }">Minutes</label>\
                                <input data-bind="textInput: minutes, attr: { id: id + \'-minutes\' }" class="form-control" type="number" min="0" max="59" />\
                            </div>\
                        </div>\
                        <div class="inline-block">\
                            <div class="form-group">\
                                <label data-bind="attr: { for: id + \'-seconds\' }">Seconds</label>\
                                <input data-bind="textInput: seconds, attr: { id: id + \'-seconds\' }" class="form-control" type="number" min="0" max="59" />\
                            </div>\
                        </div>\
                    </div>\
                </div>\
            </input>\
            <span class="input-group-addon" data-bind="click: inputClick"><span class="hover-action glyphicon glyphicon-chevron-down"></span></span>\
        </div>\
    </div>'
});

Your logic can be split up in to three parts: 您的逻辑可以分为三部分:

  1. parsing data ( executing the regex ), 解析数据( 执行正则表达式 ),
  2. modifying it ( your number inputs ), 修改它( 您的数字输入 ),
  3. and preparing the data for display ( converting 7 to 07 ) 并准备显示数据( 7转换为07

You managed to do all of these things quite well, but couldn't manage to two-way bind the hours , minutes and seconds of your popup display. 您设法很好地完成了所有这些操作,但是无法对弹出式显示的hoursminutesseconds进行双向绑定。

To fix this issue, I propose to change these properties to read/write computed values. 为了解决此问题,我建议更改这些属性以read/write计算值。

Preparing the change 准备变更

First, let's create an object internal to the component's view model that keeps track of the raw hour, minute and seconds numerical values: 首先,让我们在组件的视图模型内部创建一个对象,该对象跟踪原始的小时,分​​钟和秒数值

var time = {
  HH: ko.observable(0),
  mm: ko.observable(0),
  ss: ko.observable(0)
};

Keep this object up to date by subscribing to the value passed to the component: 通过订阅传递给组件的value使该对象保持最新:

this.value.subscribe(parseTime);

// Initial settings for time object
parseTime(this.value());

With the parseTime function being: parseTime函数为:

var parseTime = function(timeString) {
  var parts = /^([0-9]{1,2}):([0-5]?[0-9]):([0-5]?[0-9])$/.exec(timeString);

  time.HH(parts ? +parts[1] : 0); // Note we're casting to Number
  time.mm(parts ? +parts[2] : 0);
  time.ss(parts ? +parts[3] : 0);
}

Now that we have a time object that keeps track of changes made outside the component, we can move on to the individual controls. 现在我们有了一个time对象,该对象可以跟踪在组件外部进行的更改,接下来可以进入各个控件。

Creating the { read, write } computeds 创建{ read, write }计算

Now, we can create individual computed properties for hours, minutes and seconds. 现在,我们可以为小时,分钟和秒创建单独的计算属性。

Their write methods will forward the input values to time.HH , time.mm and time.ss . 他们的write方法会将输入值转发到time.HHtime.mmtime.ss

Their read methods act as a "display value" and prepend 0 for values < 10 . 他们的read方法充当“显示值”,并且值< 10前面加0 For example: 例如:

this.hours = ko.computed({
  // Ensure two digits
  read: function() {
    return (time.HH() < 10 ? "0" : "") + time.HH();
  },
  // Cast to a number and limit between 0 and 23
  write: function(v) { 
    time.HH(Math.max(Math.min(23, +v), 0));
  }
});

Exposing user input outside of the component 将用户输入暴露在组件外部

The last step is to make sure the changes made in the internal time object are published back to the component's passed value . 最后一步是确保将内部time对象中所做的更改发布回组件的传递value Since we've already defined t he read methods to add zeroes when needed, this gets a bit easier: 由于我们已经定义了read方法以在需要时添加零,因此这变得容易一些:

this.evalValue = function() {
  var hrs = self.hours();
  var mins = self.minutes();
  var secs = self.seconds();
  self.value(hrs + ':' + mins + ':' + secs);
};

this.hours.subscribe(this.evalValue);
this.minutes.subscribe(this.evalValue);
this.seconds.subscribe(this.evalValue);

(If you don't like the three subscribes, you can wrap evalValue in a computed as well...) (如果您不喜欢这三个订阅,则也可以将evalValue包装在计算机中。)

Concluding 结论

These changes ensure your values keep in sync. 这些更改可确保您的值保持同步。 I'm not sure what edge cases and input sanitation you require, but I hope you can build on this example and reconfigure when needed. 我不确定您需要什么边缘情况和输入环境卫生,但是我希望您可以在此示例上构建并在需要时进行重新配置。

Here's the code in a fiddle: https://jsfiddle.net/uxnasqf5/ 这是小提琴中的代码: https : //jsfiddle.net/uxnasqf5/

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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