0

I am trying to make a base view model for my app but I am struggling with accessing the context of the base viewmodel.

Here is my base viewmodel:

define(["config", 'services/logger'], function (config, logger) {
    'use strict';
    var
    app = require('durandal/app'),
    baseViewModel = function () {
        this.items = ko.observableArray();
        this.title = ko.observable();
        this.selectedItem = ko.observable();
    };
    baseViewModel.prototype = (function () {
        var
        populateCollection = function (initialData, model) {
            var self = this;
            if (_.isEmpty(self.items())) {
                ko.utils.arrayForEach(initialData, function (item) {
                    // self here works for all extending modules such as users
                    self.items.push(new model(item));
                });
            }
        },
        deleteItem = function (item) {
            // "this" here same as prototype gives me window object :(
            // so removing never happes
            this.items.remove(item);
            logger.log(config.messages.userMessages.confirmDeleted(item.Name()), {}, '', true);
        },
        confirmDelete = function (item) {
            var
            userMessage = config.messages.userMessages.confirmDelete(item.Type(), item.Name()),
            negation = config.confirmationModalOptions.negation,
            affirmation = config.confirmationModalOptions.affirmation;

            app.showMessage(userMessage, 'Deleting ' + item.Type(), [affirmation, negation]).then(
                function (dialogResult) {
                    dialogResult === affirmation ? deleteItem(item) : false;
                });
        };
        return {
            populateCollection: populateCollection,
            confirmDelete: confirmDelete,
            deleteItem: deleteItem
        }
    })();
    return baseViewModel;
});

and an example of where I am using this non-working thing is:

define(['services/logger', 'models/user', 'viewmodels/baseviewmodel', 'services/dataservice'], function (logger, user, baseviewmodel, dataservice) {
    var
    users = new baseviewmodel();
    users.title('Users')
    users.searchTerm = ko.observable().extend({ persist: users.title() + '-searchterm' });
    users.activate = function () {
        this.populateCollection(dataservice.getUsers, user.model);
    }
    return users;
});

Items do get populated correctly using populateCollection. confirmDelete also gets bound correctly in the template which is probably due to not needing context but the deleteItem needs the context so it can access items and call remove on it.

How do I correctly access this as the context of the baseViewModel so I can easily refer to it in my methods with this pattern?

Many Thanks

makeitmorehuman
  • 11,287
  • 3
  • 52
  • 76
  • Your view isn't here but I hope you use something like option 4 in this post: http://stackoverflow.com/a/8707661/1641941 As that attaches only one handler and will delegate to the right function depending on the `event.target`. Don't know about knockout but this is relevant for any list of items that need to be clicked. It's best not to attach a handler on the item but on the container. – HMR Oct 24 '13 at 03:03

1 Answers1

1

edit: it looks like maybe this isn't being bound correctly in the deleteItem because it's being called in a callback inside of confirmDelete. So try this snippet on your original model

    confirmDelete = function (item) {
        var self=this,
        userMessage = config.messages.userMessages.confirmDelete(item.Type(), item.Name()),
        negation = config.confirmationModalOptions.negation,
        affirmation = config.confirmationModalOptions.affirmation;

        app.showMessage(userMessage, 'Deleting ' + item.Type(), [affirmation, negation]).then(
            function (dialogResult) {
                dialogResult === affirmation ? self.deleteItem.call(self,item) : false;
            });
    };

orrr, my other solution.

I've never used the 'revealing prototype pattern' before, so here's how to do it without adhering to a pattern that neither of us understand :)

I declare everything as a property of my viewmodel and use var self = this, that way it's extremely explicit what all my code does. Here's your baseviewmodel, rewritten. It's generally helpful if all of your viewmodel logic is encapsulated within the viewmodel, and that the viewmodel declares its dependencies in the constructor/function definition.

baseViewModel = function (dataService,userModel) {
    var self = this;
    self.items = ko.observableArray();
    self.title = ko.observable();
    self.searchTerm = ko.observable().extend({ persist: self.title() + '-searchterm' });
    self.selectedItem = ko.observable();

    self.populateCollection = function (initialData, model) {
        if (_.isEmpty(self.items())) {
            ko.utils.arrayForEach(initialData, function (item) {
                // self here works for all extending modules such as users
                self.items.push(new model(item));
            });
        }
    };

    self.activate = function () {
        this.populateCollection(dataservice.getUsers, userModel);
    }

    self.deleteItem = function (item) {
        // "this" here same as prototype gives me window object :(
        // so removing never happes
        self.items.remove(item);
        logger.log(config.messages.userMessages.confirmDeleted(item.Name()), {}, '', true);
    };

    self.confirmDelete = function (item) {
        var
        userMessage = config.messages.userMessages.confirmDelete(item.Type(), item.Name()),
        negation = config.confirmationModalOptions.negation,
        affirmation = config.confirmationModalOptions.affirmation;

        app.showMessage(userMessage, 'Deleting ' + item.Type(), [affirmation, negation]).then(
            function (dialogResult) {
                dialogResult === affirmation ? self.deleteItem(item) : false;
            });
    };
};

Note that because I also moved your dependencies into the viewmodel constructor, now your initialization code looks like:

define(['services/logger', 'models/user', 'viewmodels/baseviewmodel', 'services/dataservice'], function (logger, user, baseviewmodel, dataservice) {
    var users = new baseviewmodel(dataservice,user.model);
    users.title('Users')
    return users;
});
scaryman
  • 1,880
  • 1
  • 19
  • 30
  • 1
    Love that first sentence, pattern that we both don't understand :) lol I know this one mate, I will have quit a few of these so need to save memory and prototype will keep the shared ones as one method in memory so a big difference for me. Will give a vote up though – makeitmorehuman Oct 23 '13 at 21:34
  • added an idea from reading the code more throughly, check it out. – scaryman Oct 23 '13 at 21:39
  • The idea looks interesting, today I was trying something like that by passing the self from populateCollection instead of confirmDelete like you have but that resulted in another extender's module to have wrong items after a delete. Will try this tomorrow and get back to you – makeitmorehuman Oct 23 '13 at 21:45
  • 1
    In your second example it will take more memory and cpu to create a baseViewModel instance but the confirmDelete function that calls showMessage does not need to pass a closure to it. I would suggest leaving all the other functions on the protype of baseViewModel and only move confirmDelete in the baseViewModel's body. Or pass a closure/(use bind) to showMessage as you've done in the first example. http://stackoverflow.com/a/19068438/1641941 – HMR Oct 24 '13 at 02:06