Javascript 清除焦点上的默认值、显示格式化的数字并将转换后的数据存储为可观察数据的输入?

Javascript 清除焦点上的默认值、显示格式化的数字并将转换后的数据存储为可观察数据的输入?,javascript,jquery,html,knockout.js,Javascript,Jquery,Html,Knockout.js,背景 我正在尝试创建符合以下条件的用户友好输入元素: 解析并验证用户输入 当为空且未聚焦时,显示灰显提示 未聚焦时显示格式化的值 通过在聚焦时删除格式设置,最大限度地减少不必要的击键和鼠标单击 在内部存储数值,用于数学计算 我曾尝试使用自定义的击出绑定处理程序实现上述功能,但在重用此逻辑以在span元素中显示格式化输出时,我似乎陷入了困境 问题 在下面的提琴中,我定义了一个“数字”绑定。例如, 此绑定使用事件处理来实现上述标准,将原始JavaScript数字写入可观察对象,并将格式化字符串写

背景

我正在尝试创建符合以下条件的用户友好输入元素:

  • 解析并验证用户输入
  • 当为空且未聚焦时,显示灰显提示
  • 未聚焦时显示格式化的值
  • 通过在聚焦时删除格式设置,最大限度地减少不必要的击键和鼠标单击
  • 在内部存储数值,用于数学计算
我曾尝试使用自定义的击出绑定处理程序实现上述功能,但在重用此逻辑以在span元素中显示格式化输出时,我似乎陷入了困境

问题

在下面的提琴中,我定义了一个“数字”绑定。例如,

此绑定使用事件处理来实现上述标准,将原始JavaScript数字写入可观察对象,并将格式化字符串写入输入元素的值

不幸的是,这也让我在如何最好地显示输出方面有点困惑:

  • 使用
    不起作用,因为数字处理程序被硬编码为更新元素的值
  • 使用
    意味着我将丢失写入数字处理程序的格式规则
  • 使用
    会中断句子中的文本流,并且由于显示不可用的输入,似乎会导致不直观的用户体验
任何关于如何干净优雅地达到上述标准的建议都将不胜感激

示例

jsFiddle:


注意:虽然本例侧重于数字输入,但我最终还是希望对文本和数据输入也进行同样的操作。

您需要有一个可观察的对象,该对象位于支持值前面,以处理格式化、取消格式化

最好的方法是通过subscribablefn扩展点创建一个包装器

对fiddle HTML的唯一更改是更改2个文本绑定

<span data-bind="text: waterUsed.formattedValue">
<span data-bind="text: discount.formattedValue">

我想我会坚持你的包装解决方案,我不太喜欢覆盖库函数。。。我忘了提到我需要IE8兼容性,所以我没有占位符。。。但谷歌搜索确实找到了这个链接,似乎也得到了类似的结果:
ko.subscribable.fn['asFormattedNumber'] = function (defaultValue, options) {
    var target = this;

    var prefix = options.prefix || '';
    var postfix = options.postfix || '';
    var decimals = options.decimals || 0;
    var isFixed = options.isFixed || false;
    var roundFactor = Math.pow(10, decimals);

    // Very basic - Doesn't assume any number format
    var valueExtractor = new RegExp( '^' + (prefix ? '\\' + prefix : '' ) + '([0-9\\.\\,]+)' + (postfix ? '\\' + postfix : '' ) + '$' );

    // Extracts the number portion a formatted string
    var unformatter = function( value ) {
        // If not a match, just return the value
        return (value.match(valueExtractor) || ['', value])[1];
    };

    // Formats the value according to options
    var formatter = function(value) {

        // If no value, return empty string.  Important to tell the difference
        // for when the default value is entered into the input box
        if ( value === undefined || value === null ) {
            return '';
        }
        return prefix + value.toFixed(decimals) + postfix;
    };

    // This is the observable the world will see
    var wrapperObs = ko.observable();

    // If true, formatted value will be blank and placeholder should be shown
    var wrapperIsEmpty = true;
    // Flag to stop recursion
    var wrappedIsBeingSet = false;

    // Check if the target observable is writeable.  If it isn't then our wrapper can never be set,
    // so no point in setting up a subscription on the wrapperObs.
    if ( ko.isWriteableObservable(target) ) {
        wrapperObs.subscribe(function(newValue) {
            wrappedIsBeingSet = true;
            if ( newValue === '' ) {
                wrapperIsEmpty = true;
                target(defaultValue);
                return;
            }

            var unformattedValue = unformatter(newValue);
            var parsed = parseFloat(unformattedValue);

            if ( isNaN(parsed) && target() === defaultValue ) {
                wrapperObs('');
                return;
            }

            if ( isFixed ) {
                parsed = Math.round( parsed * roundFactor ) / roundFactor;
            }

            if ( parsed !== target() ){
                target(parsed);
            }

            wrapperObs( formatter(parsed ));
        });
    }

    target.subscribe(function(newValue) {
        // Handles situations where input is empty and resets target to defaultValue;
        if ( !wrappedIsBeingSet ) {
            var formattedValue = formatter(newValue)

            wrapperObs(formattedValue);
        }

        wrapperIsBeingSet = false;
    });

    // Initialise initial state
    if ( target() === undefined ) {
        wrapperObs('');
    } else {
        target.notifySubscribers(target());
    }

    // Add stuff to the public observable.
    wrapperObs.value = target;
    wrapperObs.placeholder = formatter(defaultValue);
    wrapperObs.unformattedValue = ko.computed( function() {
        return wrapperIsEmpty && target() === defaultValue ? '' : target();
    });
    wrapperObs.formattedValue = ko.computed( function() { 
        return formatter(target());
    });

    return wrapperObs;
}
// custom knockout binding for managing formatted numeric inputs such as dollars, kilolitres & percentages
ko.bindingHandlers.number = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        var $el = $(element),
            obsValue = valueAccessor();

        $el.attr('placeholder', obsValue.placeholder);

        // prepare input field for editing by removing unneccessary characters (dollar signs, etc)
        $el.focus(function () {
            this.value = obsValue.unformattedValue();
            $el.attr('placeholder', '');
        });

        // restore proper input field format (showing dollars signs, etc)
        $el.blur(function () {
            this.value = obsValue();
            $el.attr('placeholder', obsValue.placeholder);
        });

        return ko.bindingHandlers['value'].init(element, valueAccessor, allBindings, viewModel, bindingContext);
    },
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        ko.bindingHandlers['value'].update(element, valueAccessor, allBindings, viewModel, bindingContext);
    }
};

function ViewModel() {
    var self = this;

    self.waterUsed = ko.observable().asFormattedNumber(0, {
        postfix: " KL",
        decimals: 3
    });

    self.price = ko.observable().asFormattedNumber(0, {
        prefix: "$",
        decimals: 3,
        isFixed: true
    });

    self.discount = ko.observable().asFormattedNumber(0, {
        postfix: "%",
        decimals: 0
    });

    self.grossCost = ko.computed(function () {
        return self.waterUsed.value() * self.price.value();
    }).asFormattedNumber(0, {
        prefix: "$",
        decimals: 2,
        isFixed: true
    });

    self.netCost = ko.computed(function () {
        return self.grossCost.value() - (self.grossCost.value() * (self.discount.value() / 100));
    }).asFormattedNumber(0, {
        prefix: "$",
        decimals: 2,
        isFixed: true
    });
}

var viewModel = new ViewModel();
ko.applyBindings(viewModel);