user1732364
user1732364

Reputation: 995

Remove selected row in table from Knockout observable array

Alright i have working code that removes a selected row(s) via a checkbox being checked. However i am running into the issue of enforcing that only one of the radio buttons can be checked at any given moment. My first approach is to tie a click event to the each radio button and if it gets clicked, it loops through the observable array and marks all "false." Then it simply flips the flag to true for the item that fired the event. I know this isn't the best way but my lack luster knowledge of knockout is forcing me down this path..even though this method doesn't work atm. Can anyone shed light on what i am doing wrong or how to properly wire this up?

The html for the table

                <table class="accountGroups information" id="tblAccountGroups">
                <tr>
                    <td width="125px;" style="font-weight: bold;">StandardAccountNo</td>
                    <td width="125px;" style="font-weight: bold; text-align: center;">Primary</td>
                    <td style="font-weight: bold;">Effective Date</td>
                    <td style="font-weight: bold;">End Date</td>
                    <td style="font-weight: bold;">Remove</td>
                </tr>
                <!-- ko foreach: NewAccountGroupDetails-->
                <tr id="Model.NewAccountGroupDetails[0].AccountGroupName" class="acctgrp-row">
                    <td>
                        <div>
                            <input style="width: 100%;" data-bind="value: StandardAccountNo, attr: {name: 'NewAccountGroupDetails[' + $index() + '].StandardAccountNo'}" />
                        </div>
                    </td>
                    <td>
                        <div style="text-align:center;">
                            <input style="width:100%;" type="radio" data-bind="value: IsPrimary, attr: {name: 'NewAccountGroupDetails[' + $index() + '].IsPrimary'}, click: $parent.markIsPrimary" />
                        </div>
                    </td>
                    <td>
                        <div>
                            <input style="width:125px;" class="datepicker" data-bind="value: EffectiveDate, attr: {name: 'NewAccountGroupDetails[' + $index() + '].EffectiveDate'}" readonly="readonly" />
                        </div>
                    </td>
                    <td>
                        <div>
                            <input style="width:125px;" class="datepicker" data-bind="value: EndDate, attr: {name: 'NewAccountGroupDetails[' + $index() + '].EndDate'}" readonly="readonly" />
                        </div>
                    </td>
                    <td>
                        <div style="text-align:center;">
                            <input type="checkbox" data-bind="checked: markedForDeletion, attr: {name: 'NewAccountGroupDetails[' + $index() + '].MarkedForDeletion'}" />
                        </div>
                    </td>
                </tr>
                <!-- /ko -->
            </table>

The JS below powers the page

////VIEW MODEL FOR KNOCKOUT////
var Detail = function () {
    this.StandardAccountNo = ko.observable('');
    this.IsPrimary = ko.observable(false);
    this.EffectiveDate = ko.observable(formattedDate(new Date()));
    this.EndDate = ko.observable(formattedDate(new Date()));
    this.markedForDeletion = ko.observable(false);
};

var ViewModel = function () {
    var rawList = '@Html.Raw(new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(Model.NewAccountGroupDetails))';
    this.NewAccountGroupDetails = ko.observableArray(convertJSONToKoObservableObject($.parseJSON(rawList)));
    this.NewAccountGroupDetails.push(new Detail());

    this.deleteMarkedItems = function () {
        this.NewAccountGroupDetails.remove(function (item) {
            return item.markedForDeletion();
        });
    };

    this.markIsPrimary = function () {
        for (i = 0; this.NewAccountGroupDetails().length > 0; i++) {
            this.NewAccountGroupDetails[i].IsPrimary(false);
        }
        return item.IsPrimary(true);
    };

    this.addNew = function () {
        this.NewAccountGroupDetails.push(new Detail());

        $('.datepicker').each(function (i, obj) {
            $(obj).datepicker({ changeYear: true, changeMonth: true });
        });
    }
};

ko.applyBindings(new ViewModel());

function convertJSONToKoObservableObject(json) {
    var ret = [];
    $.each(json, function (i, obj) {
        var newOBJ = {};
        for (prop in obj) {
            newOBJ[prop] = ko.observable(obj[prop]);
        }
        ret.push(newOBJ);
    });

    return ret;
}

Once i have the page working the way i want it to, i'll look into syntax improvements such as ko mapping library for the array.

Upvotes: 3

Views: 3580

Answers (3)

Matthew James Davis
Matthew James Davis

Reputation: 12295

In your view model, construct the remove button like this:

viewModel.remove = function (row) {
    console.log(row);
    viewModel.NewAccountGroupDetails.remove(row);
};

Now, the current context is passed as the first argument to any callback in knockout. Therefore, if you add a button with data-bind="click: $parent.remove", it will call the viewModel.remove function with the row context.

<tr ...>
    ...
    <td>
        <button data-bind="click: $parent.remove">Remove</button>
    </td>
</tr>

Upvotes: 2

JotaBe
JotaBe

Reputation: 39055

I'd need some extra information, but let me show you an example, and give you a few advices:

First, the advices:

  1. in order to convert your regular object in an object with observable properties an arrays you can use the Knockout Mapping plugin.
  2. you can omit the step of parsing the JSON. You can simply assigng the JSON to a var, like this: var JSON=*your serialized JSON*; (Don't forget the semicolon at the end.
  3. instead of including so many code in the data-bind, like this: NewAccountGroupDetails['+ $index() + '].EndDate, do this calculation on the viewmodel itself, an use a computed named, for example EndDateName
  4. your viewmodel should include a selectedRow observable. When the user selects the row, put the row there, and you can use a computed observable that determines if a row is the selected row or not.
  5. take into account that you can bind events that invoke functions in your code, and this events carry the data associated to the DOM object that originated the event. I.e. if the users clicks a row associated to a account group detail, you'll receive it in the event.

Example for 2:

// Instead of:
var viewModelJson = '[{"name": "Pepe"},{"name":"Juan"}]';
var viewModel = $.parseJSON(viewModelJson);

// Do this directly:

var people = [{"name": "Pepe"},{"name":"Juan"}];

As 4 and 5 are not clear at once, this is a simple sample of what you want to achieve.

<ul data-bind="foreach: people">
    <li data-bind="text: name, click: $root.select,
       css: {red: $data == $root.selectedPerson()}" >
    </li>
</ul>    

NOTE that the css class red is applied when the condition true. And the condition is that the value bound to the current row is the same as the value in the selectedPerson observable.

And this is the corresponding JavaScript (remember to reference knockout mapping!!)

var people = [{"name": "Pepe"},{"name":"Juan"}];

var PeopleModel = function(people) {
    var self = this;
    self.selectedPerson = ko.observable();    // This will hold the selected person
    self.people = ko.mapping.fromJS(people);  // Note ko.mapping!!
    self.select = function(person) {  // event receives the current data as 1st param
        self.selectedPerson(person);
    }
    self.delete = function(person) {
        // find de index of person and remove 1 item from that index
        self.people.splice(self.people.indexOf(person),1);
    }
    return self;
};

var peopleModel = new PeopleModel(people);

ko.applyBindings(peopleModel);

You can run the jsfiddle here.

If you change the click binding to invoke $root.delete instead of $root.select, you'll see the person dissapear from the list when clicking it. Of course, you can add an extra element to do so.

NOTE: you can read the docs on click binding on knockout js site.

And a last advice: it's much better to use Web API, or a method returning a JsonResult to recover the data directly from the server, and keep the js on a separate file.

UPDATE A little bit mode code.

You can add this HTML:

<input type="button" data-bind="click: removeSelected" value="removeSelected"/>

And this method in the view model:

self.removeSelected = function() {
    if (self.selectedPerson()) {
        self.delete(self.selectedPerson());
    }
};

If you do so, when clicking the button, if there is a selected item, it will be removed from the list.

UPDATE: Another, more comple example

Here you have a more complete example, in this fiddle, that includes the code below:

CSS:

body {
    font-family: Arial;
}
.container {
    margin: 10px 0;
    border: solid 1px #ABF;
}
.container > div {
    padding: 4px;
    border: solid 1px #ABF;
    position: relative;
}
.selected {
    border: solid 1px #00A;
    color: #00A;
    background-color: #BCF;
}

HTML:

<div data-bind="foreach: people" class="container">
    <div data-bind="click: $root.select,
           css: {selected: $data == $root.selectedPerson()}" >
               <!-- ko text: name --><!-- /ko -->
               <input type="button" value="Remove" 
                   style="right:3px;top:2px; position:absolute;"
                   data-bind="click:$root.delete"/>
    </div>
</div>    

<div data-bind="visible: selectedPerson()" >
    <input type="button" data-bind="click: removeSelected" value="Remove Selected"/>
    <input type="button" data-bind="click: unSelect" value="Deselect"/>
</div>

<div data-bind="visible: selectedPerson()" class="container">
    <div>
        Selected: <!-- ko text: selectedPerson().name --><!-- /ko -->
    </div>
</div>

JavaScript:

var people = [{"name": "Pepe"},{"name":"Juan"},{"name":"Luis"},{"name":"Adolfo"}];

var PeopleModel = function(people) {
    var self = this;
    self.selectedPerson = ko.observable();    // This will hold the selected person
    self.people = ko.mapping.fromJS(people);  // Note ko.mapping!!
    self.select = function(person) { // The event receives the current data as parameter
        self.selectedPerson(person);
    };
    self.delete = function(person) {
        // find de index of person and remove (splice) it from the observable array
        self.people.splice(self.people.indexOf(person),1);
        self.selectedPerson(null);
    }
    self.removeSelected = function() {
        if (self.selectedPerson()) {
            self.delete(self.selectedPerson());
        }
    };
    self.unSelect = function() {
        self.selectedPerson(null);
    }
    return self;
};

var peopleModel = new PeopleModel(people);

ko.applyBindings(peopleModel);

Upvotes: 1

Jose M.
Jose M.

Reputation: 1316

Try to temporarily save the selected row when you select it

    function AccountGroupViewModel() {
        var viewModel = this;
        viewModel.selectedRow = null;
        // ...

        viewModel.selectRow = function (data) {
            // ...
            viewModel.selectedRow = data;
        }

        viewModel.remove = function () {
            // ...
            if (viewModel.selectedRow != null) {
                this.NewAccountGroupDetails.remove(viewModel.selectedRow);
            }
        }
    }

Upvotes: 0

Related Questions