0

I'm having the following problem: I have a list of items, any of which a user can click on to edit. At this time, a bootstrap modal dialog shows with fields for each of the editable values. I'm updating the values for the fields from an underlying Knockout viewmodel, so when the user edits an item on the modal dialog, they can see the field being modified in the background. So, the modifying currently works fine. However, when the dialog first opens, it doesn't have the values from the item the user selected; instead, it has the values that were loaded previously. But, when the user starts to edit a field, it instantly updates to what the value is supposed to be, and allows the user to continue editing the correct field. Not sure what's going on here.

Here is my modal:

<script id="myModal" type="text/html">
          <div class="modal-header">
            <button type="button" class="close" data-bind="click: close" aria-hidden="true">&times;</button>
            <h3 data-bind="html: header"></h3>
          </div>
          <div class="modal-body">

                  <div class="form-group">
                    <label>First Name</label>
                    <input type="text" data-bind="value: modal.firstName, valueUpdate: 'afterkeydown'" class="form-control" />
                    <label>Last Name</label>
                    <input type="text" data-bind="value: modal.lastName, valueUpdate: 'afterkeydown'" class="form-control" />
                    <label>Phone</label>
                    <input type="text" data-bind="value: modal.phone, valueUpdate: 'afterkeydown'" class="form-control" />
                    <label>Email</label>
                    <input type="text" data-bind="value: modal.email, valueUpdate: 'afterkeydown'" class="form-control" />
                </div>

          </div>
          <div class="modal-footer">
            <a href="#" class="btn" data-bind="click: close, html: closeLabel"></a>
            <a href="#" class="btn btn-primary" data-bind="click: action, html: primaryLabel"></a>
          </div>
  </script>


<!-- Create a modal via custom binding -->
        <div data-bind="bootstrapModal: modal" data-keyboard="false" data-backdrop="static"></div>

Here is the part where the list is populated via knockout:

<ul data-bind="foreach: tasks">
    <li>
        <div style="border:double">
            <div>
                <label data-bind="text: firstName"></label>
            </div>

            <div>
                <label data-bind="text: lastName"></label>
            </div>

            <div>
                <label data-bind="text: phone"></label>
            </div>

            <div>
                <label data-bind="text: email"></label>
            </div>

            <div>
                <button data-bind="click: editI.bind(this)">Edit</button>                            
                @*<button data-bind="click: $parent.removeUser">Delete</button>*@
            </div>
        </div>
    </li> 
</ul>

<form @*data-bind="submit: addUser"*@>
    <button type="submit">Add User</button>
</form>
<button @*data-bind="click: save"*@>Save</button>

Here is where I set the modal values, which works, when the knockout viewmodel is loaded:

viewModel.modal = {
    header: ko.observable("This is a modal"),
    firstName: ko.observable("a"),
    lastName: ko.observable("a"),
    phone: ko.observable("a"),
    email: ko.observable("a"),
    body: ko.observable("test body"),
    closeLabel: "Close",
    primaryLabel: "Do Something",
    show: ko.observable(false), /* Set to true to show initially */
    onClose: function () {
        viewModel.onModalClose();
    },
    onAction: function () {
        viewModel.onModalAction();
    }

Finally, here is the edit function that gets called when the modal is opened. This is where things go awry. Item elements are set to the modal viewmodel, but aren't shown in the modal until a user starts editing that item...then, bam, the item shows in the field.

    self.editI = function (item) {
        viewModel.modal.body = item.email;
        viewModel.modal.firstName = item.firstName;
        viewModel.modal.lastName = item.lastName;
        viewModel.modal.phone = item.phone;
        viewModel.modal.email = item.email;

        prevState = item;

        viewModel.modal.show(true);

Note: I've found a couple of SO posts similar to this one: how to destroy bootstrap modal window completely? They haven't really helped though.

Community
  • 1
  • 1

1 Answers1

0

After a quick glance, what jumps out is that you don't use the correct syntax to assign the values to your observables. Try this:

self.editI = function (item) {
    viewModel.modal.email(item.email()); // item.email() OR ko.unwrap(item.email) if unsure whether it is an observable you're receiving
    viewModel.modal.firstName(item.firstName()); // item.firstName() OR ko.unwrap(item.firstName) if unsure whether it is an observable you're receiving
    viewModel.modal.lastName(item.lastName()); // item.lastName() OR ko.unwrap(item.lastName) if unsure whether it is an observable you're receiving
    viewModel.modal.phone(item.phone()); // item.phone() OR ko.unwrap(item.phone) if unsure whether it is an observable you're receiving

    prevState = item;

    viewModel.modal.show(true);

Edit: observables are basically function wrappers around your variable. By assigning their value with =, you remove the observable wrapper, because you re-assign the entire variable. By using the ()-syntax, you actually CALL the wrapper function and it will handle the UI update and assigning the new value to its inner variable.

Edit 2: you can make your code a little cleaner (at least in my opinion) by using chaining:

self.editI = function (item) {
    prevState = item;
    viewModel.modal
        .email(item.email())
        .firstName(item.firstName())
        .lastName(item.lastName())
        .phone(item.phone())
        .show(true);

To wrap up: In order to give an observable a new value and see the change in the ui, use ():

var x = ko.observable(3);
x(4); // UI will now reflect 4

To get the underlying value out of an observable, use () or ko.unwrap (works if the variable is observable and also if it isn't, which is often useful)

var x = ko.observable(3);
console.log(x()); // 3
console.log(ko.unwrap(x)); // 3
console.log(ko.unwrap(3)); // 3, not giving any errors

You needed to assign the value of one observable to another, so you combine both:

var x = ko.observable(3);
var y = ko.observable(4);
x(y()); // UI will reflect x = 4
x(ko.unwrap(y)); // UI will reflect x = 4
var z = 4;
x(ko.unwrap(z)); // 4
x(z()); // Error

Edit 3: live edit in a simple way (added because of comments below this answer). First, some updates to your HTML template (notice the with-binding).

<script id="myModal" type="text/html">
      <div class="modal-header">
        <button type="button" class="close" data-bind="click: close" aria-hidden="true">&times;</button>
        <h3 data-bind="html: header"></h3>
      </div>
      <div class="modal-body">
            <div class="form-group" data-bind="with: modal.item">
                <label>First Name</label>
                <input type="text" data-bind="value: firstName, valueUpdate: 'afterkeydown'" class="form-control" />
                <label>Last Name</label>
                <input type="text" data-bind="value: lastName, valueUpdate: 'afterkeydown'" class="form-control" />
                <label>Phone</label>
                <input type="text" data-bind="value: phone, valueUpdate: 'afterkeydown'" class="form-control" />
                <label>Email</label>
                <input type="text" data-bind="value: email, valueUpdate: 'afterkeydown'" class="form-control" />
            </div>

      </div>
      <div class="modal-footer">
        <a href="#" class="btn" data-bind="click: close, html: closeLabel"></a>
        <a href="#" class="btn btn-primary" data-bind="click: action, html: primaryLabel"></a>
      </div>
</script>

Now what is this modal.item? It is basically the item you're editing. Not a copy, but the item itself! This will give you live-edit. However, we put the item inside the observable item that was created for this case and used in the with-binding. See what we do here? We created bindings in the modal template, in order not to break them we need to bind against an observable that will be filled with our item. This way we avoid your initial problem.

self.editI = function (item) {
    viewModel.modal.item(item); // viewModel.modal.item = ko.observable(null);    
    prevState = ko.toJS(item); // Because we are now editing 'item' directly, prevState will change along if we simply set it to item. So instead, I used ko.toJS to make a 'flat' copy.    
    viewModel.modal.show(true);

A 'restore' to the previous state can be done by copying back the properties of prevState into item:

self.restoreChanges = function () {
    var editingItem = viewModel.modal.item(); // Get the item we're editing out of the observable
    if (editingItem && prevState) {
        editingItem.email(prevState.email);
        editingItem.firstName(prevState.firstName);
        // Rest of properties
    }
}
Hans Roerdinkholder
  • 3,000
  • 1
  • 20
  • 30
  • Okay. Looks like a step in the right direction. When I do that, I get something like the following in the modal dialog for each textbox: function c(){if(0 –  Aug 20 '14 at 14:43
  • Right! I've edited my answer. I now realize the values you were copying into the edit-fields were ALREADY observables. However, you still can't use direct assignment (`=`) because in that way you still re-assign the variable and destroy your bindings. You have to get the underlying value from the original observable (using `()` or `ko.unwrap()`) and copy it into your other observables. See my answer for details. – Hans Roerdinkholder Aug 20 '14 at 14:46
  • Added a little general explanation to the end of the answer in case you got confused along the line. – Hans Roerdinkholder Aug 20 '14 at 14:53
  • That definitely fixed the problem of the modal not populating correctly, however, ko.unwrap() seems to break the binding to the viewmodel, and it doesn't get updated as I type in the field for that item. Any clue? –  Aug 20 '14 at 14:57
  • I still have a problem with broken bindings. –  Aug 20 '14 at 15:17
  • Okay, I'm starting to understand more and more what you are trying to achieve (I think). When you edit an item from your foreach-list, you want to get live updates and not have an explicit 'save'-step, right? Because with an explicit save-step, you would copy the properties from your modal-object back into your item, so exactly the reverse of what we have been doing. If you want live-edit, all we've been doing is not really the right way to go. It will be easiest if you remove all the fields from modal, and instead add 1 observable. I will update my answer with example code. – Hans Roerdinkholder Aug 21 '14 at 06:14