georgian98
georgian98

Reputation: 85

Javascript and Knockout

I'm new to Javascript as well as Knockout. I'm stuck at binding my ViewModal. Following is code that is working when ViewModal and View are in same Index.chtml file:

ProfilesViewModel = function () {
    self = this;
    self.ProfileId = ko.observable("");
    self.FirstName = ko.observable("");
    self.LastName = ko.observable("");
    self.Email = ko.observable("");
    self.Image = ko.observable("");
    self.Phone = ko.observable("");

    // Public data properties
    self.Profiles = ko.observableArray([]);

    GetAllProfiles();

    function GetAllProfiles() {
        //  alert("here");
        $.ajax({
            type: "get",
            url: "@Url.Action("getAllProfiles")",
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            success: function (data) {
               self.Profiles(data); //Put the response in ObservableArray
            },
            error: function (error) {
               alert(error.status + "<--and--> " + error.statusText);
            }
        });
    };
}

But When I move my ViewModal to another Js file as follows Modal.js code:

ProfilesViewModel = function () {
    self = this;
    self.ProfileId = ko.observable("");
    self.FirstName = ko.observable("");
    self.LastName = ko.observable("");
    self.Email = ko.observable("");
    self.Image = ko.observable("");
    self.Phone = ko.observable("");

    // Public data properties
    self.Profiles = ko.observableArray([]);
};

ko.applyBindings(new ProfilesViewModel()); // This makes Knockout get to work

and in Index.cshtml:

var obj = new ProfilesViewModel();

GetAllProfiles();

function GetAllProfiles() {
    //  alert("here");
    $.ajax({
        type: "get",
        url: "@Url.Action("getAllProfiles")",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function (data) {
            obj.Profiles(data); //Put the response in ObservableArray
        },
        error: function (error) {
            alert(error.status + "<--and--> " + error.statusText);
        }
    });
}

Binding is not successfull. Idont know where I'm doing mistake. Pls advice.

I would also appreciate if better modular approach is suggested.

Upvotes: 1

Views: 229

Answers (4)

user3691330
user3691330

Reputation: 1

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
  <style type="text/css">
        .Nav
        {
            border: 1px solid #cccccc;
            border-radius: 20px;
            height: auto;
            position: absolute;
            opacity: 0.8;
            margin: auto;
            top: -20px;
            /*left: -80px;*/
            vertical-align: middle;
        }

        .aside1
        {
            border: 0px solid #cccccc;
            border-radius: 20px;
            width: 250px;
            height: auto;
            position: absolute;
            opacity: 0.8;
            margin:0 -130px -130px 0;
            top: 50%;
            right: -10px;
            bottom:-90px;
            vertical-align: middle;
        }

        ul
        {
            padding: 10px;
            margin: 0px;
        }
        li
        {
            display: block;
            padding: 20px;
            margin: 0px;
            border: 1px solid #cccccc;
            background: -webkit-linear-gradient(left top, white , brown);
            background: -moz-linear-gradient(left top, white , brown);
            background: -ie-linear-gradient(left top, white , brown);
        }
        .Content
        {
            background-color: WhiteSmoke;
            border: 1px solid #CCCCCC;
            width: 80%;
            height: 500px;

            border-radius: 0px 25px 0px 25px;

            position: relative;
            text-indent: 10px;
        }
        a
        {
            text-decoration: none;
            color: Black;
            font-family:Arial;
            font-size:12px;
            font-weight:bold;

        }

        .socialsites
        {
            content: "";
            border: 1px solid #cccccc;
            border-radius: 20px;
            height: auto;
            position: absolute;
            opacity: 0.8;
            margin: auto;
            top: 50%;
            right: 150px;
            bottom:-10px;
            vertical-align: middle;
            width: auto;
        }

        .asideul
        {
            line-height: 30px;
        }

        .asidecontent
        {
            width: 20px;
            height: 20px;
            display: block;
            opacity: 0.5;
            margin: 10px;
            padding: 10px;
            color: black;
            font-size: 20px;
            text-indent: 40px;
            -webkit-border-radius: 20px;
            -moz-border-radius: 20px;
            border-radius: 20px;
            box-shadow: 1px 0px 5px rgba(0,0,0,10);
            -webkit-transition: all 0.2s ease-in-out;
            -moz-transition: all 0.2s ease-in-out;
            -o-transition: all 0.2s ease-in-out;
            transition: all 0.2s ease-in-out;
            content: "";
            background: -webkit-linear-gradient(left top, white , brown);
            background: -moz-linear-gradient(left top, white , brown);
        }
        li.asidecontent:hover
        {
            width: 200px;
            opacity: 1;
            background: Brown;
            background-color: Brown;
        }
    </style>
</head>
<body>
<center><h1>Welcome to Patient Portal</h1></center>
<br />
<div class="Content">
        <div class="Nav">
            <ul>
                <li><a href="#">Consultation Appointment</a></li>
                <li><a href="#">Service Appointment</a></li>
                <li><a href="#">Home Care Appointment</a></li>
                <li><a href="#">Ambulance Appointment</a></li>
                <li><a href="#">Clinical History</a></li>
                <li><a href="#">Deposits</a></li>
                <li><a href="#">Vouchers</a></li>
            </ul>
        </div>
        <div class="aside1">
            <div id="socialsites">
                <ul class="asideul">
                    <li class="asidecontent">Facebook</li>
                    <li class="asidecontent">Twitter</li>
                    <li class="asidecontent">Google+</li>
                </ul>
            </div>
        </div>
    </div>
</body>
</html>

Upvotes: 0

Grofit
Grofit

Reputation: 18465

I tend to use a modular approach for my view models... so I would make re-usable models and just attach them via composition in my viewmodel for each page... so for example:

function SomeModel()
{
   this.Id = ko.observable(0);
   this.Name = ko.observable("Some Default Name");
}

This can then be used by doing var someModel = new SomeModel();, this helps you separate your concerns within the pages as if you have a page which contains a login component, a user details component and maybe a products component you can express your data in a simple way and then validate them easily enough.

Then you would have a view model which looks like this:

function SomeViewModel()
{
    this.SomeModel = new SomeModel();
}

or

function SomeViewModel(someModel)
{
   this.SomeModel = someModel;
}

So then you would new it up and apply it like this:

var viewModel = new SomeViewModel();
ko.applyBindings(viewModel);

Then finally in your HTML you would use it like so:

<div data-bind="text: SomeModel.Id" />
<input data-bind="value: SomeModel.Name" />

or

<!-- ko with: SomeModel -->
<div data-bind="text: Id" />
<input data-bind="value: Name" />
<!-- /ko -->

So then this way you can isolate your logic into their own classes (controversial) and then easily compose your view models when they share similar concerns, as you can then express your login concerns via a class, then attach that to whatever viewmodel requires login functionality etc. Also this makes testing your stuff easier as you can test the actual logic on the POJO models without having to have a UI.

May not be a direct answer to your question but should hopefully help your design going forward.

Upvotes: 0

Chris Pratt
Chris Pratt

Reputation: 239430

In your external JS, you call ko.applyBindings with a ProfileViewModel defined inline, but later when you call GetAllProfiles, you're using another instance of ProfileViewModel that is not bound.

You've got a number of issues with your JS here. Since you're new, the most important thing you need to know is namespacing. Defining variables and functions in the global scope is dangerous. You want to encapsulate all your code inside a namespace custom to your application. This is as easy as:

var MyAwesomeNamespace = MyAwesomeNamespace || {};

Call it whatever you like, but try to pick something that is unique to you or your application -- something unlikely to be used by someone else. Then, once you have that, just define things as properties of your namespace:

MyAwesomeNamespace.SomeFormerlyGlobalVariable = 'foo';

Then, when dealing with things like AJAX, it's good to create a utility/service object to hold that logic. If it's only meant for this page, the convention is to name it after the main object or purpose of the page. With your circumstance, I'd do something like:

MyAwesomeNamespace.ProfileDisplay = function () {
    var _getAllProfiles = function () { ... };

    return {
        GetAllProfiles: _getAllProfiles
    }
})();

The above is called a closure. We're setting ProfileDisplay to an anonymous function that's immediately called in place (the () at the end). This will make the value of ProfileDisplay the return value of the function, so you now have the ability to have private and public members, somewhat like a more traditional object-oriented class. Anything not returned by the function is private and not accessible to the rest of your code, while the items in the returned object are public and act as an API available to the rest of your code. Now you could call your AJAX with:

MyAwesomeNamespace.ProfileDisplay.GetAllProfiles();

However, we'll now add one more thing to this, a callback parameter. Whether you'll need to make multiple calls to this AJAX endpoint or not, its not a bad idea to make it generalize so it can be used in a different scenario, if needed. The AJAX call will always be the same, but what happens on success is contextual, so that is the part we'll abstract. Here, I'm also going to switch to the less verbose, $.getJSON, since you're not doing anything crazy enough here to actually need $.ajax.

MyAwesomeNamespace.ProfileDisplay = function () {
    var _getAllProfiles = function (callback) {
        $.getJSON('/api/profiles')
            .done(callback)
            .fail(function (jqXHR, textStatus, error) {
                alert(error.status + "<--and-->" + error.statusText);
            });
    };

    return {
        GetAllProfiles: _getAllProfiles
    }
})();

That was easy. Now you have a resusable method to fetch profiles. I also threw in another feature called "promises". That's a topic to itself, which I encourage you to investigate further on your own. I'll only say that while the use here is very simple, they're extremely powerful.

Finally, you define your view model in the same way on your namespace. However, here is where we'll define the callback that will handle the AJAX response, since the viewModel inherently has access to itself, it makes it a cinch to get at the Knockout observables.

MyAwesomeNamespace.ProfileViewModel = function () {
    var self = this;

    self.UpdateProfileArray = function () {
        MyAwesomeNamespace.ProfileDisplay.GetAllProfiles(function (result) {
            self.Profiles(result);
        });
    });

    ...
}

Finally, we'll expand our utility object with an initialization method and a method to wire up page events:

MyAwesomeNamespace.ProfileDisplay = function () {
    var _init = function () {
        var viewModel = MyAwesomeNamespace.ProfileViewModel();
        ko.applyBindings(viewModel);
        _wireEvents(viewModel);
        // the initial fetch of profiles list on page load
        viewModel.UpdateProfilesArray();
    });

    var _wireEvents = function (viewModel) {
        // Imagining a button that can be clicked to refresh list of
        // profiles on demand to illustrate how you're wire everything
        // together here.
        $('#RefreshProfilesButton').on('click', viewModel.UpdateProfilesArray);
    });

    ...

    // See, here we're not returning `_wireEvents` because public
    // access is not needed
    return {
        Init: _init,
        GetAllProfiles: _getAllProfiles
    }
});

All of that code can safely go in your external JS. And finally, with that, all you need on your actual view is:

<script src="/path/to/external.js"></script>
<script>
    $(document).ready(function () {
        MyAwesomeNamespace.ProfileDisplay.Init();
    });
</script>

Much cleaner.

Finally one word about the URL for your AJAX method. Here, I hard-coded it to not distract from the discussion, but it's of course better to rely on the routing framework to give you that information. We don't have access to that from inside that external javascript file, which means we have to pass it along via the inline javascript. This is another area where having a namespace helps keep things orderly. So, first we want to initialize our namespace:

var MyAwesomeNamespace = MyAwesomeNamespace || {};

You're want to add that line first to all your javascript. It ensures that if the namespace has not been initialized, it's initialized to an empty object. The combination of = with a binary value separated by the ||, means essentially, if this object is already set, then set it to itself (basically do nothing), or if not, set it to an empty object.

Then, we can set a variable off the namespace to hold our URL. (In practice, you may want to further setup a URLs object or something off the namespace, and then specify the URL variables off of that, just to keep things extra-tidy.)

MyAwesomeNamespace.GetAllProfilesURL = '@Url.Action("getAllProfiles")';

And then modify the _getAllProfiles method:

var _getAllProfiles = function (callback) {
    $.getJSON(MyAwesomeNamespace.GetAllProfilesURL)
        .done(callback)
        .fail(function () { ... });
}

Upvotes: 5

Tony
Tony

Reputation: 7445

I think it is because you call ko.applyBindings before dom is loaded. If you use jQuery write it as follows:

$(document).ready(function() {
    ko.applyBindings(new ProfilesViewModel());
});

Also you can place your script tags at the bottom of your index.html (before body closing tag).

Upvotes: 0

Related Questions