2

The situation that I'm trying to resolve is how to map a child array of json data to my view model. The business logic is that for a given position, there is a collection of projects, with each project having a collection of experiences. If I use the knockout mapper function, I'm able to loop through and populate my experience collection, but my viewModel has computed functions, subscribed events, etc and I'm not sure how to wire up my incoming inner array to that viewModel.

The models (in an abbreviated format)

// for creating project objects
my.Project = function (selectedProject) {
    var self = this;
    self.project = ko.observable();
    self.company = ko.observable();
    self.experiences = ko.observableArray([]);
    self.hourlyRate = ko.observable();
    self.hours = ko.observable();
    self.hoursPerWeek = ko.observable();
    self.hoursTypeId = ko.observable();
    self.id = ko.observable();
    self.maxDate = ko.observable();
    self.minDate = ko.observable();
    self.minDescriptionLength = ko.observable();
    self.memberId = ko.observable();
    self.name = ko.observable(); 
    self.startDate = ko.observable();
    // non-persistable properties
    self.chartVals = ko.observableArray([]);
    self.isSelected = ko.computed(function () {
        if (typeof selectedProject === 'undefined') {
            return false;
        } else {
            return selectedProject === self;
        }
    });
    self.newExperience = ko.observable(new my.Experience());
    self.selectProject = function (p) {
        console.log(p.name());
        $("#selectedProjectName").text(p.name());
    };
    self.enableDetails = function () {
        my.vm.proficencyTip = ko.observable();
        console.log("enableDetails pow");
        return true;
    },
    self.disableDetails = function () {
        console.log('disableDetails mouse-off');
        return false;
    };
    self.isSelectedProficiency = ko.observable(false);
    self.selectedProficiency = function (p) {
        console.log('Proficiency value is ' + p.name);
    };
    self.updateProject = function () {
        my.postProjectData(self);
    };
    self.hoursTypeId.subscribe(function () {
        var endDate = new Date();
        var startDate = new Date();
        if (self.endDate() != "") {
            endDate = $.datepicker.parseDate("mm/dd/yy", self.endDate()); 
        }
        if (self.startDate() != undefined) {
            startDate = $.datepicker.parseDate("mm/dd/yy", self.startDate());
        }
        var days = (endDate - startDate) / 1000 / 60 / 60 / 24;
        var weeks = days / 7;
        if (self.hoursTypeId() == 2) {
            if (self.hoursPerWeek() > 0) {
                self.totalHours((weeks * self.hoursPerWeek()).toFixed(0));
            } else {

            }
        }
        if (self.hoursTypeId() == 1) {
            if (self.totalHours() > 0) {
                self.hoursPerWeek((self.totalHours() / weeks).toFixed(0));
            }
        }
    });
};
// for creating Position Models
my.Experience = function (selectedExperience) {
    var self = this;
    self.id = ko.observable();
    self.projectId = ko.observable();
    self.positionId = ko.observable();
    self.memberId = ko.observable();
    self.frequencyId = ko.observable();
    self.skillName = ko.observable();
    self.skillId = ko.observable();
    self.proficiencyId = ko.observable();
    self.frequencyId = ko.observable();
    self.proficiency = ko.observable();
    self.frequency = ko.observable();
    self.description = ko.observable();
    self.skill = {
        name:ko.observable()
    };
    self.proficiency.subscribe(function () {
        self.proficiencyId = self.proficiency();
        console.log('proficiency subscribed: ' + self.proficiency());
        my.setCounterHint(self.frequency(), self.proficiency(), self.description());

        var tip = "Don't just list those skills your strongest in. It's just as important to add new skills you are aquiring!";
        var result = $.grep(my.ajaxData.member.Proficiencies, function (e) { return e.Id == self.proficiency(); });
        if (result.length == 0) {
            // not found
        } else if (result.length == 1) {
            // access the foo property using result[0].foo
            tip = result[0].Name + ':\nAutonomy: ' + result[0].Autonomy + '\nContext: ' + result[0].nContext + '\nKnowledge: ' + result[0].Knowledge + '\nWorkmanship: ' + result[0].Workmanship;
        } else {
            // multiple items found
        }
        $(".proficiencyTip").attr('title', tip).attr('alt', tip);
        $(".proficiencyQuestionMark").fadeIn('slow');
    });

    self.frequency.subscribe(function () {
        self.frequencyId = self.frequency();
        console.log('frequency subscribed: ' + self.frequency());
        self.frequencyId = self.frequency();
        $("#newExperienceFrequency").val(self.frequencyId);
        my.setCounterHint(self.frequency(), self.proficiency(), self.description());
        var tip;

        $(".frequencyTip").attr('title', tip).attr('alt', tip);
        $(".frequencyQuestionMark").fadeIn('slow');
    });

    self.minDesc = ko.observable(my.getMinDescriptionLen(self.frequency(), self.proficiency()));

    self.mouseoverDescription = function () {
        $(".tip").hide(); 
        $(".descriptionQuestionMark").fadeIn('slow');
    },
    self.mouseoffDescription = function () { 
        $(".descriptionQuestionMark").delay(3000).fadeOut("slow");
    };

    });
};


The function where I'm loading the data.
loadProjectsForPosition = function (position) {
    // if we have no projects, add one 
    if (position.projects.length == 0) {
            position.projects.push(new my.Project()
                        .company(position.company())
                        .companyName(position.companyName())
                        .endDate(position.endDate())
                        .experiences([])
                        .hourlyRate(position.hourlyRate())
                        .hours(position.hours())
                        .maxDate(position.maxDate())
                        .minDate(position.minDate())
                        .memberId(position.memberId())
                        .description("Summarize the project and its objectives here.")
                        .name('New')
                        .positionId(position.positionId())
                        .startDate(position.startDate())
                        .totalHours(position.totalHours())
                );

        $.each(my.ajaxData.member.Projects, function (i, p) { 
            if (p.PositionId == position.id()) {
                position.projects.push(new my.Project(position.projects[0])
                        .chartVals(p.ChartVals)
                        .company(p.Company)
                        .companyName(p.CompanyName)
                        .creditMinutes(p.CreditMinutes)
                        .description(p.Description)
                        .endDate(p.EndDate)
                        **.experiences(p.Experiences)** 
                        .hourlyRate(p.HourlyRate)
                        .hours(p.Hours)
                        .hoursPerWeek(p.HoursPerWeek)
                        .hoursTypeId(p.HoursTypeId)
                        .id(p.Id)
                        .maxDate(p.MaxDate)
                        .minDate(p.MinDate)
                        .minDescriptionLength(p.MinDescriptionLength)
                        .memberId(p.MemberId)
                        .name(p.Name)
                        .positionId(p.PositionId)
                        .startDate(p.StartDate)
                        .totalCompensation(p.TotalCompensation)
                        .totalHours(p.TotalHours)
                        .weightedHours(p.WeightedHours)
                        .isSelectedProficiency(false)
                );
                position.selectedPosition = true;
            }
        });
    };
}

The grandchild has children and grandchildren as well, so as much as I'm searching for an answser to this issue, I'm looking for what the pattern is in knockout mapping for dealing with 'turtles all the way down'.

As always, thanks for taking a look, sharing your experience and offering your well reasoned opinion.

1 Answer 1

4

If you are going to use the mapping plugin, then you will probably want to look at the mapping options and how to customize how objects are created. The documentation is here: http://knockoutjs.com/documentation/plugins-mapping.html#customizing_object_construction_using_create

Although, if you already have nice constructor functions for your various objects, then you may not need the mapping plugin at all.

For example, as you are passing your data through the parent constructor you can do:

var mappedExperiences = ko.utils.arrayMap(selectedProject.experiences || [], function(item) {
    return new my.Experience(item);
});

self.experiences(mappedExperiences);
Sign up to request clarification or add additional context in comments.

1 Comment

Ryan, can't thank you enough for taking time out of your Sunday to share your experience and wisdom. You're the patron saint of Knockout.js and finder of lost developers! That arraymap technique and your pithy explanation worked like a charm.

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.