Shopping Cart logic (?) trouble with KnockoutJS

好久不见. 提交于 2020-01-02 07:29:20

问题


The Goal

Make a dynamic list of products.

The Scenario

I have a shopping application with products. When I click on the add button of a product, I want to display in the sidebar, the product what I have added.

Summarized Problem (You just need to read this)

I have the following code in my ProductsSummary/Index.cshtml (using Razor Engine):

<ul class="summary">
    @if (Session["ProductsSummary"] == null)
    {
        <li class="is-empty">
            <p>Your summary is empty.</p>
        </li>
        <li data-bind="attr: { 'data-product-id': id }">
            <div class="product-summary-actions float-right">
                <button class="btn btn-danger btn-mini remove-item">
                    <i class="icon-remove"></i>
                </button>
            </div>
            <div class="product-summary-quantity">
                <h6 data-bind="text: infoComposition"></h6>
            </div>
            <div class="product-summary-description">
                <p data-bind="text: name"></p>
            </div>
        </li>
    }
    else
    {
        foreach 
          (var product in 
             (List<MyApp.Models.Data.getSpecificProductToShoppingList_Result>)
                Session["ProductsSummary"])
        {
        <!-- ko foreach: products -->
        <li data-product-id="@product.id">
            <div class="product-summary-actions float-right">
                <button class="btn btn-danger btn-mini remove-item">
                    <i class="icon-remove"></i>
                </button>
            </div>
            <div class="product-summary-quantity">
                <h6>
                   @product.quantity 
                   @product.measure@(@product.quantity == 1 ? "" : "s")
                </h6>
            </div>
            <div class="product-summary-description">
                <p>@product.name</p>
            </div>
        </li>
        <!-- /ko -->
        }
    }
</ul>

As you can see, there is three <li> on the code. The first is to display a message with "The summary is empty." case the session is null (and of course, the Session is null case any product has been added); the second li serves as model for Knockout when I add something when the session is null; and the last one is to display the products what are in the session.

I'm feeling a little "DRY" right here. How can I reuse the same template regardless of whether session exists or not?

Detailed Problem

I have the following code in my ProductsSummary/Index.cshtml (using Razor Engine):

<ul class="summary">
    @if (Session["ProductsSummary"] == null)
    {
        <li class="is-empty">
            <p>Your summary is empty.</p>
        </li>
        <li data-bind="attr: { 'data-product-id': id }">
            <div class="product-summary-actions float-right">
                <button class="btn btn-danger btn-mini">
                    <i class="icon-remove"></i>
                </button>
            </div>
            <div class="product-summary-quantity">
                <h6 data-bind="text: infoComposition"></h6>
            </div>
            <div class="product-summary-description">
                <p data-bind="text: name"></p>
            </div>
        </li>
    }
    else
    {
        foreach 
          (var product in 
             (List<MyApp.Models.Data.getSpecificProductToShoppingList_Result>)
                Session["ProductsSummary"])
        {
        <!-- ko foreach: products -->
        <li data-product-id="@product.id">
            <div class="product-summary-actions float-right">
                <button class="btn btn-danger btn-mini remove-item">
                    <i class="icon-remove"></i>
                </button>
            </div>
            <div class="product-summary-quantity">
                <h6>
                   @product.quantity 
                   @product.measure@(@product.quantity == 1 ? "" : "s")
                </h6>
            </div>
            <div class="product-summary-description">
                <p>@product.name</p>
            </div>
        </li>
        <!-- ko -->
        }
    }
</ul>

As you can see, there is an if that checks if ProductsSummary session exists. If yes, then the application displays on the screen a list of products that I added on my summary.

Case the session is null, as you can see, the application displays the message within the li with is-empty class.

The point is: I really need of the "template" after <li class="is-empty">[...]</li> to display an item that was added to the summary?

I mean, I know that Knockout needs something to display when I click the "Add Product" button regardless of whether or not there is a session, but I'm repeating the same template for similar purposes.

Look to this fragment:

<li data-product-id="@product.id">
    <div class="product-summary-actions float-right">
        <button class="btn btn-danger btn-mini remove-item">
            <i class="icon-remove"></i>
        </button>
    </div>
    <div class="product-summary-quantity">
        <h6>
           @product.quantity 
           @product.measure@(@product.quantity == 1 ? "" : "s")
        </h6>
    </div>
    <div class="product-summary-description">
        <p>@product.name</p>
    </div>
</li>

In this case, I'm using it within foreach because I must to display the items fetched from the database.

On the other hand, the following fragment exists if there isn't a session:

<li data-bind="attr: { 'data-product-id': id }">
    <div class="product-summary-actions float-right">
        <button class="btn btn-danger btn-mini">
            <i class="icon-remove"></i>
        </button>
    </div>
    <div class="product-summary-quantity">
        <h6 data-bind="text: infoComposition"></h6>
    </div>
    <div class="product-summary-description">
        <p data-bind="text: name"></p>
    </div>
</li>

As you can see, the both fragments are similar — one represents the data from database, and the other represents a model to work with Knockout when there is no session, respectively — and I need a way to "templatize" this.

What I Really Need

  1. Someone enters in my site/application;
  2. At the right side of my layout there is a sidebar with a message: "The summary is empty.";
  3. "Oh, what a nice product! I will add it to my summary!", then the user clicks on Add button, the "The summary is empty." message disappears and the product added by user appears in the format of an item from a list (the same template that I have passed before [first/second fragment]).
  4. "Ok, I will to another category of products now." — *The user clicks on "TVs" category* — "OH MY GOD! Look at this TV! I will add to my summary right now!" — *The user clicks on "Add Button" of a random TV.* — Already had a product in the list, but another (the TV) appears.
  5. "Oh, nevermind. I do not have money. I will remove these items from my summary." — *The user clicks on "remove button" of each product on the summary* — And without products, the summary displays: "The summary is empty." just like a magic, without any refresh or something like this.

(Funny, huh?)

The KnockoutJS Script

$(document).ready(function () {
    function Product(id, name, measure, quantity) {
        this.id = ko.observable(id);
        this.name = ko.observable(name);
        this.measure = ko.computed(function () {
            return quantity > 1 ? measure + "s" : measure;
        }, this);
        this.quantity = ko.observable(quantity);
        this.infoComposition = ko.computed(function () {
            return this.quantity() + " " + this.measure()
        }, this);
    }

    function SummaryViewModel() {
        this.products = ko.observableArray([]);

        this.addToSummary = function (formElement) {
            var $productId = $(formElement).children("[name=productId]").val();

            var match = $(".summary")
                           .find("li[data-product-id=" + $productId + "]").length;

            if (!match) {
                var $productName = 
                        $(formElement).children("[name=productName]").val(),
                    $productMeasure = 
                        $(formElement).children("[name=productMeasure]").val(),
                    $productQuantity = 
                        $(formElement).children("[name=productQuantity]").val();

                this.products.push
                   (new Product
                       ($productId, 
                        $productName, 
                        $productMeasure, 
                        $productQuantity));

                $.ajax({
                    type: "POST",
                    url: "/ProductsSummary/Add",
                    data: 
                       { 
                         productId: $productId, 
                         productQuantity: $productQuantity 
                       }
                });
            }
        }
    };

    var summaryViewModel = new SummaryViewModel();
    ko.applyBindings(summaryViewModel);

    $("body").on("click", ".remove-item", function () {
        summaryViewModel.products.remove(ko.dataFor(this));

        $.ajax({
            type: "POST",
            url: "/ProductsSummary/Remove",
            data: { productId: $(this).closest("li").data("product-id") }
        });
    });
});

What is happening, eventually?

What I'm doing works and does not work. Technically, my code works, but I wouldn't to repeat it. Is it possible?

Technical Details

The server-side team is with C#.NET with MVC 4 and Razor Engine and the client-side team is KnockoutJS and jQuery.


回答1:


For the empty cart message, you can do this:

<li class="is-empty" data-bind="visible: products().length < 1">
    <p>Your summary is empty.</p>
</li>

For the rest, you should be able to do this (no MVC loops):

     <!-- ko foreach: products -->
     <li data-bind="attr: { 'data-product-id': id }">
        <div class="product-summary-actions float-right">
            <button class="btn btn-danger btn-mini remove-item">
                <i class="icon-remove"></i>
            </button>
        </div>
        <div class="product-summary-quantity">
            <h6 data-bind="text: infoComposition"></h6>
        </div>
        <div class="product-summary-description">
            <p data-bind="text: name"></p>
        </div>
    </li>
    <!-- /ko -->

And populate the list on the client side, even if you have items saved to the session. In your view, serialize the existing data to a JSON object and save it to a javascript variable on the page which can be read on document ready and pushed into the products observable.

var existingItems = @Html.Raw(Json.Encode((List<MyApp.Models.Data.getSpecificProductToShoppingList_Result>)Session["ProductsSummary"]));

$(document).ready(function() {
    // push existingItems into products observable.
});

Note my syntax may not be quite right on the JSON encoding.



来源:https://stackoverflow.com/questions/17304720/shopping-cart-logic-trouble-with-knockoutjs

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!