Search code examples
javascriptjqueryknockout.jsko.observablearray

Knockout, Web API & SignalR - Uncaught TypeError


I'm trying to set a value to an element of my observableArray, and get an error i can't resolve:

Uncaught TypeError: Property 'Locked' of object #<Object> is not a function 

This is my piece of code i have written:

$(function () {
    // -----------------------------------------------------------------------//
    // ** brand - model ** //
    // -----------------------------------------------------------------------//
    var Brand = function (e) {
        var self = this;
        self.Id = ko.observable(e ? e.Id : '');
        self.Name = ko.observable(e ? e.Name : '').extend({ required: true });
        self.Description = ko.observable(e ? e.Description : '');
        self.LogoId = ko.observable(e ? e.LogoId : '');
        self.Logo = (e ? (e.LogoId != null ? ko.observable(new Logo(e.Logo)) : null) : null);
        self.DisplayOrder = ko.observable(e ? e.DisplayOrder : '');
        self.Deleted = ko.observable(e ? e.Deleted : '');
        self.State = ko.observable(e ? e.State : '');
        self.DateChanged = ko.observable(e ? e.DateChanged : '');
        self.DateCreated = ko.observable(e ? e.DateCreated : '');
        self.Locked = ko.observable(e ? e.Locked : '');

        // validation
        self.Errors = ko.validation.group(self);
        self.IsValid = function () {
            if (self.Errors().length > 0) {
                self.Errors.ShowAllMessages();
                return false;
            }
            return true;
        };
    };


    // -----------------------------------------------------------------------//
    // ** logo - model ** //
    // -----------------------------------------------------------------------//
    var Logo = function (e) {
        var self = this;
        self.Id = ko.observable(e ? e.Id : '');
        self.FileName = ko.observable(e ? e.FileName : '');
        self.URL = ko.observable(e ? e.URL : '');
        self.PictureType = ko.observable(e ? e.PictureType : '');
        self.Deleted = ko.observable(e ? e.Deleted : '');
        self.State = ko.observable(e ? e.State : '');
        self.DateChanged = ko.observable(e ? e.DateChanged : '');
        self.DateCreated = ko.observable(e ? e.DateCreated : '');
    };


    // -----------------------------------------------------------------------//
    // ** view - model ** //
    // -----------------------------------------------------------------------//
    var BrandViewModel = function (hub) {
        // init
        var self = this;
        var url = "/api/brand/brands";

        // public data properties
        self.Brands = ko.observableArray([]);
        self.NewBrand = ko.observable(new Brand());

        // -----------------------------------------------------------------------//
        // ** Web API Actions ** //
        // -----------------------------------------------------------------------//

        // load
        self.Load = function () {
            // block
            self.BlockBrands();

            // Initialize the view-model
            $.ajax({
                url: url,
                type: 'GET',
                contentType: 'application/json; charset=utf-8',
                success: function (data) {
                    // add brands
                    self.Brands(data);

                    // tooltip
                    self.InitTooltip();
                },
                error: function (err) {
                    self.ShowError(err);
                },
                complete: function () {
                    self.UnblockBrands();
                }
            });
        }


        // -----------------------------------------------------------------------//
        // ** SignalR Actions ** //
        // -----------------------------------------------------------------------//

        self.LockItem = function (id) {
            // find item
            var brand = self.getBrandById(id);
            brand.Locked(true);
        }

        self.UnlockItem = function (id) {
            // find item
            var brand = self.getBrandById(id);
            brand.Locked(false);
        }

        // -----------------------------------------------------------------------//
        // ** Utilities ** //
        // -----------------------------------------------------------------------//

        self.getBrandById = function (Id) {
            return ko.utils.arrayFirst(self.Brands(), function (item) {
                if (item.Id == Id) {
                    return item;
                }
            });
        }



        self.Load();
    };

    // -----------------------------------------------------------------------//
    // ** init ** //
    // -----------------------------------------------------------------------//
    var hub = $.connection.brand;
    var brandViewModel = new BrandViewModel(hub);

    // -----------------------------------------------------------------------//
    // ** signalR ** //
    // -----------------------------------------------------------------------//


    hub.client.LockItem = function (id) {
        brandViewModel.LockItem(id);
    }

    hub.client.UnlockItem = function (id) {
        brandViewModel.UnlockItem(id);
    }

    $.connection.hub.start();

    // -----------------------------------------------------------------------//
    // ** knockout ** //
    // -----------------------------------------------------------------------//

    // knockout validation
    ko.validation.configure({
        insertMessages: true,
        decorateElement: true,
        errorElementClass: 'error'
    });

    // knockout binding
    ko.applyBindings(brandViewModel);

});

HTML - Knockout binding

<table data-bind="visible: Brands().length > 0" class="table table-striped table-bordered table-hover" id="brands">       
    <tbody data-bind="foreach: Brands">
        <tr>
            <td class="align-center">
                <!-- ko if: Logo -->
                <img data-bind="attr: { src: Logo.URL() + '?width=50&height=50' }" class="img-polaroid" />
                <!-- /ko -->
            </td>
            <td data-bind="text: Name()"></td>
            <td data-bind="date: DateChanged()" class="align-center"></td>
            <td class="align-center">
                <!-- ko ifnot: Locked -->
                <a data-bind="click: $root.Edit" class="btn blue tip" data-original-title="wijzigen"><i class="icon-edit"></i></a>
                <a data-bind="click: $root.ShowDeleteModal" class="btn red tip" data-original-title="verwijderen"><i class="icon-trash"></i></a>
                <!-- /ko -->
                <!-- ko if: Locked -->
                <div class="btn black"><i class="icon-lock"></i></div>
                <!-- /ko -->
            </td>
        </tr>
    </tbody>
</table>

Am I doing something wrong assigning the value? I've also tried to do it like this : brand.Locked = true;. This time i get no error, but knockout doesn't respond.


Solution

  • The problem is that when you're calling ko.utils.arrayFirst(self.Brands()), your Brands are no longer observables but regular javascript objects (with 'Locked' as a property and not as an observable function), and that is because when you're retrieving the data from the server and pushing it into your Brands array, you're not wrapping it with ko.observableArray.

    Try:

    success: function (data) {
                  // add brands
                  self.Brands(ko.observableArray(data));
    

    And in your getBrandById function:

    self.getBrandById = function (Id) {
                return ko.utils.arrayFirst(self.Brands(), function (item) {
                    if (item.Id() == Id) {
                        return item;
                    }
                });
    

    EDIT:

    Actually, since your Brands array contains Brand objects, ko.observableArray is not sufficient to convert the inner properties of each Brand to obversable as well. you're gonna need to use the mapping plugin, as follows:

    success: function (data) {
                 // add brands
                 self.Brands = ko.mapping.fromJS(data);
    

    More about the ko.utils.mapping plugin here.