简体   繁体   中英

Knockout - strategies to get and set selectbox values in templates, when sharing an observableArray?

I have just started experimenting with JavaScript, JQuery and Knockout.js (I'm normally working on the server-side in Java) and it's beyond brilliant - however I have hit a wall, and I'm extremely doubtful that I'm doing, whatever I'm trying to do, 'by the book'.

I have created a fiddle for it here: http://jsfiddle.net/kcyhw/

My mission:

  • I need to create a template that can be re-used all over the website. Fx. a template that contains the possibility to create, edit and delete users - which would make it easy to create users in a "User configuration"-window, or as a part of a wizard. Either way, the logic controlling everything should be the same. However, besides sharing the same arrayObservable for data, the selection choices should of course not observe each other. Right now, it's entirely a select box. I'm using JQuery.serialize to convert an entire form to key-value, to be sent to a server, so it's important that I not only get the value, but also have it "saved" in the value attribute on the select box.

My problem:

  • I simply can't figure out how Knockout.js and the select box is connected. All the objects are displayed fine by their respective values (id and fullname), both in the select box, and in the properties section. When using serialize with jQuery it just prints: "perselect="... so it doesn't get a value.

I tried the following:

  • Using optionValue in the data-bind - it works, and it binds to the "value", however, I can see that it "takes over" my binding, and kills the text I retrieve from the object. I removed it, and continued...

  • Computed values, however it didn't work out since the template wants (after my knowledge) a literal object, and functions can't reference other properties in such an object.

  • Created my own binding, so I could get a reference to both the element (the select box) and all the other binding values. In this function I tried, using jQuery, to set the attribute 'value' on the element which was passed in, however, it doesn't work. I can also see, that the binding gets called 4 times (that probably because it calls init and then update for each template I created which contains the select box).

To me, it looks like I have created a friggin' mess, and I would really appreciate if some smart people could point me into the right direction of how to solve this. Resources, code-snippets, advice... whatever you got.

The code:

<html>

<head>
<script src="javascript/jquery-1.10.2/jquery-1.10.2.js"></script>
<script src="javascript/knockout-2.3.0/knockout-2.3.0.js"></script>
<script src="javascript/knockout.mapping-master-2.0/knockout.mapping-latest.js"></script>
<script type="text/javascript" src="javascript/json2-2.0/json2.js"></script>
<title>A Knockout Demo</title>


<script>
    /**
     * JQuery Function
     */
    $(document).ready(function() {
        // Domain Object
        var Person = function(id, fullname) {
            var self = this;
            self.id = id;
            self.fullname = fullname;
        };

        // Knockout Model
        var KoModel = function() {
            var self = this;

            // Declare observables
            self.persons = ko.observableArray();

            // Allows observables to share an array without observing each other
            self.createPersonSelector = function(namevalue) {
                var person = new Object();
                person.selectedPerson = ko.observable();
                person.name = namevalue;
                return person;
            }

            // Prints a serialized string which could be sent to the server
            self.printFormElements = function(formElements) {
                alert($(formElements).serialize());
            }

            // Will change the person select value, to a real value
            self.changePersonSelectValue = function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                var value = valueAccessor(), allBindings = allBindingsAccessor();

                // Next, whether or not the supplied model property is observable, get its current value
                var valueUnwrapped = ko.unwrap(value);

                // Now manipulate the DOM element
                var $eleme = $(element);

                if ($eleme == null) {
                    return;
                }

                // Change to item number two in the list *doesn't work*.
                $eleme.val(2);
            };

            // Person selectbox value binding
            ko.bindingHandlers.personSelect = {
            init : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                self.changePersonSelectValue(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
            },
            update : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                self.changePersonSelectValue(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
            }
            };

            // Put some test-data into the array
            self.persons.push(new Person(1, 'Martin Smith'));
            self.persons.push(new Person(2, 'Andy Gregersen'));
            self.persons.push(new Person(3, 'Thomas Peep'));
        };

        // Apply bindings
        ko.applyBindings(new KoModel());
    });
</script>

<script type="text/html" id="person-template">
<span>Choose ID: </span><select data-bind="options: $root.persons, optionsText: 'id', personSelect: true, value:selectedPerson, attr: {'name': name, 'id': name}"></select></br>
<span>ID:</span> <span data-bind="text: selectedPerson().id"></span></br>
<span>Full Name: </span> <span data-bind="text: selectedPerson().fullname"></span></br>
</script>
<body>
    <h1>Person Select One</h1>
    <form data-bind="submit: printFormElements">
        <div
            data-bind="template: { name: 'person-template', data:createPersonSelector('personselect')}"></div>
        <button type="submit">Submit</button>
        </br>
    </form>

    <h1>Person Select Two</h1>
    <form data-bind="submit: printFormElements">
        <div
            data-bind="template: { name: 'person-template', data:createPersonSelector('personselecttwo')}"></div>
        <button type="submit">Submit</button>
        </br>
    </form>
</body>

</html>

The easiest way for me to answer is by changing quite a few things that I might do differently. The main issue that's holding you back is that you're using jQuery for things that can be handled by KO in a much easier way.

Below are the things I'd change, to see the full result have a look at this fiddle (which doesn't use jQuery at all).

Simplify your model to something like this:

var KoModel = function() {
    var self = this;

    // Declare observables
    self.persons = ko.observableArray();

    // Prints a serialized string which could be sent to the server
    self.printFormElements = function() {
        alert(ko.mapping.toJSON(self));
    }

    // Hold selected person
    self.selectedPersons = ko.observableArray();
};

A few things to note:

  • The "print" function now uses the mapping plugin , which is great for serializing view models;
  • It's much, much shorter. The "CreatePersonSelector" and "changePersonSelectValue" functions won't be needed anymore, nor do the custom bindings;
  • The selectedPersons is now an observable, and an array at that because the view could potentially be a multi-select;
  • On a side note, I've placed adding the test values to outside the ViewModel;

This corresponds to the following View for starting off a template:

<div data-bind="template: { name: 'person-template' }"></div>

I've removed the data bit for now. This means each instance of this code would bind to (the same) $root view model. If you don't want that I'd suggest creating a container view model to hold several KoModel s.

The template looks like this:

<span>Choose ID: </span>
<select data-bind="options: persons, optionsText: 'fullname', selectedOptions: selectedPersons"></select><br />
<!-- ko foreach: selectedPersons -->
<span>ID:</span> <span data-bind="text: id"></span><br />
<span>Full Name: </span> <span data-bind="text: fullname"></span><br />
<!-- /ko -->

Here's the jist:

  • The data-bind is much simpler. You don't need to fiddle with value attributes because Knockout will bind each option to a specific item in your array;
  • This leaves you free to use the fullname for the text;
  • The selectedOptions bit tells Knockout where to store selected items in your view model;
  • The selected options are shown in a foreach because the select could potentially be multiple select.

Now the ko.mapping.toJSON(self) call in the view model will generate something like this:

{
    "persons": [{
        "id": 1,
        "fullname": "Martin Smith"
    }, {
        "id": 2,
        "fullname": "Andy Gregersen"
    }, {
        "id": 3,
        "fullname": "Thomas Peep"
    }],
    "selectedPersons": [{
        "id": 2,
        "fullname": "Andy Gregersen"
    }]
}

As you can see the list of selected persons is there, to be sent to the server. The rest is there by default, but the mapping plugin can be configured to great detail.

Hope this helps and solves your problem!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM