Tag Archives: knockout

Knockout Binding Handler: validationPopover

<script type="text/javascript">
    ko.bindingHandlers.validationPopover = {
        init: function (element, valueAccessor, allBindings) {
            if (!allBindings.has("value")) {
                return;
            }
            var $element = $(element);
            var value = allBindings.get("value");
            $element.popover($.extend({}, ko.unwrap(valueAccessor()), {
                content: function () {
                    return value.error();
                },
                trigger: "manual"
            }));
            value.subscribe(function () {
                $element.popover(value.isValid() ? "hide" : "show");
            });
        }
    };
</script>

<input type="text" data-bind="value: name, validationPopover: {}" />

<script type="text/javascript">
    ko.validation.init({
        insertMessages: false
    });
    
    function ViewModel() {
        var self = this;
        self.name = ko.observable().extend({ required: true });
    }
    
    ko.applyBindings(new ViewModel());
</script>

Knockout Helper: subscribeOnce

ko.subscribable.fn.subscribeOnce = function (callback, target, event) {
    var ignore = false;
    this.subscribe(function (value) {
        if (ignore) {
            return;
        }
        ignore = true;
        callback.call(target, value);
        ignore = false;
    }, null, event);
};

function callback(value) {
    console.log(value);
    if (value < 3) {
        this(value + 1);
    }
    console.log(value);
}

var x = ko.observable(0);
x.subscribe(callback, x);
x(1);
console.log("x = " + x());

var y = ko.observable(0);
y.subscribeOnce(callback, y);
y(1);
console.log("y = " + y());

Output:

1
2
3
3
2
1
x = 3
1
1
y = 2

Update: February 18, 2015

There are a couple problems with changing an observable’s value in its own subscription. First, other subscriptions will see the intermediate writes to the observable. Second, the corresponding values will get sent to subsequent subscriptions (i.e., those made after the self-subscription) in reverse order.

var z = ko.observable(0);
z.subscribeOnce(function (value) {
    if (value < 3) {
        z(value + 1);
    }
});
z.subscribe(function (value) {
    console.log(value);
});
z(1);

Output:

2
1

So don’t use subscribeOnce. Instead, hide the observable behind a writable computed observable that performs the necessary preprocessing before setting a value.

var _z = ko.observable(0);
var z = ko.computed({
    read: _z,
    write: function (value) {
        if (value < 3) {
            value++;
        }
        _z(value);
    }
});
z.subscribe(function (value) {
    console.log(value);
});
z(1);

Output:

2

Knockout Binding Handler: tooltip

<script type="text/javascript">
    ko.bindingHandlers.tooltip = {
        normalize: function (valueAccessor) {
            var value = ko.unwrap(valueAccessor());
            if (typeof value === "object" && "data" in value) {
                return value;
            } else {
                return { data: value };
            }
        },
        
        getOptions: function (element, valueAccessor) {
            var value = ko.bindingHandlers.tooltip.normalize(valueAccessor);
            return $.extend({}, ko.unwrap(value.options), {
                items: element,
                content: ko.unwrap(value.data)
            });
        },
        
        init: function (element, valueAccessor) {
            var options = ko.bindingHandlers.tooltip.getOptions(element, valueAccessor);
            $(element).tooltip(options);
        },
        
        update: function (element, valueAccessor) {
            var options = ko.bindingHandlers.tooltip.getOptions(element, valueAccessor);
            $(element).tooltip("option", options);
        }
    };
</script>

<div data-bind="tooltip: 'Hypertext Markup Language'">HTML</div>
<div data-bind="
    tooltip: {
        data: now().toLocaleTimeString(),
        options: { track: true }
    }">
    Time
</div>

<script type="text/javascript">
    function ViewModel() {
        var self = this;
        self.now = ko.observable(new Date());
        setInterval(function () {
            self.now(new Date());
        }, 1000);
    }
    
    ko.applyBindings(new ViewModel());
</script>

Knockout Binding Handler: sortable

<script type="text/javascript">
    ko.bindingHandlers.sortable = {
        normalize: function (valueAccessor) {
            var value = ko.unwrap(valueAccessor());
            if (typeof value === "object" && "data" in value) {
                return value;
            } else {
                return { data: valueAccessor() };
            }
        },
        
        getHandlers: function (data) {
            var startIndex;
            return {
                start: function (event, ui) {
                    startIndex = ui.item.index();
                },
                
                stop: function (event, ui) {
                    var stopIndex = ui.item.index();
                    ui.item.remove();
                    var item = data.splice(startIndex, 1)[0];
                    data.splice(stopIndex, 0, item);
                }
            };
        },
        
        init: function (element, valueAccessor) {
            var self = ko.bindingHandlers.sortable;
            var value = self.normalize(valueAccessor);
            var handlers = self.getHandlers(value.data);
            var options = $.extend({}, ko.unwrap(value.options), handlers);
            $(element).sortable(options);
            return ko.bindingHandlers.foreach.init.apply(this, arguments);
        },
        
        update: function () {
            ko.bindingHandlers.foreach.update.apply(this, arguments);
        }
    };
</script>

<ul data-bind="sortable: letters">
    <li data-bind="text: $data"></li>
</ul>

<button type="button" data-bind="click: add">Add</button>
<ul data-bind="
    sortable: {
        data: numbers,
        options: {
            handle: '.handle',
            cancel: ''
        }
    }">
    <li>
        <button type="button" class="handle">Move</button>
        <span data-bind="text: $data"></span>
    </li>
</ul>

<script type="text/javascript">
    function ViewModel() {
        var self = this;
        self.letters = ko.observableArray(["a", "b", "c"]);
        self.numbers = ko.observableArray();
        self.add = function () {
            self.numbers.push(self.numbers().length + 1);
        };
    }
    
    ko.applyBindings(new ViewModel());
</script>

Knockout Binding Handler: slide

<script type="text/javascript">
    ko.bindingHandlers.slide = {
        enabled: true,
        queue: "ko.bindingHandlers.slide",
        
        init: function (element, valueAccessor) {
            var visible = Boolean(ko.unwrap(valueAccessor()));
            $(element).toggle(visible);
        },
        
        update: function (element, valueAccessor) {
            var self = ko.bindingHandlers.slide;
            var $element = $(element);
            $element.stop(self.queue, true);
            var visible = Boolean(ko.unwrap(valueAccessor()));
            if (self.enabled) {
                var slide = visible ? "slideDown" : "slideUp";
                $element[slide]({ queue: self.queue });
                $element.dequeue(self.queue);
            } else {
                $element.toggle(visible);
            }
        }
    };
</script>

<button type="button" data-bind="click: toggle">Toggle</button>
<div data-bind="slide: visible">Hello, world!</div>

<script type="text/javascript">
    function ViewModel() {
        var self = this;
        self.visible = ko.observable(true);
        self.toggle = function () {
            self.visible(!self.visible());
        };
    }
    
    ko.applyBindings(new ViewModel());
</script>

Knockout Extender: filter

ko.extenders.filter = function (target, pattern) {
    var result = ko.computed({
        read: target,
        write: function (value) {
            if (typeof value !== "string") {
                target(value);
                return;
            }
            var current = target();
            var filtered = value.replace(pattern, "");
            if (filtered !== current) {
                target(filtered);
            } else if (value !== current) {
                target.notifySubscribers(filtered);
            }
        }
    }).extend({ notify: "always" });
    result(target());
    return result;
};

var x = ko.observable().extend({ filter: /[aeiou]/gi });
x("hello");
console.log(x());
x("WORLD");
console.log(x());

Output:

hll
WRLD

Knockout Extender: observe

ko.extenders.observe = function (target, source) {
    target(source);
    var result = ko.computed({
        read: function () {
            return target()();
        },
        write: function (value) {
            target()(value);
        }
    });
    result.observe = function (source) {
        target(source);
    };
    return result;
};

var x = ko.observable(10);
var y = ko.observable(20);
var z = ko.observable().extend({ observe: x });
console.log(z());
x(11);
console.log(z());
y(21);
console.log(z());
z.observe(y);
console.log(z());
x(12);
console.log(z());
y(22);
console.log(z());
z(30);
console.log(z());
console.log(y());

Output:

10
11
11
21
21
22
30
30

Knockout Extender: numeric

function log(value) {
    console.log(typeof value + ": " + value);
}

ko.extenders.numeric1 = function (target) {
    return ko.computed({
        read: target,
        write: function (value) {
            var number = parseFloat(value);
            target(isNaN(number) ? value : number);
        }
    });
};

var x = ko.observable().extend({ numeric1: {} });
x(1);
log(x());
x("2");
log(x());
x("three");
log(x());

ko.extenders.numeric2 = function (target) {
    target.value = ko.computed(function () {
        return parseFloat(target());
    });
    return target;
};

var y = ko.observable().extend({ numeric2: {} });
y("4");
log(y());
log(y.value());
y("five");
log(y());
log(y.value());

Output:

number: 1
number: 2
string: three
string: 4
number: 4
string: five
number: NaN

Knockout Extender: coalesce

ko.extenders.coalesce = function (target, defaultValue) {
    var result = ko.computed({
        read: function () {
            return target() === undefined ? ko.unwrap(defaultValue) : target();
        },
        write: function (value) {
            target(value);
        }
    });
    result.defaultValue = ko.computed(function () {
        return ko.unwrap(defaultValue);
    });
    return result;
};

var x = ko.observable().extend({ coalesce: 1 });
console.log(x());
x(2);
console.log(x());
console.log(x.defaultValue());
x(undefined);
console.log(x());
var y = ko.observable(3);
var z = ko.observable().extend({ coalesce: y });
console.log(z());
y(4);
console.log(z());

Output:

1
2
1
1
3
4

Knockout Extender: cache

ko.extenders.cache1 = function (target) {
    target.cached = ko.observable(target.peek());
    target.cache = function () {
        target.cached(target());
    };
    return target;
};

var x = ko.observable(1).extend({ cache1: {} });
x(2);
console.log(x());
console.log(x.cached());
x.cache();
console.log(x.cached());

ko.extenders.cache2 = function (target) {
    var cached = ko.observable(target.peek());
    var result = ko.computed({
        read: cached,
        write: function (value) {
            target(value);
        }
    });
    result.current = target;
    result.cache = function () {
        cached(target());
    };
    return result;
};

var y = ko.observable(3).extend({ cache2: {} });
y(4);
console.log(y.current());
console.log(y());
y.cache();
console.log(y());

Output:

2
1
2
4
3
4