2

Trying to understand ko.mapping in conjunction with TypeScript and RequireJS. As I understand it, I can create a view model, bind it to a view and expose a complex object to my view via the view model. I am having no luck getting this to work. Most examples on the web want to show how to take a web service response and bind it directly. I am seeking a more basic example than that - I just want to map an unbound object to the screen. I could certainly do it manually, but I think the tool was designed for this exact purpose...

I have two needs:

  1. show a value on initial display - possibly blank
  2. Use a button to change the value.

I have been playing with some sample code as a proof of concept, as the most basic version I could come up with. The idea is to present a view with a button. The text of the button should load with "Hello World!", and when clicked be updated to "Goodbye moon...".

I think my view model needs two objects...

  1. POJO
  2. binding object, instantiated to be a ko.mapping.fromJS({})

My understanding (which is likely wrong) is that the mapping will take the POJO in and automatically create an observable version of the POJO in the binding object. The view is bound to the binding object. At any time, such as a click of a button, I can augment my POJO, and reload into the binding object and my view will update accordingly.

My View Model is connected as I can set break points and watch them get hit. The loading of the page fails because the bound object is not available. If I change from ko.mapping to standard observables it loads fine.

What am I missing when considering ko.mapping? Is my approach completely flawed?


Basic POJO Class

class DefaultModel {
    public myField: string;
}
export = DefaultModel;

View

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>TypeScript HTML App</title>
        <script data-main="Application/require-config" src="Scripts/require.js"></script>
    </head>
    <body>
        <h1>TypeScript HTML App</h1>
        <button id="myMethodTest" data-bind="text: boundModel().myField, click: function () { myButton_Click() }" ></button>
    </body>
</html>

View Model

/// <reference path="../Scripts/typings/knockout/knockout.d.ts" />
/// <reference path="../Scripts/typings/knockout.mapping/knockout.mapping.d.ts" />

import DefaultModel = require("Models/DefaultModel");
import ko = require("knockout");

class DefaultViewModel {
    public basicModelInstance: DefaultModel;
    public boundModel: any;

    constructor() {
        // INSTANTIATE THE BOUND MODEL TO BE A BLANK KO MAPPED AWARE OBJECT
        this.boundModel = ko.mapping.fromJS({});

        // SETUP A BASIC INSTANCE OF A POJO
        this.basicModelInstance = new DefaultModel;
        this.basicModelInstance.myField = "Hello World!";

        // LOAD THE POPULATED POJO INTO THE BOUND OBVSERVABLE OBJECT
        this.boundModel = ko.mapping.fromJS(this.basicModelInstance, {}, this.boundModel);
    }

    myButton_Click() {
        // UPDATE THE POJO
        this.basicModelInstance.myField = "Goodbye Moon...";

        // RELOAD THE POJO INTO THE BOUND OBJECT
        this.boundModel = ko.mapping.fromJS(this.basicModelInstance, {}, this.boundModel);
    }
}
export = DefaultViewModel;

RequireJS Configuration

/// <reference path="../Scripts/typings/requirejs/require.d.ts" />

require.config({
    baseUrl: "",
    paths: {
        "jQuery": "Scripts/jquery-2.1.1",
        "knockout": "Scripts/knockout-3.2.0.debug",
        "utilities": "Application/utilities",
        "ViewModelMapper": "Application/ViewModelMapper",
        "komapping": "Scripts/knockout.mapping-latest.debug"


    },

    shim: {
        "jQuery": {
            exports: "$"
        },
        komapping: {
            deps: ['knockout'],
            exports: 'komapping'
        }
    },
});

require(["jQuery"], function ($) {
    $(document).ready(function () {
        // alert('dom ready');

        require(["utilities", "knockout", "ViewModelMapper", "komapping"], (utilities, knockout, viewModelMapper, komapping) => {
            utilities.defineExtensionMethods($);
            knockout.mapping = komapping;

            var url = window.location;
            var location = utilities.getLocation(url);
            var urlPath = location.pathname;
            var urlPathWithoutExtension = urlPath.replace(".html", "");

            var viewModel = viewModelMapper.getViewModel(urlPathWithoutExtension);
            knockout.applyBindings(viewModel);
        });
    });
});

2 Answers 2

2

I awarded this to @wired_in for the help provided. Here I will provide a working version of the code that finally resolved my issue.

My theory - if the mapping can take the result of an AJAX call and auto-magically map it to an observable, why can't any ordinary POJO? Well, it can! This basic ability is liberating. Now I am free to create models without polluting them with 'observable'. The models can behave like any ordinary object with no special handling. Manipulate the model to desire, then when needed to be represented on the view, bind it via the ko.mapping.fromJS call.

Here is the final solution. I will present it in the same order I presented the original question...


Basic POJO Class:

class DefaultModel {
    public myField: string;
}
export = DefaultModel;

View:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>TypeScript HTML App</title>
        <script data-main="Application/require-config" src="Scripts/require.js"></script>
    </head>
    <body>
        <h1>TypeScript HTML App</h1>
        <button id="myMethodTest" data-bind="text: boundModel.myField, click: function () { myButton_Click() }" ></button>
    </body>
</html>

View Model:

/// <reference path="../Scripts/typings/knockout/knockout.d.ts" />
/// <reference path="../Scripts/typings/knockout.mapping/knockout.mapping.d.ts" />

import DefaultModel = require("Models/DefaultModel");
import ko = require("knockout");


class DefaultViewModel {
    public basicModelInstance: DefaultModel;
    public boundModel: KnockoutObservable<DefaultModel>;

    constructor() {
        // SETUP A BASIC INSTANCE OF A POJO
        this.basicModelInstance = new DefaultModel;
        this.basicModelInstance.myField = "Hello World!";

        // LOAD THE POPULATED POJO INTO THE BOUND OBVSERVABLE OBJECT
        this.boundModel = ko.mapping.fromJS(this.basicModelInstance);
    }

    myButton_Click() {
        // UPDATE THE POJO
        this.basicModelInstance.myField = "Goodbye Moon...";

        // RELOAD THE POJO INTO THE BOUND OBJECT
        this.boundModel = ko.mapping.fromJS(this.basicModelInstance, this.boundModel);
    }
}
export = DefaultViewModel;

RequireJS Configuration:

/// <reference path="../Scripts/typings/requirejs/require.d.ts" />

require.config({
    baseUrl: "",
    paths: {
        "jQuery": "Scripts/jquery-2.1.1",
        "knockout": "Scripts/knockout-3.2.0.debug",
        "utilities": "Application/utilities",
        "ViewModelMapper": "Application/ViewModelMapper",
        "komapping": "Scripts/knockout.mapping-latest.debug"


    },

    shim: {
        "jQuery": {
            exports: "$"
        },
        komapping: {
            deps: ['knockout'],
            exports: 'komapping'
        }
    },
});

require(["jQuery"], function ($) {
    $(document).ready(function () {
        // alert('dom ready');

        require(["utilities", "knockout", "ViewModelMapper", "komapping"], (utilities, knockout, viewModelMapper, komapping) => {
            utilities.defineExtensionMethods($);
            knockout.mapping = komapping;

            var url = window.location;
            var location = utilities.getLocation(url);
            var urlPath = location.pathname;
            var urlPathWithoutExtension = urlPath.replace(".html", "");

            var viewModel = viewModelMapper.getViewModel(urlPathWithoutExtension);
            knockout.applyBindings(viewModel);
        });
    });
});

Conclusion:

In the end, I was stuck on 3 things...

  1. My view incorrectly referred to the binding object in the view model. Thanks to @wired_in for assistance in this area
  2. In the constructor, I was passing too many parameters. Thanks to @wired_in for pointing this out. The documentation for KnockoutJS.mapping is unclear in this area. I think the use of 1 vs. 3 parameters is optional here.
  3. In the method myButton_Click, I need to pass a reference to the existing, already bound object (A.K.A., viewmodel inside a viewmodel). This was the key to allowing updates to an existing bound model.

Now I can control when my view will change based on data manipulated under the covers. Whether or not the data is derived from an AJAX call, or internal computational manipulation, from a 3rd party system, from an uploaded file - whatever - I can now have the data visible in the view. Pretty cool.

In the end, the question - "why have data in an unbound POJO? Why not just use the bound object and manipulate it as an observable?" - I think the answer is "portability". I want the freedom to pass an ordinary object in and out of the code base without special consideration. This notion of marking an object as observable is constraint imposed by the framework - a workaround to make binding possible. It's not desirable to require applying the 'observable' attribute everywhere. Separation of concerns Baby! Anyway, off my soap box now...

Thanks @wired_in.

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

4 Comments

I'm glad you found a solution to your problem, and I appreciate the credit for the answer, however I want to elaborate on a few things. In your specific case here, this solution might be optimal. It may not seem terrible to maintain when you only have one property being changed by one button, but consider a complex UI with many properties that are changed by several different UI elements and also changed under the hood, and everything has to stay in sync.
Using your method, everytime the POJO is changed under the hood, you have to call the mapping code to manually update the bound object so that the view gets updated. On the flip side, everytime a UI element such as a text box updates the bound model, you have to manually map back to the POJO to keep everything in sync. You are defeating the purpose of data binding. I realize getting started with knockout and using the observables seems counter-intuitive and strange, but once you get used to it, it's very powerful and simple.
If you still aren't convinced and using a custom object to manipulate your data just doesn't work for you, I would suggest looking into angularJS, which offers a method of data-binding that does allow you to just use regular JS objects with no need for observables.
@wired_in - thanks for the suggestions. I understand your position on correct and best-practices usage of Knockout. I actually agree with what you are saying, especially for large projects. Best-practices was not the intent of my question. I was merely seeking knowledge how to do this one task of binding a POJO to an observable object and using ko.mapping to handle the heavy lifting of mapping (even though this may seem anti-best-practice). Just trying to expose the envelop of capabilities. With this knowledge (which was hard to come by - thx again) I am well armed for future endeavors.
1

A) In your View Model code, the call to ko.mapping.fromJS only needs the first two parameters. Since it returns the bound model, you don't need to pass in your bound model. It should be:

this.boundModel = ko.mapping.fromJS(this.basicModelInstance, {});

B) viewModel.boundModel is not a function, it's an object. So in your html, your binding text: boundModel().myField should be text: boundModel.myField

C) You are misunderstanding the way the binding is supposed to work. Once you have your bound model, there is no need to update the "POJO" and then recreate your bound model every time something in your view model changes. The two-way data binding that knockout offers will keep your view model and your ui (html) in sync, and so you only have to work with your view model from then on. When you need to take what's in your view model and update your "POJO", which should only be when you need to update the server, you can use the ko.mapping.toJS function which does the opposite of the ko.mapping.fromJS function. You pass in your bound model and it will give you back the vanilla JS "POJO" object, removing all of the observables.

7 Comments

regards to importing komapping - isn't ko.mapping a ko plugin, and as such it doesn't import autonomously, but rather it imports as part of knockout whenever knockout is imported, assuming it has been set?
It's been a while since I worked with requireJS, so actually I'm not sure the best way to handle it, but it seems like I would load the two separately because there may be cases where you use knockout but you don't need the mapping plugin, so you would be loading it for no reason.
Either way, that's not the main issue here, so I'll remove it from the answer.
regarding answer part D) - I think I understand the view is supposed to bind to the view model, and that any two-way data binding requires a variable of type observable. In it's most simple form, I would not need a POJO. Instead I would merely use the observables. My question is - if I have data in a POJO and want to use it in my view model variables (not saying how I got the POJO data), should/could I use ko.mapping, or should I translate my POJO to my observable manually?
You can use ko.mapping.fromJS to update the view model as well. You just pass in the current bound view model. When you call it this way, there is no return value (just call the function, don't assign it to anything)
|

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.