Почему установка optionsValue нарушает обновление Knockout? - PullRequest
0 голосов
/ 02 июля 2018

Я проходил уроки по нокауту, и я играл с одним уроком, когда что-то меня озадачило. Вот мой HTML:

<h2>Your seat reservations</h2>

<table>
    <thead><tr>
        <th>Passenger name</th><th>Meal</th><th>Surcharge</th>
    </tr></thead>
    <tbody data-bind="foreach: seats">
        <tr>
            <td><input data-bind="value: name" /></td>
            <td><select data-bind="options: $root.availableMeals, optionsValue: 'mealVal', optionsText: 'mealName', value: meal"></select></td>
            <td data-bind="text: formattedPrice"></td>
        </tr>    
    </tbody>
</table>

<button data-bind="click: addSeat">Reserve another seat</button>

... и вот мой JavaScript:

// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);

    self.formattedPrice = ko.computed(function() {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";        
    });
}

// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealVal: "STD", mealName: "Standard (sandwich)", price: 0 },
        { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 },
        { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }
    ];    

    // Editable data
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);

    // Operations
    self.addSeat = function() {
        self.seats.push(new SeatReservation("", self.availableMeals[0]));
    }
}

ko.applyBindings(new ReservationsViewModel());

Когда я запускаю этот пример и выбираю другое «Питание» из раскрывающегося меню для пассажира, значение «Доплата» равно , а не обновлено. Причина этого, по-видимому, заключается в том, что я добавил optionsValue: 'mealVal' в атрибут data-bind для select, и когда я его удаляю, «Доплата» действительно обновляется, когда выбрана новая выпадающая опция. Но почему добавление optionsValue нарушает обновление? Все, что он делает, это устанавливает атрибуты value списка select, что весьма полезно для отправки формы - я не понимаю, почему это должно препятствовать автоматическому обновлению Knockout.

ОБНОВЛЕНИЕ: После дальнейшего изучения я обнаружил, что formattedPrice fn все еще вызывается, но self.meal() теперь преобразуется в строку значений, такую ​​как PRM вместо целого Объект питания Но почему это? В документации говорится, что optionsValue устанавливает атрибут value в HTML, но ничего не говорится об изменении поведения модели представления.

Я думаю, что происходит, когда вы указываете options: $root.availableMeals, но не указываете optionsValue, Knockout волшебным образом определяет, какой выбор в списке вы сделали, когда выбор был изменен, и дает вам доступ к объект из availableMeals вместо просто строкового значения, которое было введено в атрибут value. Это не выглядит хорошо документированным.

Ответы [ 2 ]

0 голосов
/ 03 июля 2018

Хорошо, после просмотра кода Knockout я выяснил, что происходит, и на момент написания статьи это не задокументировано.

Привязка value, когда она читает значение элемента select, не просто смотрит на значение DOM для элемента; звонит var elementValue = ko.selectExtensions.readValue(element);

Теперь, что неудивительно, selectExtensions реализует специальное поведение для select (и их дочерних object) элементов. Вот где происходит волшебство, потому что, как говорится в комментарии в коде:

    // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
    // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
    // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns.

Итак, когда привязка значения пытается прочитать элемент select через selectExtensions.readValue(...), он придет к следующему коду:

        case 'select':
            return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;

Это в основном говорит: «Хорошо, найдите выбранный индекс и снова используйте эту функцию, чтобы прочитать элемент option по этому индексу. Затем он читает элемент option и приходит к следующему:

        case 'option':
            if (element[hasDomDataExpandoProperty] === true)
                return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
            return ko.utils.ieVersion <= 7
                ? (element.getAttributeNode('value') && element.getAttributeNode('value').specified ? element.value : element.text)
                : element.value;

Aha! Таким образом, он хранит свой собственный флаг «имеет свойство DOM data expando», и если он установлен, он НЕ получает простой element.value, но идет в свою собственную память JavaScript и получает значение. Вот как он может вернуть сложный объект JS (например, объект еды в примере с моим вопросом) вместо только строки атрибута value. Однако, если этот флаг не установлен, он действительно просто возвращает строку атрибута value.

Расширение writeValue, как и ожидалось, имеет другую сторону этого, где оно будет записывать сложные данные в память JS, если это не строка, но в противном случае оно просто сохранит их в атрибутной строке value для option

    switch (ko.utils.tagNameLower(element)) {
        case 'option':
            if (typeof value === "string") {
                ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
                if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
                    delete element[hasDomDataExpandoProperty];
                }
                element.value = value;
            }
            else {
                // Store arbitrary object using DomData
                ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
                element[hasDomDataExpandoProperty] = true;

                // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
                element.value = typeof value === "number" ? value : "";
            }
            break;

Так что, как я и подозревал, Knockout хранит сложные данные за кулисами, но только когда вы просите его сохранить сложный объект JS. Это объясняет, почему, когда вы не указываете optionsValue: [someStringValue], ваша вычисляемая функция получает сложный объект еды, тогда как, когда вы его указываете, вы просто передаете основную строку - Knockout просто дает вам строку из option s value атрибут.

Лично я думаю, что это должно быть ЯВНО задокументировано, потому что это немного неожиданное и особенное поведение, которое может сбить с толку, даже если это удобно. Я попрошу их добавить это в документацию.

0 голосов
/ 02 июля 2018

Я думаю, вы понимаете, что происходит и почему это нарушает ваш код, но все еще ищете объяснение, когда вам действительно нужно использовать optionsValue, а когда нет.

Когда использовать optionsValue привязку

Допустим, ваши блюда могут быть распроданы, и вы хотите проверить обновления на сервере availableMeals:

const availableMeals = ko.observableArray([]);
const loadMeals = () => getMeals().then(availableMeals);
const selectedMeal = ko.observable(null);

loadMeals();

ko.applyBindings({ loadMeals, availableMeals, selectedMeal });

function getMeals() {
  return {
    then: function(cb) {
      setTimeout(cb.bind(null, [{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 }, { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 }, { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }]), 500);  
    }
  }

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<select data-bind="options: availableMeals, 
                   value: selectedMeal,
                   optionsText: 'mealName'"></select>
                   
<button data-bind="click: loadMeals">refresh meals</button>

<div data-bind="with: selectedMeal">
  You've selected <em data-bind="text: mealName"></em>
</div>

<div data-bind="ifnot: selectedMeal">No selection</div>

<p>Make a selection, click on refresh and notice the selection is lost when new data arrives.</p>

Что происходит при замене объектов в availableMeals:

  • Knockout повторно отображает параметры поля выбора
  • Knockout проверяет новые значения для selectedMeal() === mealObject
  • Knockout не находит объект в selectedMeal и по умолчанию использует первый параметр
  • Knockout записывает ссылку на новый объект в selectedMeal

Проблема: вы теряете свой выбор пользовательского интерфейса, потому что объект, на который он указывает, больше не находится в доступных опциях.

optionsValue на помощь!

optionsValue позволяет нам решить эту проблему. Вместо хранения ссылки на объект , который может быть заменен в любое время, мы храним значение примитива , строку внутри mealVal, которая позволяет нам проверять равенство между разные вызовы API! Нокаут теперь делает что-то вроде:

selection = newObjects.find(o => o["mealVal"] === selectedMeal());

Давайте посмотрим на это в действии:

const availableMeals = ko.observableArray([]);
const loadMeals = () => getMeals().then(availableMeals);
const selectedMeal = ko.observable(null);

loadMeals();

ko.applyBindings({ loadMeals, availableMeals, selectedMeal });

function getMeals() {
  return {
    then: function(cb) {
      setTimeout(cb.bind(null, [{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 }, { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 }, { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }]), 500);  
    }
  }

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<select data-bind="options: availableMeals, 
                   value: selectedMeal,
                   optionsText: 'mealName',
                   optionsValue: 'mealVal'"></select>
                   
<button data-bind="click: loadMeals">refresh meals</button>

<div data-bind="if: selectedMeal">
  You've selected <em data-bind="text: selectedMeal"></em>
</div>

<div data-bind="ifnot: selectedMeal">No selection</div>

<p>Make a selection, click on refresh and notice the selection is lost when new data arrives.</p>

Минусы optionsValue

Обратите внимание, как мне пришлось переписать with привязку? Внезапно у нас есть только одно из свойств meal, доступных в нашей модели представления, что весьма ограничивает. Здесь вам нужно будет выполнить дополнительную работу, если вы хотите, чтобы ваше приложение могло обновлять свои данные. Ваши два варианта:

  1. Сохраните строку (хэш) по вашему выбору и фактический объект независимо, или
  2. Имейте хранилище моделей представлений, когда новые данные сервера поступают, сопоставьте с существующими экземплярами, чтобы обеспечить сохранение состояний выбора.

Если это поможет, я мог бы добавить фрагменты кода, чтобы лучше объяснить эти два подхода

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...