简体   繁体   中英

How do you get a Knockout observable array to work with a jQuery dialog

I am new to javascript and knockout. Last week I spent all day trying to get a knockout observable to work with a jquery dialog for editing and adding new values to the array of items. I got SO close but in the end could not get it to work exactly.

My question has two parts.

I have created a JSFiddle that perfectly re-creates my problem.

This fiddle demonstrates that I can edit an existing discount code just fine.

I can also add a discount code. However when I add or edit a second (or third, etc) NEW discount code ALL of the NEW ones get edited. It does add the new one but the first NEW one gets the same values as the second new item.

Here is the fiddle...

http://jsfiddle.net/sethspearman/2jxtpw7s/28/

First Question. Can you get this fiddle to work? I think the fix could be simple. What do I need to change.

Second Question. I know that this fiddle example is NOT making best usage of knockout bindings.

For example, I am setting and then adding or editing using a vm.CurrentDiscountCode observable. However, I know that knockout has a $data binding context that could and should be used instead. I could never figure out how to get it to work. I figured out how to EDIT existing items using the $data context but could not figure out how to add new ones.

So to sum it up.
What is the BARE minimum of change to get the fiddle working? And what is the OPTIMAL way to get this working?

HERE IS THE CODE from the fiddle.

EDIT Fiddle is now working. Did not work when using HTTPS.

HTML

<body>
    <h1>Discount Codes Test</h1>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-debug.js"></script>
<script id="editTmpl" type="text/html">
    <div>
        <ul>
            <li>
                <div class="inline-item">
                    <input type="hidden" name="codeId" id="codeId" data-bind="value: $parent.currentId" />
                </div>
            </li>
            <li>
                <div class="inline-item w4 right-text">
                    <span class="label auto">Title</span>
                </div>
                <div class="inline-item">
                    <input type="text" name="description" id="description" data-bind="value: $parent.currentDescription" />
                </div>
            </li>
            <li>
                <div class="inline-item w4 right-text">
                    <span class="label auto">Code</span>
                </div>
                <div class="inline-item">
                    <input type="text" name="discountCode" id="discountCode" data-bind="value: $root.currentCode, keypressvalidator: $root.currentCode, visible: $parent.currentItemIsNew() == true" />
                    <label data-bind="text: $parent.currentCode, visible: $parent.currentItemIsNew() == false"></label>
                </div>
            </li>
            <li>
                <div class="inline-item w4 right-text">
                    <span class="label auto">Discount Amount</span>
                </div>
                <div class="inline-item">
                    <input type="text" name="amount" id="amount" data-bind="value: $root.currentDiscountAmount, visible: $parent.currentItemIsNew() == true" />
                    <label data-bind="text: $parent.currentDiscountAmount, visible: $parent.currentItemIsNew() == false"></label>
                </div>
            </li>
            <li>
                <div class="inline-item w4 right-text">
                    <span class="label auto">Max Usages</span>
                </div>
                <div class="inline-item">
                    <input type="text" name="maxUsages" id="maxUsages" data-bind="value: $root.currentMaxUsages, visible: $parent.currentItemIsNew() == true" />
                    <label data-bind="text: $parent.currentMaxUsages, visible: $parent.currentItemIsNew() == false"></label>
                </div>
            </li>
            <li>
                <div class="inline-item w4 right-text">
                    <span class="label auto">Number of Usages</span>
                </div>
                <div class="inline-item">
                    <label data-bind="text: $parent.currentNumberOfUsages"></label>
                </div>
            </li>
            <li>
                <div class="inline-item w4 right-text">
                    <span class="label auto">Active</span>
                </div>
                <div class="inline-item">
                    <input type="checkbox" name="isActive" id="isActive" data-bind="checked: $root.currentActive" />
                </div>
            </li>
        </ul>

        <button data-bind="jqButton: {}, click:$root.accept">Accept</button>
        <button data-bind="jqButton: {}, click:$root.cancel">Cancel</button>
    </div>
</script>    


<!--
**************************************
-->   
        <div>
            <fieldset data-regmode="printed">
                <div>
                    <h3>Add or Edit Discount Codes</h3>
                    <div id="discountCodes"></div>
                    <table>
                        <thead>
                            <tr>
                                <th>Title</th>
                                <th>Code</th>
                                <th>DiscountAmount</th>
                                <th>MaxUsages</th>
                                <th>Active</th>
                                <th>&nbsp;</th>
                            </tr>
                        </thead>
                        <tbody data-bind="foreach: DiscountCodes">
                            <tr style="cursor: pointer">
                                <td data-bind="text: Description"></td>
                                <td data-bind="text: Code"></td>
                                <td data-bind="text: DiscountAmount"></td>
                                <td data-bind="text: MaxUsages"></td>
                                <td data-bind="text: NumberOfUsages"></td>
                                <td data-bind="text: Active"></td>
                                <td><button type="button" data-bind="click: $root.editDiscountCode">Edit</button></td>
                            </tr>
                        </tbody>
                        <tfoot>
                            <tr> 
                                <td colspan="6"><button type="button" id="create-code" data-bind="click: createDiscountCode">Add New</button></td>
                            </tr>
                        </tfoot>
                    </table>
                </div>  
            </fieldset>
        </div>
        <div id="details" data-bind="jqDialog: { autoOpen: false, resizable: false, modal: true, height: 400, width:350, title:'Add/Edit Discount Code' }, template: { name: 'editTmpl', data: currentDiscountCode, if: currentDiscountCode }, openDialog: currentDiscountCode"></div>


</body>

JAVASCRIPT

//custom binding to initialize a jQuery UI dialog
ko.bindingHandlers.jqDialog = {
    init: function (element, valueAccessor) {
        var options = ko.utils.unwrapObservable(valueAccessor()) || {};

        //handle disposal
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            $(element).dialog("destroy");
        });

        //dialog is moved to the bottom of the page by jQuery UI. Prevent initial pass of ko.applyBindings from hitting it
        setTimeout(function () {
            $(element).dialog(options);
        }, 0);
    }
};

var currentDialog;
//custom binding handler that opens/closes the dialog
ko.bindingHandlers.openDialog = {
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        if (typeof value == 'undefined') {
            return;
        }
        currentDialog = $(element);
        if (value) {
            $(element).dialog("open");
        } else {
            $(element).dialog("close");
        }
    }
};

//custom binding to initialize a jQuery UI button
ko.bindingHandlers.jqButton = {
    init: function (element, valueAccessor) {
        var options = ko.utils.unwrapObservable(valueAccessor()) || {};

        //handle disposal
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            $(element).button("destroy");
        });

        $(element).button(options);
    }
};

var vm = {};
var DiscountCode = function (description, code, discountAmount, maxUsages, numberOfUsages, active) {
    this.Id = ko.observable(0);
    this.Description = ko.observable(description)
    this.Code = ko.observable(code);
    this.DiscountAmount = ko.observable(discountAmount);
    this.MaxUsages = ko.observable(maxUsages);
    this.NumberOfUsages = ko.observable(numberOfUsages);
    this.Active = ko.observable(active);
}

var code1 = new DiscountCode('Give a 25% discount','DISC25',25, 5, 0, true); 
var code2 = new DiscountCode('Give a 10% discount','DISC10',10,20, 0, true); 
var code3 = new DiscountCode('Give a 5% discount' ,'DISC05', 5,30, 0, true); 

var discountCodes = ko.observableArray([code1,code2,code3]);

vm.DiscountCodes = discountCodes;

vm.currentId = ko.observable();
vm.currentDescription = ko.observable();
vm.currentCode = ko.observable().extend({ codeConverter: 0 });
vm.currentDiscountAmount = ko.observable();
vm.currentMaxUsages = ko.observable();
vm.currentNumberOfUsages = ko.observable();
vm.currentActive = ko.observable();

vm.currentDiscountCode = ko.observable();
vm.revertableDiscountCode = ko.observable();
vm.currentItemIsNew = ko.observable(false);

vm.editDiscountCode = function (discountCodeToEdit) {
    vm.revertableDiscountCode(discountCodeToEdit);
    vm.currentDiscountCode(discountCodeToEdit);
    vm.currentItemIsNew(false);
    setCurrent(discountCodeToEdit);
};

vm.createDiscountCode = function() {
    vm.currentDiscountCode(new DiscountCode("", "", 0, 0, 0, true));
    vm.currentItemIsNew(true);
    setCurrent(vm.currentDiscountCode());
};

function setCurrent(discountCode) {
    vm.currentId(discountCode.Id());
    vm.currentDescription (discountCode.Description());
    vm.currentCode(discountCode.Code());
    vm.currentDiscountAmount ( discountCode.DiscountAmount());
    vm.currentMaxUsages(discountCode.MaxUsages());
    vm.currentNumberOfUsages(discountCode.NumberOfUsages());
    vm.currentActive  (discountCode.Active());
}

vm.removeDiscountCode = function(discountCodeToRemove) {
    vm.DiscountCodes.remove(discountCodeToRemove);
}

vm.accept = function() {
    var currentItem = vm.currentDiscountCode();

    if (vm.currentItemIsNew()) {
        vm.DiscountCodes.push({
            Id: vm.currentId,
            Description: vm.currentDescription,
            Code: vm.currentCode,
            DiscountAmount: vm.currentDiscountAmount,
            MaxUsages: vm.currentMaxUsages,
            NumberOfUsages: vm.currentNumberOfUsages,
            Active: vm.currentActive
        });
        vm.currentItemIsNew(false);
    } else {
        currentItem.Id(vm.currentId());
        currentItem.Description(vm.currentDescription());
        currentItem.Code(vm.currentCode());
        currentItem.DiscountAmount(vm.currentDiscountAmount());
        currentItem.MaxUsages(vm.currentMaxUsages());
        currentItem.NumberOfUsages(vm.currentNumberOfUsages());
        currentItem.Active(vm.currentActive());
    }

    vm.currentDiscountCode("");
}

vm.cancel = function() {
    vm.currentDiscountCode(vm.revertableDiscountCode);
    currentDialog.dialog("close");
    vm.currentDiscountCode("");
}

ko.applyBindings(vm);

CSS

body {
    font-size: 62.5%;
}

label, input {
    display: block;
}

    input.text {
        margin-bottom: 12px;
        width: 95%;
        padding: .4em;
    }

fieldset {
    padding: 0;
    border: 0;
    margin-top: 25px;
}

h1 {
    font-size: 1.2em;
    margin: .6em 0;
}

div#users-contain {
    width: 350px;
    margin: 20px 0;
}

    div#users-contain table {
        margin: 1em 0;
        border-collapse: collapse;
        width: 100%;
    }

        div#users-contain table td, div#users-contain table th {
            border: 1px solid #eee;
            padding: .6em 10px;
            text-align: left;
        }

.ui-dialog .ui-state-error {
    padding: .3em;
}

.validateTips {
    border: 1px solid transparent;
    padding: 0.3em;
}

Thanks in advance for your help.

Seth

In response to your question: "what is the OPTIMAL way to get this working?"

I would not start with custom binding handlers. Start with a viewModel with well-defined methods (most of which you have). In your methods, just manually execute the jquery to open and close the dialog. Get that working first. Once you have it working, you can move it back into a bindingHandler if you think it's worthwhile.

Changing this...

if (vm.currentItemIsNew()) {       
   vm.DiscountCodes.push({
        Id: vm.currentId,
        Description: vm.currentDescription,
        Code: vm.currentCode,
        DiscountAmount: vm.currentDiscountAmount,
        MaxUsages: vm.currentMaxUsages,
        NumberOfUsages: vm.currentNumberOfUsages,
        Active: vm.currentActive
    });

to this...

if (vm.currentItemIsNew()) {
    vm.DiscountCodes.push(new DiscountCode(
        vm.currentDescription(),
        vm.currentCode(),
        vm.currentDiscountAmount(),
        vm.currentMaxUsages(),
        vm.currentNumberOfUsages(),
        vm.currentActive()
    ));

...did the trick.
HT to Ryan Niemeyer (knockmeout.com) for the solution

And to see a greatly simplified version...

http://jsfiddle.net/gh6mzrxp/34/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM