Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bind modal data to knockout model

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([{comment: "test"}]),
        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.

like image 404
Samsquanch Avatar asked Dec 23 '13 21:12

Samsquanch


People also ask

What is data-bind knockout?

Knockout's declarative binding system provides a concise and powerful way to link data to the UI. It's generally easy and obvious to bind to simple data properties or to use a single binding.

How do you trigger modals?

To trigger the modal window, you need to use a button or a link. Then include the two data-* attributes: data-toggle="modal" opens the modal window. data-target="#myModal" points to the id of the modal.

What is Model Knockout JS?

38 Lectures 2 hours. More Detail. KnockoutJS is basically a library written in JavaScript, based on MVVM pattern that helps developers build rich and responsive websites. The model separates the application's Model (stored data), View (UI) and View Model (JavaScript Representation of model).


1 Answers

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 :)

like image 167
Paul D. Avatar answered Oct 12 '22 18:10

Paul D.