Knockout - Cascading Dropdown

by Doug Hill

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.2.0/css/font-awesome.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2-bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.2/jquery-ui.js"></script>
<div class="container-fluid">
    <section class="heirarchy">
        <div class="row">
            <div class="col-xs-12">
                 <h3>Hierarchy Filters:</h3>

            </div>
        </div>
        <div class="row">
            <section class="hrFilter">
                <div class="col-sm-1">
                    <div class="filterLabel form-group"><i class="fa fa-fw fa-sitemap"></i><span data-bind="text: hrCatLabel" data-helptip="filters-hierarchy" title="&lt;i class=&quot;fa fa-fw fa-lg fa-th-sitemap&quot;&gt;&lt;/i&gt; Hierarchy Category"></span>

                    </div>
                </div>
                <div class="col-sm-4">
                    <div class="filterValue form-group">
                        <div data-bind="css: {'noentry': hrCatSelected() === undefined, 'entry': hrCatSelected() !== undefined }, toggle: hrCatEdit, filterAnimate: hrCatEdit()" class="filterText"><span data-bind="text: hrCatSelected() === undefined ? 'nothing selected' : hrCatSelected"></span>

                        </div>
                        <div class="filterInput">
                            <select name="hrCatSelect2" data-bind="options: hrCatValues, optionsValue: 'id', optionsText: 'text', optionsCaption: '', value: hrCatSelected, uniqueName: true, select2: {allowClear: false, placeholder: 'select hierarchy category'}" class="form-control input-sm"></select>
         ...

CSS

.select2-bs-validation(@text-color: @input-color;
 @border-color: @input-border;
 @background-color: @input-bg;
 @input-color: @white;
 @hover-color: @enchanting) {
    .select2-choice, .select2-search-field {
        color: @text-color;
    }
    .select2-container-multi .select2-search-choice .select2-search-choice-close:hover, .select2-container .select2-choice .select2-search-choice-close:hover {
        color: @text-color;
    }
    // Set the border and box shadow on specific inputs to match .select2-choice, .select2-container, .select2-container-multi, .select2-search-field, .select2-container-multi .select2-choices .select2-search-field {
        border-color: @border-color;
    }
    .select2-container.select2-dropdown-open {
        background: @input-color;
    }
    .input-group-addon {
        color: @text-color;
        border-color: @border-color;
        background-color: @background-color;
    }
    .form-control-feedback {
        color: @text-color;
        i.fa-lg {
            vertical-align: -17%;
        }
    }
}
// PRIMARY WRAPPER FOR ALL SELECT2 COMPONENTS .select2-container {
    margin: 0 !important;
    padding: 0 !important;
    border: 0;
    &.select2-container-active {
        border: 1px solid @enchanting;
        outline: 0;
        box-shadow: 0 0 6px rgba(68, 200, 245, 0.3);
        .border-radius-kit(@border-radius-base, @border-radius-base, @border-radius-base, @border-radius-base);
        .select2-choice, .select2-choices {
            border: none;
            background: transparent;
            outline: 0;
            &:focus {
                border: none !important;
                outline: 0;
            }
        }
        &.select2-container {
            height: @input-height-base;
        }
        &.select2-container-multi {
            // box-shadow: none;
        }
        .select2-choices {
            .select2-search-field {
                border: none;
            }
        }
    }
    // DROPDOWN OPEN...

JavaScript

/*global ko, jQuery*/
ko.bindingHandlers.toggle = {

    init: function (element, valueAccessor) {
        "use strict";
        var value = valueAccessor();
        if (!ko.isObservable(value)) {
            throw new Error('Toggle binding should be used only with observable values.');
        }

        ko.applyBindingsToNode(element, {
            click: function () {
                value(!value());
            }
        });
    },

    update: function (element, valueAccessor) {
        "use strict";
        ko.utils.toggleDomNodeCssClass(element, 'active', ko.unwrap(valueAccessor()));
    }
};

ko.bindingHandlers.filterAnimate = {

    update: function (element, valueAccessor) {
        "use strict";

        // WHEN VALUE CHANGES, SLOW FADE ELEMENT IN OR OUT
        var value = ko.utils.unwrapObservable(valueAccessor()),

            //SET THE ELEMENT VAR
            $element = jQuery(element),
            $el1 = $element,
            $el2 = $el1.next(),
            $el3 = $el2.next(),

            // WHETHER OR NOT VALUE IS OBSERVABLE
            valueUnwrapped = ko.unwrap(value);

        // MANIPULATE THE DOM
        if (valueUnwrapped === true) {

            $el1.slideUp(350);
            $el2.delay(450).show('drop', function () {
                jQuery(this).find('input').first().focus();
            });

        } else {

            $el2.hide('drop');
            $el1.delay(450).fadeIn(350);
            //            $el1.effect('highlight', {color: 'rgba(76, 255, 0, 0.11)'}, 2000);

        }
    }
};
ko.bindingHandlers.select2 = {

    init: function (element, valueAccessor, allBindings) {
        "use strict";

        var obj = valueAccessor(),
            lookupKey = allBindings.lookupKey,
            $element = jQuery(element),
            select2Options;

        select2Options = allBindings.get('select2') || {};

        $element.select2(select2Options);

        if (lookupKey) {

            var value =...