Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make an object's properties individually editable using KnockoutJS

I'm new to KnockoutJS. I want to know if there is a better way to accomplish the functionality below.

An object's properties show up as text in a table row. I can click each span of text individually to make a textbox appear so I can edit the text. It's important to note that other properties related to the object do NOT become editable. Only one property is editable at a time. To accomplish this, during KO Mapping I actually overwrite each property with an object that has two properties: A 'value' property that holds the original property's value, and a 'hasFocus' property that I use to keep track of the visibility of the textbox.

Here's a JSFiddle to show how I currently have it programmed. Be sure to click on the Item names and prices to see the textbox appear.

Further explanation

I have a table where each row represents a TransactionItem.

In the starting state, each field is text. When clicked, the text disappears and a textbox appears. When the textbox loses focus, the textbox disappears and the modified text reappears.

The following steps show what I did to accomplish this:

  1. Map the data from the server using KO Mapping plugin ('myData' in this example),

    var  myData = [
        {
        TransactionItems: [
            {
            Name: "Item1",
            Price: "1.00"       
            },
            {
            Name: "Item2",
            Price: "2.00"       
            },
            {
            Name: "Item3",
            Price: "3.00"       
            },
        ]},
    ];
    
    var mappingOptions = {          
        'TransactionItems': {
            create: function (options) {
                return new TransactionItem(options.data);
            }
        }   
    }
    var viewModel = {};
    var self = viewModel;
    viewModel.transactions = ko.mapping.fromJS(myData, mappingOptions);
    
    ko.applyBindings(viewModel);
    
  2. Within the TransactionItems constructor function, grab the current values of the TransactionItem properties ('name', 'price'), and store them in temp variables. Overwrite the properties with new objects. These new objects contain two values: the 'value' of the original property, and a new 'hasFocus' property

    //Grab the current values of the properties
    var itemValue = this.Name();
    var priceValue = this.Price();
    
    //Recreate properties as objects, give them a 'hasFocus' property
    this.Name = { value: itemValue, hasFocus: ko.observable(false)};
    this.Price = { value: priceValue, hasFocus: ko.observable(false) };
    

For example, the 'name' property becomes the object Name: { value: 'Item1', hasFocus: false }.

  1. In HTML, data-bind the 'hasFocus' of each property to control when to show/hide the text/textbox.

  2. When text is clicked, the 'hasFocus' property is set to true, which will hide the text, and displays the textbox.

  3. When textbox blurs, the 'hasFocus' property is set to false, which hides the textbox, and displays the text.

I want to know if there is a better approach to accomplish this functionality. Have I gone way off track? Thanks!

Larger snippet of code:

<table>
    <thead>
        <tr>            
            <th>Name</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>  
        <!-- ko foreach: transactions -->
            <!-- ko template: { name: 'listItems', foreach: TransactionItems } --> 
            <!-- /ko -->
        <!-- /ko -->
    </tbody>
</table>

<script type="text/html" id="listItems">
    <tr>
        <td>
            <!-- Either show this -->
            <span data-bind="visible: !Name.hasFocus(), 
                             text: Name.value, 
                             click: editItem.bind($data, Name)"></span>

            <!-- Or show this -->
            <input data-bind="visible: Name.hasFocus, 
                              value: Name.value, 
                              hasfocus: Name.hasFocus, 
                              event: { 
                                        focus: editItem.bind($data, Name), 
                                        blur: hideItem.bind($data, Name) 
                                     }" 
            /><!-- end input -->
        </td>
        <td>
            <!-- Either show this -->
            <span data-bind="visible: !Price.hasFocus(), 
                             text: Price.value, click: editItem.bind($data, Price)"></span>

            <!-- Or show this -->
            <input data-bind="visible: Price.hasFocus, 
                              value: Price.value, 
                              hasfocus: Price.hasFocus, 
                              event: { 
                                        focus: editItem.bind($data, Price), 
                                        blur: hideItem.bind($data, Price) 
                                     }" 
            /><!--input end -->
        </td>
    </tr>
</script>

<!-- END OF HTML -->

<script >

function TransactionItem(data) {

    ko.mapping.fromJS(data, {}, this);

    this.editable = ko.observable(false);
    this.hasFocus = ko.observable(false);

    //Grab the current values of the properties
    var itemValue = this.Name();
    var priceValue = this.Price();

    //Recreate properties as objects, give them a 'hasFocus' property
    this.Name = { value: itemValue, hasFocus: ko.observable(false)};
    this.Price = { value: priceValue, hasFocus: ko.observable(false) };

    this.editItem = function (objProperty) {
        this.editable(true);
        objProperty.hasFocus(true);        
    }

    this.hideItem = function (objProperty) {
        this.editable(false);
        objProperty.hasFocus(false);
    }       

}

//MAPPING
var mappingOptions = {

    'TransactionItems': {
        create: function (options) {
            return new TransactionItem(options.data);
        }
    }   
}

//DATA
var  myData = [
    {
    TransactionItems: [
        {
        Name: "Item1",
        Price: "1.00"       
        },
        {
        Name: "Item2",
        Price: "2.00"       
        },
        {
        Name: "Item3",
        Price: "3.00"       
        },
    ]},
        ];

//VIEWMODEL        
var viewModel = {};
var self = viewModel;
viewModel.transactions = ko.mapping.fromJS(myData, mappingOptions);

ko.applyBindings(viewModel);

        </script>
like image 804
JerryM Avatar asked Mar 05 '26 04:03

JerryM


1 Answers

I think it's simpler to have fields in your root VM object that specify which item/field is currently being edited and work with that, instead of having separate observables for each item and field's editing-status.. You can put helper functions in your VM to help deal with it in your bindings.

To deal with the inputs blur I handled clicks on the document element (or any suitable element), and make sure the click events don't bubble from the editable elements. I did that with the clickBubble binding, but with jQuery (or alternative) it might be easier.

JSFiddle: http://jsfiddle.net/antishok/a2EPT/7/

JS:

function TransactionItem(data) {
    ko.mapping.fromJS(data, {}, this);
}

function ViewModel(data) {
    var self = this;
    this.transactions = ko.mapping.fromJS(data, mappingOptions);
    this.editedItem = ko.observable();
    this.editedField = ko.observable();

    this.isEdited = function (item, field) {
        return self.editedItem() === item && self.editedField() === field;
    }

    this.editItem = function (field, item) {
        self.editedItem(item);
        self.editedField(field);
    }

    this.stopEditing = function() {
         self.editItem(undefined, undefined);
    }
}

var viewModel = new ViewModel(myData);
ko.applyBindings(viewModel);

ko.utils.registerEventHandler(document, 'click', function(event) {
    viewModel.stopEditing();
});

HTML:

<td>
        <!-- Either show this -->
        <span data-bind="visible: !$root.isEdited($data, 'Name'), 
                         text: Name, 
                         click: $root.editItem.bind($data, 'Name'), clickBubble: false"></span>

        <!-- Or show this -->
        <input data-bind="visible: $root.isEdited($data, 'Name'), 
                          value: Name, 
                          hasfocus: $root.isEdited($data, 'Name'), 
                          click: function(){}, clickBubble: false" 
        /><!-- end input -->
</td>
like image 108
antishok Avatar answered Mar 07 '26 06:03

antishok



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!