Published 03 Aug, 2022

Jquery - Bind modal data to knockout model

Category Jquery
Modified : Nov 25, 2022
61

I'm trying to have a twitter bootstrap modal open to a window that has a text area in it which is editable, then on save, it saves the appropriate data. My current code:

HTML:

<table  class="display table table-striped">
    <tbody data-bind="foreach: entries">
        <tr>
            <td>
                Placeholder
            </td>
            <!-- ko foreach: entry_data -->
            <td>
                <div class="input-group">
                    <input type="text" class="form-control col-sm-2" data-bind="value: entry_hours">
                    <span class="input-group-addon"><a class="comment" data-bind="click: function() { $root.modal.comment($data); $root.showModal(); }, css: { 'has-comment': comment.length > 0, 'needs-comment': comment.length == 0 }, attr: { title: comment }"><span class="glyphicon glyphicon-comment"></span></a></span>
                </div>
            </td>
            <!-- /ko -->
        </tr>
    </tbody>
</table>

<!-- Modal template -->
<script id="commentsModal" class="modal-dialog" type="text/html">
<div class="modal-dialog">
    <div class="modal-content">
        <div class="modal-header">
            <button type="button" class="close" data-bind="click:close" aria-hidden="true">&times;</button>
            <h4 data-bind="html:header" class="modal-title"></h4>
        </div>
        <div class="modal-body">
            <textarea class="form-control" rows="3" data-bind="value: $root.modal.comment.comment"></textarea>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" data-bind="click:close,html:closeLabel">Close</button>
            <button type="button" class="btn btn-primary" data-bind="click:action,html:primaryLabel" id="save-changes">Save changes</button>
        </div>
    </div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</script>

<!-- Create a modal via custom binding -->

<div data-bind="bootstrapModal:modal" class="modal fade" id="commentsModal" tabindex="-1" role="dialog" data-keyboard="false" data-backdrop="static"></div>

JS:

/* Custom binding for making modals */
ko.bindingHandlers.bootstrapModal = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var props = valueAccessor(),
            vm = bindingContext.createChildContext(viewModel);
        ko.utils.extend(vm, props);
        vm.close = function() {
            vm.show(false);
            vm.onClose();
        };
        vm.action = function() {
            vm.onAction();
        }
        ko.utils.toggleDomNodeCssClass(element, "modal fade", true);
        ko.renderTemplate("commentsModal", vm, null, element);
        var showHide = ko.computed(function() {
            $(element).modal(vm.show() ? 'show' : 'hide');
        });
        return {
            controlsDescendantBindings: true
        };
    }
}

var entriesdata = [{"entry_id":"51794","project_id":"2571","user_id":"89","entry_data":[{"entry_data_id":"359192","entry_id":"51794","entry_hours":"0.00","entry_date":"2013-12-22","comment":""},{"entry_data_id":"359193","entry_id":"51794","entry_hours":"8.00","entry_date":"2013-12-23","comment":"Test comment"},{"entry_data_id":"359194","entry_id":"51794","entry_hours":"8.00","entry_date":"2013-12-24","comment":"Test comment"},{"entry_data_id":"359195","entry_id":"51794","entry_hours":"0.00","entry_date":"2013-12-25","comment":""},{"entry_data_id":"359196","entry_id":"51794","entry_hours":"8.00","entry_date":"2013-12-26","comment":"Test comment"},{"entry_data_id":"359197","entry_id":"51794","entry_hours":"8.00","entry_date":"2013-12-27","comment":"Test comment"},{"entry_data_id":"359198","entry_id":"51794","entry_hours":"0.00","entry_date":"2013-12-28","comment":""}]}];
var projectsdata = [{"project_txt":"Test Project","project_id":12345}];
var TimeEntriesModel = function(entries, projects) {
    var self = this;

    self.projects = ko.observableArray(projects);

    self.entries = ko.observableArray(ko.utils.arrayMap(entries, function(entry) {
        return {
                entry_id : entry.entry_id,
                project_id : entry.project_id,
                user_id : entry.user_id,
                entry_data : ko.observableArray(entry.entry_data)
                }
    }));

    self.save = function () {
        ko.utils.stringifyJson(self.entries);

    }

    self.modal = {
        header: "Add/Edit Comment",
        comment: ko.observableArray([]),
        closeLabel: "Cancel",
        primaryLabel: "Save",
        show: ko.observable(false), /* Set to true to show initially */
        onClose: function() {
            self.onModalClose();
        },
        onAction: function() {
            self.onModalAction();
        }
    }
    console.log(ko.isObservable(self.modal.comment));
    self.showModal = function() {
        self.modal.show(true);
    }

    self.onModalClose = function() {
        // alert("CLOSE!");
    }
    self.onModalAction = function() {
        // alert("ACTION!");
        self.modal.show(false);
    }

}

ko.applyBindings(new TimeEntriesModel(entriesdata, projectsdata));

Fiddle: http://jsfiddle.net/sL3HK/

As you can see in the fiddle, the modal opens with the text box, but I'm unable to figure out how to get the 'comment' text into the modal or update the comment when the 'save' button is pressed.

Any ideas?

Also, I'm very new to Knockout, so if there's anything in there that doesn't look quite right, please feel free to correct me on it.

UPDATE:

I've been fiddling with the code, and have been able to get the "comment" into the modal, but I've not been able to successfully update it up to this point. And another problem I will eventually run into is that I only want the comment to be updated when "Save" is clicked, rather than the normal update on blur. I really think I'm going about this the wrong way, but I'm not sure what the right way is. Any more help is greatly appreciated.

Updated fiddle.

Answers

There are 2 suggested solutions here and each one has been listed below with a detailed description. The following topics have been covered briefly such as Javascript, Jquery, Knockout.js, Twitter Bootstrap, Twitter Bootstrap 3. These have been categorized in sections for a clear and precise explanation.

24

Here is a JsFiddle in which you should be able to edit comment for each entry. Here is how I proceeded to obtain this.

The ViewModels

First, I like to divide my views into partials. For each type of partial, I create a ViewModel. And an "upper level" ViewModel is used as a container for all the partial ViewModels. Here you'll need a EntryDataViewModel which I defined this way :

var EntryDataViewModel = function (rawEntryData) {
    var self = this;
    self.entry_data_id = rawEntryData.entry_data_id;
    self.entry_id = rawEntryData.entry_id;
    self.entry_hours = rawEntryData.entry_hours;
    self.entry_date = rawEntryData.entry_date;
    self.comment = ko.observable(rawEntryData.comment);
} 

Basically, this constructor does the conversion from your raw data to something you will be able to manipulate in your views. Depending on what you want to do, you can make things observable or not. comment is used in some bindings and is expected to change. We want the page to react dynamically to its changes, so let's make it observable.
Because of this change, we will change the way we create the "upper level" ViewModel (here TimeEntriesModel), and in particular :

self.entries = ko.observableArray(ko.utils.arrayMap(entries, function (entry) {
    return {
        entry_id: entry.entry_id, //same as before
        project_id: entry.project_id, // same as before
        user_id: entry.user_id, // same as before
        entry_data: ko.observableArray(entry.entry_data.map(function (entry_data) {
            return new EntryDataViewModel(entry_data); // here we use the new constructor
        }))
    }
}));

Now our ViewModels are ready to be updated. So let's change the modal.

The Modal

Again, in the modal, the comment will be subject to change, and we want to retrieve its value (to update our EntryData). So it's an observable.
Now we have to inform the modal of which EntryData we are modifying (and I think this is the main part your code was lacking). We can do this by keeping a reference of the EntryData that was used to open the modal :

self.modal = {
   ...
   comment:ko.observable(""),
   entryData : undefined,
   ...
}

Last thing to do is to update all these variables when you open the modal :

self.showModal = function (entryDataViewModel) {
    // modal.comment is already updated in your bindings, but logic can be moved here.
    self.modal.entryData = entryDataViewModel; // keep track of who opened the modal
    self.modal.show(true);
}

And when you save :

self.onModalAction = function () {
    self.modal.entryData.comment(self.modal.comment()); //save the modal's comment into the entryData.
    self.modal.show(false);
}

Conclusion

I did not want to change all your bindings and code, thus there were a lot of little changes and I think you'll have to play with the code to see how they affect the behavior of the page, how it works. My solution is not perfect of course. There remains some logic in your HTML markup that must be moved to the JS and I'm not sure you really need all the custom binding stuff. Moreover, I'm not happy about the modal. The modal stuff should belong to a EntryDataViewModel since editing the comment acts on one EntryData, but as I said, I did not want to change all your code. Tell me if you have problems with my solution :).

Update (some hints for going further)

When I said "moving logic from HTML to JS", here is what I meant. The following binding looks to complicated to belong to HTML markup.

<a class="comment" data-bind="click: function() { $root.modal.comment(comment()); $root.showModal($data); }, css: { 'has-comment': comment().length > 0, 'needs-comment': comment().length == 0 }, attr: { title: comment() }">

Some things you could do : move $root.modal.comment(comment()) to showModal, then your click binding becomes click : $root.showModal. Even the "needs-comment" binding has a logic, you could add a method needsComment to your EntryDataViewModel that contains this logic.
Keep in mind that HTML markup should not contain any logic, it should just make calls to JS functions. If a function acts on an partial of the view (for example, an EntryData), then this function belongs to the partial view model (this is why I was complaining about the modal, that acts on only one EntryData but here is located in the TimesEntriesModel). If a function manipulates a set of elements (for example, if you create an "add" button), this function belongs in the container ViewModel.

This was a VERY long and specific answer. Apologies for that. You should be able to find a lot of resources on Model View ViewModel (MVVM) on the web, that will help you in your journey :)


25

For what it's worth, I wrote the knockout-modal project to make modals easier to work with when using Knockout.

Would welcome any feedback on it, and in any case I hope it is helpful to look at.


Associated Technologies

Details of all the mentioned technology in the question that you should know about for a detailed information.

Javascript

Javascript is the programming language of the web. It is client side scripting language that runs inside a browser. It's a lightweight, interpreted programming language and is very capable for network centric applications. One of the primary reasons for its popularity is also because it's fun and flexible.

Javascript is one of the core-technologies for the web alongside HTML and CSS. It is high level language with dynamic typing, first class functions and is termed as multi-paradigm as it supports event-driven, functional and imperative programming styles. It also has various APIs to work with text, dates, DOM etc.