Отображение вложенного объекта как наблюдаемого из сложного JSON с помощью обратного вызова create

У меня сложный объект в формате JSON. Я использую Knockout Mapping, настраивая обратный вызов create и пытаюсь убедиться, что каждый объект, который должен быть наблюдаемым, действительно будет отображен как таковой.

Следующий код является примером того, что у меня есть: он позволяет пользователю добавлять cartItems, сохранять их (как JSON), очищать корзину и затем загружать сохраненные элементы.

Часть загрузки не работает: она не отображает загруженный параметр (т.е. загруженный cartItemName). Я предполагаю, что это связано с некоторым несоответствием между объектами в списке параметров и объектом, ограниченным как cartItemName (см. Это post), но я не могу этого понять.

Код (скрипка):

var cartItemsAsJson = "";
var handlerVM = function () {
  var self = this;
  self.cartItems = ko.observableArray([]);
  self.availableProducts = ko.observableArray([]);
  self.language = ko.observable();
  self.init = function () {
    self.initProducts();
    self.language("english");
  }
  self.initProducts = function () {
    self.availableProducts.push(
      new productVM("Shelf", ['White', 'Brown']),
      new productVM("Door", ['Green', 'Blue', 'Pink']),
      new productVM("Window", ['Red', 'Orange'])
    );
  }
  self.getProducts = function () {
    return self.availableProducts;
  }
  self.getProductName = function (product) {
    if (product) {
      return self.language() == "english" ? 
        product.productName().english : product.productName().french;
    }
  }
  self.getProductValue = function (selectedProduct) {
    // if not caption
    if (selectedProduct) {
      var matched = ko.utils.arrayFirst(self.availableProducts(), function (product) {
        return product.productName().english == selectedProduct.productName().english;
      });
      return matched;
    }
  }
  self.getProductColours = function (selectedProduct) {
    selectedProduct = selectedProduct();
    if (selectedProduct) {
      return selectedProduct.availableColours();
    }
  }
  self.addCartItem = function () {
    self.cartItems.push(new cartItemVM());
  }
  self.emptyCart = function () {
    self.cartItems([]);
  }
  self.saveCart = function () {
    cartItemsAsJson = ko.toJSON(self.cartItems);
    console.log(cartItemsAsJson);
  }
  self.loadCart = function () {
    var loadedCartItems = ko.mapping.fromJSON(cartItemsAsJson, {
      create: function(options) {
        return new cartItemVM(options.data);
      }
    });
    self.cartItems(loadedCartItems());
  }
}

var productVM = function (name, availableColours, data) {
  var self = this;
  self.productName = ko.observable({ english: name, french: name + "eux" });
  self.availableColours = ko.observableArray(availableColours);
}
var cartItemVM = function (data) {
  var self = this;
  self.cartItemName = data ?
     ko.observable(ko.mapping.fromJS(data.cartItemName)) :
     ko.observable();
  self.cartItemColour = data ?
     ko.observable(data.cartItemColour) :
     ko.observable();
}
var handler = new handlerVM();
handler.init();
ko.applyBindings(handler);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://rawgit.com/SteveSanderson/knockout.mapping/master/build/output/knockout.mapping-latest.js
"></script>
<div>
  <div data-bind="foreach: cartItems">
    <div>
      <select data-bind="options: $parent.getProducts(),
                optionsText: function (item) { return $parent.getProductName(item); },
                optionsValue: function (item) { return $parent.getProductValue(item); }, 
                optionsCaption: 'Choose a product',
                value: cartItemName"
      >
      </select>
    </div>
    <div>
      <select data-bind="options: $parent.getProductColours(cartItemName),
                optionsText: $data,
                optionsCaption: 'Choose a colour',
                value: cartItemColour,
                visible: cartItemName() != undefined"
      >
      </select>
    </div>
  </div>
  <div>
    <button data-bind="text: 'add cart item', click: addCartItem" />
    <button data-bind="text: 'empty cart', click: emptyCart" />
    <button data-bind="text: 'save cart', click: saveCart" />
    <button data-bind="text: 'load cart', click: loadCart" />
  </div>
</div>

Что нужно изменить, чтобы это исправить?

PS: у меня есть еще один фрагмент кода (см. Его здесь), который демонстрирует стойкость выбранное значение даже после изменения параметров - хотя там optionsValue - это простая строка, а здесь - объект.

РЕДАКТИРОВАТЬ:

Я понял проблему: вызов ko.mapping.fromJS(data.cartItemName) создает новый объект productVM, который не является одним из объектов внутри массива availableProducts. В результате ни одна из опций не соответствует productVM, содержащемуся в загруженном cartItemName, поэтому Knockout полностью очищает значение и передает undefined.

Но остается вопрос: как это исправить?


person OfirD    schedule 21.05.2018    source источник


Ответы (1)


При переходе с ViewModel -> plain object -> ViewModel вы теряете связь между продуктами в вашей корзине и продуктами в вашей handlerVM.

Распространенное решение - при загрузке простого объекта вручную искать существующие модели просмотра и вместо этого ссылаться на них. То есть:

  • Мы создаем новый cartItemVM из простого объекта
  • Внутри его cartItemName есть объект, которого нет в handlerVM.
  • Мы ищем в handlerVM продукт, похожий на этот объект, и заменяем этот объект тем, который мы находим.

В коде внутри loadCart перед установкой новых моделей просмотра:

loadedCartItems().forEach(
    ci => {
      // Find out which product we have:
      const newProduct = ci.cartItemName().productName;
      const linkedProduct = self.availableProducts()
          .find(p => p.productName().english === newProduct.english());

      // Replace the newProduct by the one that is in `handlerVM`
      ci.cartItemName(linkedProduct)
    }
)

Сценарий: https://jsfiddle.net/7z6010jz/

Как видите, сравнение на равенство некрасиво. Мы ищем english название продукта и используем его для определения соответствия. Вы также можете увидеть разницу в том, что можно наблюдать, а что нет.

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

person user3297291    schedule 30.05.2018
comment
Спасибо. Мне, конечно, интересно увидеть ваш рефакторинг. Я обновил скрипку тем, что, как я думал, вы имели в виду. Это? - person OfirD; 31.05.2018