KnockoutJS - Linking value from a input to a datalist value

后端 未结 2 1683
囚心锁ツ
囚心锁ツ 2021-02-03 13:21

I\'m generating a datalist options based on a ko observable.

function Company(company) {
    this.id = company.Id;
    this.name = company.Name;
            


        
相关标签:
2条回答
  • 2021-02-03 13:43

    I want to display the name property, but I need to store the Id.

    Since there is currently no Knockout binding for HTML5 <datalist>, I made one.

    I tried to borrow as much as I could from the select binding, so there is support for options, optionsText and optionsValue. You can specify a target observable via value.

    ko.bindingHandlers.datalist = (function () {
        function getVal(rawItem, prop) {
            var item = ko.unwrap(rawItem);
            return item && prop ? ko.unwrap(item[prop]) : item;
        }
    
        function findItem(options, prop, ref) {
            return ko.utils.arrayFirst(options, function (item) {
                return ref === getVal(item, prop);
            });
        }
        return {
            init: function (element, valueAccessor, allBindingsAccessor) {
                var setup = valueAccessor(),
                    textProperty = ko.unwrap(setup.optionsText),
                    valueProperty = ko.unwrap(setup.optionsValue),
                    dataItems = ko.unwrap(setup.options),
                    myValue = setup.value,
                    koValue = allBindingsAccessor().value,
                    datalist = document.createElement("DATALIST");
    
                // create an associated <datalist> element
                datalist.id = element.getAttribute("list");
                document.body.appendChild(datalist);
    
                // when the value is changed, write to the associated myValue observable
                function onNewValue(newVal) {
                    var dataItems = ko.unwrap(setup.options),
                        selectedItem = findItem(dataItems, textProperty, newVal),
                        newValue = selectedItem ? getVal(selectedItem, valueProperty) : void 0;
    
                    if (ko.isWriteableObservable(myValue)) {
                        myValue(newValue);
                    }
                }
    
                // listen for value changes
                // - either via KO's value binding (preferred) or the change event
                if (ko.isSubscribable(koValue)) {
                    koValue.subscribe(onNewValue);
                } else {
                    ko.utils.registerEventHandler(element, "change", function () {
                        onNewValue(this.value);
                    });
                }
    
                // init the element's value
                // - either via the myValue observable (preferred) or KO's value binding
                if (ko.isObservable(myValue) && myValue()) {
                    element.value = getVal(findItem(dataItems, valueProperty, myValue()), textProperty);
                } else if (ko.isObservable(koValue) && koValue()) {
                    onNewValue(koValue());
                }
            },
            update: function (element, valueAccessor) {
                var setup = valueAccessor(),
                    datalist = element.list,
                    dataItems = ko.unwrap(setup.options),
                    textProperty = ko.unwrap(setup.optionsText);
    
                // rebuild list of options when an underlying observable changes
                datalist.innerHTML = "";
                ko.utils.arrayForEach(dataItems, function (item) {
                    var option = document.createElement("OPTION");
                    option.value = getVal(item, textProperty);
                    datalist.appendChild(option);
                });
                ko.utils.triggerEvent(element, "change");
            }
        };
    })();
    

    I hope I got most of this right. Corrections and improvement suggestions are welcome.

    You would use it like this:

    <input list="company" data-bind="datalist: {
        options: companies, 
        optionsValue: 'id', 
        optionsText: 'name', 
        value: selectedCompany
    }" />
    

    Notes.

    • There is no need to create a separate <datalist> in the HTML. The custom binding does that for you.
    • You must, however, supply the list="" attribute of the <input> element. I found no way of dynamically setting that in JavaScript so far.
    • value is optional, but if you supply it, it must be an observable.
    • You can still use the original value binding outside of datalist. It will contain whatever text the <input> displays (no surprises there). However, writing to the built-in value also updates the datalist value, and the other way around.
    • The datalist value has precedence and will overwrite the built-in value upon view model init. After that they stay in sync.
    • options should be an array (plain old or observable) of objects — companies in this case).
    • optionsText and optionsValue are strings that should map to properties on your options.
    • You do not have to use optionsValue — in that case the entire object (a single company) would be stored in value. I would prefer that over storing only the ID.
    • Currently the set-up reacts to the change event. That means your view model won't update until you leave the input field.
    • Tested on Chrome 32, YMMV. As I said, comments and corrections are welcome.

    Below is a demo, copied from the original fiddle.

    function main() {
      function Company(company) {
          this.id = company.Id;
          this.name = company.Name;
          this.state = company.State.name;
      }
    
      function ViewModel(sampleData) {
          var self = this;
    
          self.companies = ko.observableArray();
          ko.utils.arrayForEach(sampleData, function (companyData) {
              self.companies.push(new Company(companyData));
          });
    
          // KO's "value" binding _can_ supply a start value
          self.val = ko.observable("Microsoft");
    
          // ... but it is overridden by datalist's "value" binding - in any case you can use both
          self.selectedCompany = ko.observable(1);
      }
    
      // -----------------------------------------------------------------------------------
    
      ko.applyBindings(new ViewModel([{
          Id: 1,
          Name: "Apple",
          State: {
              name: "California"
          }
      }, {
          Id: 2,
          Name: "Microsoft",
          State: {
              name: "Washington"
          }
      }, {
          Id: 3,
          Name: "IBM",
          State: {
              name: "New York"
          }
      }]));
    }
    
    // -----------------------------------------------------------------------------------
    ko.bindingHandlers.datalist = (function () {
        function getVal(rawItem, prop) {
            var item = ko.unwrap(rawItem);
            return item && prop ? ko.unwrap(item[prop]) : item;
        }
    
        function findItem(options, prop, ref) {
            return ko.utils.arrayFirst(options, function (item) {
                return ref === getVal(item, prop);
            });
        }
        return {
            init: function (element, valueAccessor, allBindingsAccessor) {
                var setup = valueAccessor(),
                    textProperty = ko.unwrap(setup.optionsText),
                    valueProperty = ko.unwrap(setup.optionsValue),
                    dataItems = ko.unwrap(setup.options),
                    myValue = setup.value,
                    koValue = allBindingsAccessor().value,
                    datalist = document.createElement("DATALIST");
    
                // create an associated <datalist> element
                datalist.id = element.getAttribute("list");
                document.body.appendChild(datalist);
    
                // when the value is changed, write to the associated myValue observable
                function onNewValue(newVal) {
                    var dataItems = ko.unwrap(setup.options),
                        selectedItem = findItem(dataItems, textProperty, newVal),
                        newValue = selectedItem ? getVal(selectedItem, valueProperty) : void 0;
    
                    if (ko.isWriteableObservable(myValue)) {
                        myValue(newValue);
                    }
                }
    
                // listen for value changes
                // - either via KO's value binding (preferred) or the change event
                if (ko.isSubscribable(koValue)) {
                    koValue.subscribe(onNewValue);
                } else {
                    ko.utils.registerEventHandler(element, "change", function () {
                        onNewValue(this.value);
                    });
                }
    
                // init the element's value
                // - either via the myValue observable (preferred) or KO's value binding
                if (ko.isObservable(myValue) && myValue()) {
                    element.value = getVal(findItem(dataItems, valueProperty, myValue()), textProperty);
                } else if (ko.isObservable(koValue) && koValue()) {
                    onNewValue(koValue());
                }
            },
            update: function (element, valueAccessor) {
                var setup = valueAccessor(),
                    datalist = element.list,
                    dataItems = ko.unwrap(setup.options),
                    textProperty = ko.unwrap(setup.optionsText);
    
                // rebuild list of options when an underlying observable changes
                datalist.innerHTML = "";
                ko.utils.arrayForEach(dataItems, function (item) {
                    var option = document.createElement("OPTION");
                    option.value = getVal(item, textProperty);
                    datalist.appendChild(option);
                });
                ko.utils.triggerEvent(element, "change");
            }
        };
    })();
    main();
    .hint {
        color: silver;
        font-size: 80%;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
    <input list="company" type="text" placeholder="Select a company..." data-bind="value: val, datalist: {
        options: companies, 
        optionsValue: 'id',  /* try removing 'optionsValue' and see what happens to the view model */
        optionsText: 'name', 
        value: selectedCompany
    }" />
    <span class="hint">(note the "change" event occurs after the field loses focus!)</span>
    
    <hr />
    <p>View Model:</p>
    <pre data-bind="text: ko.toJSON($root, null, 2)"></pre>

    0 讨论(0)
  • 2021-02-03 13:43

    would you like something like this?

    HTML

     <select data-bind="options: companies, optionsText: 'state', value: selectedCompany, optionsCaption: 'Choose...' "></select>
     <input type="text" data-bind="value: selectedCompany() ? selectedCompany().name : 'unknown'" />
    

    JAVASCRIPT

    var Company = function (company) {
        this.id = company.Id;
        this.name = company.Name;
        this.state = company.State.name;
    }
    var viewModel = {
        selectedCompany: ko.observable(),
        companies: ko.observableArray()
    };
    
    for(var x = 0; x<5; x++){
        viewModel.companies.push(new Company({Id: x, Name:'name' + x, State: { name: 'namestate' + x}}));
    }
    ko.applyBindings(viewModel);
    

    Knockout has already some options to create this selects.. see it in documentation http://knockoutjs.com/documentation/options-binding.html

    http://knockoutjs.com/documentation/options-binding.html

    0 讨论(0)
提交回复
热议问题