0

I have a Knockout viewmodel which is being created by the ko.mapping plugin.

var infoNeeded = [{
  "ProductPKUID":10069,
  "ProductID":"SW42",
  "ProductName":"Display Name of Product",
  "Quantity":1,
  "NeededInfo":[
    {
      "InfoTypeNeeded":0,
      "InfoGathered":null
    },
    {
      "InfoTypeNeeded":1,
      "InfoGathered":null}]}];
infoNeeded = ko.mapping.fromJS(infoNeeded);

Note that in this example, the viewmodel only contains one product, but it's an array that could contain more. That viewmodel is being applied to this block of HTML, which seems to be working.

<div id="additionalInfoNeeded" data-bind="foreach: infoNeeded">
    <span data-bind="text: ProductID"></span> - <span data-bind="text: ProductName"></span>
    <span data-bind="visible: Quantity > 1, text: '(Qty: ' + Quantity + ')'"></span>
    <div data-bind="foreach: NeededInfo">
        <div>
            Needs Info Type: <span data-bind="text: InfoTypeNeeded"></span>
            <div data-bind="if: (InfoTypeNeeded() === 1)">
              <species-edit params="value: $data, prodID: $parent.ProductID()"></species-edit>
            </div>
        </div>
    </div>
</div>

Notice I am using a custom element (species-edit) there if the information needed for this product in the array is infoType = 1. I want the species-edit element to grab a list of species from the server which relate to this productID, then allow the user to pick from that list. Then I want my main viewmodel to be able to see the selected species. The species json coming back from the server looks like this.

[{"ID":1,"Name":"BLUE CRAB","Chosen":false},{"ID":2,"Name":"EEL","Chosen":false},{"ID":5,"Name":"INSHORE FISH","Chosen":false},{"ID":6,"Name":"OFFSHORE FISH","Chosen":false},{"ID":7,"Name":"OTHER","Chosen":false},{"ID":8,"Name":"SHARK","Chosen":false},{"ID":9,"Name":"SHRIMP OR PRAWN","Chosen":false},{"ID":12,"Name":"AMERICAN SHAD","Chosen":false},{"ID":28,"Name":"SHELLFISH(OYSTER,CLAM,MUSSEL","Chosen":false}]

Here is my species-edit.html:

<div>
    <div>What type of species will the customer attempt to take?</div>
    <div>Insert species valid for product <span data-bind="text: productID"></span> here.
    </div>
    List of species is <span data-bind="text: data.speciesList().length"></span>
    <div class="speciesList" data-bind="if: data.speciesList().length > 0">
        <div class="header">
            <div>Unselected</div>
            <div>Selected</div>
        </div>
        <div data-bind="foreach: data.speciesList">
            <input type="checkbox" data-bind="checked: Chosen"/>
            <label data-bind="text: Name"></label>
        </div>
    </div>
</div>

And my species-edit.js:

define(['knockout'],
    function (ko) {
        function ProductSpeciesSelectionViewModel(params) {
            var self = this;
            self.productID = params.prodID;

            self.data = params.value;
            self.data.speciesList = ko.observableArray([]);
            self.data.speciesList.push({Name: "Herpdy Derp", Chosen: false});

            console.debug("gonna ask for " + window.applicationBaseUrl + "AdditionalInformation/SpeciesFor/" + self.productID());
            var jqxhr = $.getJSON(window.applicationBaseUrl + "AdditionalInformation/SpeciesFor/" + self.productID())
                .success(function (data, status, xhr) {
                    console.debug(data);
                    self.data.speciesList.push({ Name: "Test", Chosen: false });
                })
                .error(function() { alert("Error in Species Grabber!"); })
                .complete(function() { console.log("Fetch Complete"); });
        }

        return ProductSpeciesSelectionViewModel;
    });

If you notice in the species-edit viewmodel, I'm not actually using the species list returned by the server. I'm initializing the list with a "Herpdy Derp" specie, and then when the getJSON call is successful I'm adding a "Test" specie. However, the custom element isn't displaying either of those. It is receiving and displaying the productID, and it is receiving a list of length==1 (Should be 2?), but not rendering any property of the list item(s). Here is the resultant text on the screen.

SW42 - Display Name of Product
Needs Info Type: 0
Needs Info Type: 1
What type of species will the customer attempt to take?
Insert species valid for product SW42 here.
List of species is 1
Unselected
Selected

I am confused by this. It sees the speciesList as having a length of one, but is not rendering the one item. I'm also confused why that list is not length == 2, since the getJSON call has succeeded by this point.

This is my first time using custom knockout elements and I clearly haven't gotten it right. Any help would be appreciated.

2
  • 1
    The first thing that popped out at me was this bit: <div data-bind="if: (InfoTypeNeeded() === 1"> Commented Jan 6, 2017 at 21:56
  • That was a copy/paste error when pasting into Stack Overflow. The original code was "if: (InfoTypeNeeded() === @((int)InfoCollector.Species))", which asp.net was translating into "if: (InfoTypeNeeded() === 1)". I missed the trailing parenthesis when pasting, but it's there. Will update original post. Commented Jan 9, 2017 at 14:20

1 Answer 1

0

In addition to Jeff Mercado's comment about the unclosed parens, the second issue is that your JSON call isn't getting reached because of an error on console.debug right above it. You haven't defined self.PrivilegeID

Once those two things are fixed the result includes both items

function viewModel(){
  var self = this;
  self.infoNeeded = ko.mapping.fromJS([
      {
        "ProductPKUID":10069,
        "ProductID":"SW42",
        "ProductName":"Display Name of Product",
        "Quantity":1,
        "NeededInfo":[
          {"InfoTypeNeeded":0,"InfoGathered":null},
          {"InfoTypeNeeded":1,"InfoGathered":null}
        ]
      }
    ]);
}
    
ko.components.register('species-edit', {
        viewModel: function(params){
            var self = this;
            self.productID = params.prodID;

            self.data = params.value;
            self.data.speciesList = ko.observableArray([]);
            self.data.speciesList.push({Name: "Herpdy Derp", Chosen: false});

            setTimeout(function () {
                self.data.speciesList.push({ Name: "Test 2", Chosen: false });
            }, 2000);
            self.data.speciesList.push({ Name: "Test 1", Chosen: false });
        },
        template: `
        <div>What type of species will the customer attempt to take?</div>
        <div>
          Insert species valid for product <span data-bind="text: productID"></span> here.
        </div>
        List of species is <span data-bind="text: data.speciesList().length"></span>
        <div class="speciesList" data-bind="if: data.speciesList().length > 0">
            <div class="header">
                <div>Unselected</div>
                <div>Selected</div>
            </div>
            <div data-bind="foreach: data.speciesList()">
              <div style="border:1px dashed blue">
                <input type="checkbox" data-bind="checked: Chosen">
                <label data-bind="text: Name"></label>
              </div>
            </div>
        </div>
        `
    });
    

        
 ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>


<div id="additionalInfoNeeded" data-bind="foreach: infoNeeded">
    <span data-bind="text: ProductID"></span> - <span data-bind="text: ProductName"></span>
    <span data-bind="visible: Quantity > 1, text: '(Qty: ' + Quantity + ')'"></span>
    <div data-bind="foreach: NeededInfo">
        <div>
            Needs Info Type: <span data-bind="text: InfoTypeNeeded"></span>
            <div data-bind="if: (InfoTypeNeeded() === 1)"><species-edit params="value: $data, prodID: $parent.ProductID()"></species-edit></div>
        </div>
    </div>
</div>

EDIT:

The main issue after looking at the pastebin links appears to be with the way you are loading your dependencies. You're loading knockout.js in a script tag at the top, and then you're telling require.js to load knockout separately in its paths config. So anything using 'ko' on the index page is using a different instance of knockout than anything using ko within your components.

Sign up to request clarification or add additional context in comments.

8 Comments

My apologies for both of those copy/paste errors. The original post has been updated. Thank you for the gomix link. It's strange to me that it's working here, but not in my implementation. I do see a few minor differences, but nothing that jumps out to me as a problem. My component is being registered with requireJS, whereas yours is inline. My second item in speciesList is being added inside of a callback, whereas yours is in the viewModel initializer.
However, when I inspect the Knockout context of both of our pages (using the "Knockoutjs context debugger" extension for chrome) I see a glaring difference in our parent viewmodels. In your viewmodel, infoNeeded[0].NeededInfo[1].speciesList exists. In mine, it does not. I'm not sure why that is.
I can't exactly reproduce your ajax call, but I replaced my inline one with a timer to simulate the callback. That still works so it's not a problem with the asynchronicity of the item being added. I'm afraid I'm not having much luck reproducing your problem. Any chance you can post more of your code?
I've created a reproduction in the form of three static files. The main HTML file, the viewmodel, and the template. You can see from running ko.html that the main view renders properly, and the species-edit.html template renders, but not correctly.
in species-edit.html the foreach binding needs parens. foreach:data.speciesList(). I think any time you have to dig into a context deeper than the current context you have to fully unwrap the observables. If you're binding to an observable in the current context it's automatic but since your observable is a child of data you have to be explicit
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.