【问题标题】:How to implement observable "bridge" in knockout.js?如何在 knockout.js 中实现可观察的“桥”?
【发布时间】:2018-05-06 01:55:08
【问题描述】:

我在 Knockout.js 中编写了一个持续时间选择器控件(sn-p 下面,also on 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>

问题在于,它不会对通过参数传递的可观察值的变化做出反应。但是,我不知道如何在不陷入无限循环的情况下实现它。目前,我有:

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

然后:

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

问题是,如果我对传递的 observable 的变化做出反应并重新生成小时、分钟和秒,我会导致他们的订阅者被通知,所以值会被重新生成,所以它会改变,所以小时,分钟并且秒数将被重新生成,因此他们的订阅者将再次收到通知......等等。

如何实现从单个可观察对象到其他三个可观察对象的所谓双向桥接?类似于 WPF 中的多重绑定。

【问题讨论】:

  • 做一个 sn-p 的好东西!我冒昧地移动了 sn-p。如果您不喜欢这些更改,只需在编辑历史记录中点击回滚即可。 :-)
  • 我无法重现陷入无限循环。你还有这个问题吗?
  • @Skeevs 你不能因为我没有在原始值更改上实现刷新小时/分钟/秒。我不知道该怎么做。
  • 如果我通过另一个输入更改原始值,一切都会按预期工作。 jsfiddle.net/bkhxjqed 和@Skeevs 一样,我无法重现您描述的问题。
  • @user3297291 是吗?使用我的编辑器将分钟更改为 40,然后使用另一个输入将时间更改为“12:34:56”。虽然编辑框会显示正确更改的时间,但分钟框仍会显示 40。

标签: javascript jquery knockout.js data-binding


【解决方案1】:

您应该使用pure Computed observable,而不是通过订阅组成数据项。

在任何情况下,如果该值实际上没有更改,则不应通知订阅者,因此对复合值的更改会转化为对单个值的更改,然后将复合值重新分配给与之前相同的值它已经有应该停止订阅周期。

示例实现:

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>'
});

【讨论】:

    【解决方案2】:

    您的逻辑可以分为三个部分:

    1. 解析数据(执行正则表达式),
    2. 修改它(你的号码输入),
    3. 并准备显示数据(7转换为07

    您设法很好地完成了所有这些事情,但无法将弹出显示的hoursminutesseconds 双向绑定。

    为了解决这个问题,我建议将这些属性更改为read/write 计算值。

    准备变更

    首先,让我们在组件的视图模型内部创建一个对象,用于跟踪原始小时、分钟和秒数字值:

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

    通过订阅传递给组件的value 来保持此对象的最新状态:

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

    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);
    }
    

    现在我们有一个 time 对象来跟踪组件外部所做的更改,我们可以继续进行各个控件。

    创建{ read, write } 计算

    现在,我们可以为小时、分钟和秒创建单独的计算属性。

    他们的write 方法会将输入值转发到time.HHtime.mmtime.ss

    他们的read 方法充当“显示值”并在0 前面添加值&lt; 10。例如:

    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));
      }
    });
    

    在组件外部公开用户输入

    最后一步是确保在内部time 对象中所做的更改被发布回组件传递的value。由于我们已经定义了 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);
    

    (如果您不喜欢这三个订阅,您也可以将evalValue 包装在一个计算中...)

    结束

    这些更改可确保您的价值观保持同步。我不确定您需要哪些边缘案例和输入卫生,但我希望您可以在此示例的基础上进行构建并在需要时重新配置。

    这是小提琴中的代码:https://jsfiddle.net/uxnasqf5/

    【讨论】:

      猜你喜欢
      • 2018-11-26
      • 2019-12-16
      • 1970-01-01
      • 1970-01-01
      • 2015-01-18
      • 1970-01-01
      • 1970-01-01
      • 2012-04-20
      • 1970-01-01
      相关资源
      最近更新 更多