ng-repeat with controller for each table row: how do I access x-editable form elements?
Asked Answered
P

3

12

I setting up a scenario very similar to the Editable Row example from the x-editable demo site. In this scenario, a there is a simple table with three columns for data and a fourth for edit and delete buttons. A third button outside of the table adds a row to the table. When the form is editable, the data columns become editable (the primary feature of x-editable library). For this demo, the first column becomes a simple text edit and the second two columns become drop lists.

The table is created by having an ng-repeat on a row template. I need to do a few different things that all involve accessing the scope created by the ng-repeat. I need to

  • detect when the row is editable and when it is not
  • filter the options for the second drop list when the first drop list changes

In order to try to work with this demo, I've added a controller for the individual row. That has given me some access to the form (name = rowform), but I'm still not able to set a watch on the "make" property. I can't even find what property of the form is changing when the user makes a selection.

How do I set up a watch on the 'make' property?

Page Controller

    angular.module('app').controller("quoteBuckingRaterController",
    function ($scope, $q, $filter, listService, transactionDataService) {

        $scope.equipment = []; 
        $scope.makes = []; 
        $scope.models = [];

        $scope.showModel = function(equip) {
            if(equip.model) {
                var selected = $filter('filter')($scope.models, {id: equip.model});
                return selected.length ? selected[0].name : 'Not set';
            } else {
                return 'Not set';
            }
        };

        $scope.showMake = function(equip) {
            if (equip.model) {
                var selected = $filter('filter')($scope.models, { id: equip.model });
                if (selected.length && selected.length > 0) {
                    if (equip.make != selected[0].make)
                        equip.make = selected[0].make;
                    return selected[0].make;
                }
                else {
                    return 'Not set';
                }
            } else {
                return 'Not set';
            }
        };

        $scope.checkName = function (data, id) {
            if (!data) {
                return "Description is required";
            }
        };

        $scope.checkModel = function (data, id) {
            if (!data) {
                return "Model is required";
            }
        };

        $scope.saveEquipment = function (data, id) {
            $scope.inserted = null;
        };

        $scope.cancelRowEdit = function (data, id) {
            $scope.inserted = null;
        };

        $scope.removeEquipment = function(index) {
            $scope.equipment.splice(index, 1);
        };

        $scope.addEquipment = function() {
            $scope.inserted = {
                id: $scope.equipment.length+1,
                name: '',
                make: null,
                model: null 
            };
            $scope.equipment.push($scope.inserted);
        };

        $scope.filterModels = function (make) {
            $scope.models = _.where($scope.allModels, function(item) {
                return item.make == make;
            });
        };

        //called by another process when page loads
        $scope.initialize = function (loaded) {
            return $q(function (resolve, reject) {
                if (!loaded) {
                    listService.getEquipmentModels().then(function (data) {
                        $scope.allModels = data;
                        $scope.models = data;

                        //uses underscore.js
                        $scope.makes = _.chain(data)
                                        .map(function (item) {
                                            var m = {
                                                id: item.make,
                                                name: item.make
                                            };
                                            return m;
                                        })
                                        .uniq()
                                        .value();                            
                        resolve();
                    });
                }
            });
        }
    });

Row Controller

angular.module('app').controller("editRowController",
function ($scope) {
    $scope.testClick = function () {
        alert('button clicked');
    };

    $scope.make = null;

    $scope.$watch('make', function () {
        alert('how do I tell when the make has been changed?');
        this.$parent.$parent.filterModels(make.id);
    });
});

HTML

<div>
    <div class="col-md-12" style="margin-bottom: 3px">
        <div class="col-md-4 col-md-offset-1" style="padding-top: 6px; padding-left: 0px"><label>Equipment</label></div>
        <div class="col-md-offset-10">
            <button class="btn btn-primary btn-sm" ng-click="addEquipment()">Add row</button>
        </div>
    </div>
    <div class="col-md-10 col-md-offset-1">    
        <table class="table table-bordered table-hover table-condensed">
            <tr style="font-weight: bold; background-color: lightblue">
                <td style="width:35%">Name</td>
                <td style="width:20%">Make</td>
                <td style="width:20%">Model</td>
                <td style="width:25%">Edit</td>
            </tr>
            <tr ng-repeat="equip in equipment" ng-controller="editRowController">
                <td>
                    <!-- editable equip name (text with validation) -->
                    <span editable-text="equip.name" e-name="name" e-form="rowform" onbeforesave="checkName($data, equip.id)" e-required>
                        {{ equip.name || 'empty' }}
                    </span>
                </td>
                <td>
                    <!-- editable make (select-local) -->
                    <span editable-select="equip.make" e-name="make" e-form="rowform" e-ng-options="s.value as s.name for s in makes">
                        {{ showMake(equip) }}
                    </span>
                </td>
                <td>
                    <!-- editable model (select-remote) -->
                    <span editable-select="equip.model" e-name="model" e-form="rowform" e-ng-options="g.id as g.name for g in models" onbeforesave="checkModel($data, equip.id)" e-required>
                        {{ showModel(equip) }}
                    </span>
                    <button type="button" ng-disabled="rowform.$waiting" ng-click="testClick()" class="btn btn-default">
                        test
                    </button>
                </td>
                <td style="white-space: nowrap">
                    <!-- form -->
                    <form editable-form name="rowform" onbeforesave="saveEquipment($data, equip.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == equip">
                        <button type="submit" ng-disabled="rowform.$waiting" class="btn btn-primary">
                            save
                        </button>
                        <button type="button" ng-disabled="rowform.$waiting" ng-click="rowform.$cancel()" class="btn btn-default">
                            cancel
                        </button>
                    </form>
                    <div class="buttons" ng-show="!rowform.$visible">
                        <button class="btn btn-primary" ng-click="rowform.$show()">edit</button>
                        <button class="btn btn-danger" ng-click="removeEquipment($index)">del</button>
                    </div>
                </td>
            </tr>
        </table>
    </div>
</div>
Pennyroyal answered 19/9, 2015 at 4:22 Comment(1)
A working jsfiddle might help here.Rica
P
5

ng-repeat creates a child scope for each row (for each equipment). The scope of the EditRowController is therefore a childScope of the parent quoteBuckingRaterController.

This childScope contains:

  • all properties of the parent scope (e.g. equipment, makes, models)
  • the property equip with one value of the equipment array, provided by ng-repeat
  • any additional scope property that is defined inside the ng-repeat block, e.g. rowform

Therefore you are able to access these properties in the childController editRowController using the $scope variable, e.g.:

$scope.equip.make
$scope.equipment

and inside the ng-repeat element in the html file by using an angular expression, e.g:

{{equip.make}}
{{equipment}}

Now to $scope.$watch: If you provide a string as the first argument, this is an angular expression like in the html file, just without surrounding brackets {{}}. Example for equip.make:

$scope.$watch('equip.make', function (value) {
     console.log('equip.make value (on save): ' + value);
});

However, angular-xeditable updates the value of equip.make only when the user saves the row. If you want to watch the user input live, you have to use the $data property in the rowform object, provided by angular-xeditable:

$scope.$watch('rowform.$data.make', function (value) {
    console.log('equip.make value (live): ' + value);
}, true);

You can also use ng-change:

<span editable-select="equip.make" e-name="make" e-ng-change="onMakeValueChange($data)" e-form="rowform" e-ng-options="s.value as s.name for s in makes">

JS:

$scope.onMakeValueChange = function(newValue) {
    console.log('equip.make value onChange: ' + newValue);
}

That should solve your first question: How to watch the make property.

Your second question, how to detect when the row is editable and when it is not, can be solved by using the onshow / onhide attributes on the form or by watching the $visible property of the rowform object in the scope as documented in the angular-xeditable reference

<form editable-form name="rowform" onshow="setEditable(true)" onhide="setEditable(false)">

$scope.setEditable = function(value) {
      console.log('is editable? ' + value);
};

// or
$scope.$watch('rowform.$visible', function(value) {
  console.log('is editable? ' + value);
});

You might ask why the rowform object is in the current childScope. It is created by the <form> tag. See the Angular Reference about the built-in form directive:

Directive that instantiates FormController.

If the name attribute is specified, the form controller is published onto the current scope under this name.

A working snippet with your example code:

angular.module('app', ["xeditable"]);

angular.module('app').controller("editRowController", function ($scope) {
    $scope.testClick = function () {
        alert('button clicked');
    };

    $scope.$watch('equip.make', function (value) {
        console.log('equip.make value (after save): ' + value);
    });
  
    $scope.$watch('rowform.$data.make', function (value) {
        console.log('equip.make value (live): ' + value);
    }, true);
  
    // detect if row is editable by using onshow / onhide on form element
    $scope.setEditable = function(value) {
      console.log('is equip id ' + $scope.equip.id + ' editable? [using onshow / onhide] ' + value);
    };
  
    // detect if row is editable by using a watcher on the form property $visible
    $scope.$watch('rowform.$visible', function(value) {
      console.log('is equip id ' + $scope.equip.id + ' editable [by watching form property]? ' + value);
    });
});


angular.module('app').controller("quoteBuckingRaterController", function ($scope, $filter) {
    $scope.equipment = []; 
    $scope.makes = [{value: 1, name: 'Horst'}, {value: 2, name: 'Fritz'}]; 
    $scope.models = [{id: 1, name: 'PC', make: 1}];

    $scope.showModel = function(equip) {
        if(equip.model) {
            var selected = $filter('filter')($scope.models, {id: equip.model});
            return selected.length ? selected[0].name : 'Not set';
        } else {
            return 'Not set';
        }
    };

    $scope.showMake = function(equip) {
        if (equip.model) {
            var selected = $filter('filter')($scope.models, { id: equip.model });
            if (selected.length && selected.length > 0) {
                if (equip.make != selected[0].make)
                    equip.make = selected[0].make;
                return selected[0].make;
            }
            else {
                return 'Not set';
            }
        } else {
            return 'Not set';
        }
    };

    $scope.checkName = function (data, id) {
        if (!data) {
            return "Description is required";
        }
    };

    $scope.checkModel = function (data, id) {
        if (!data) {
            return "Model is required";
        }
    };

    $scope.saveEquipment = function (data, id) {
        $scope.inserted = null;
    };

    $scope.cancelRowEdit = function (data, id) {
        $scope.inserted = null;
    };

    $scope.removeEquipment = function(index) {
        $scope.equipment.splice(index, 1);
    };

    $scope.addEquipment = function() {
        $scope.inserted = {
            id: $scope.equipment.length+1,
            name: '',
            make: null,
            model: null 
        };
        $scope.equipment.push($scope.inserted);
    };
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-xeditable/0.1.9/js/xeditable.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/angular-xeditable/0.1.9/css/xeditable.css" rel="stylesheet"/>
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet"/>
<div ng-app="app" ng-controller="quoteBuckingRaterController">
    <div class="col-md-12" style="margin-bottom: 3px">
        <div class="col-md-4 col-md-offset-1" style="padding-top: 6px; padding-left: 0px"><label>Equipment</label></div>
        <div class="col-md-offset-10">
            <button class="btn btn-primary btn-sm" ng-click="addEquipment()">Add row</button>
        </div>
    </div>
    <div class="col-md-10 col-md-offset-1">    
        <table class="table table-bordered table-hover table-condensed">
            <tr style="font-weight: bold; background-color: lightblue">
                <td style="width:35%">Name</td>
                <td style="width:20%">Make</td>
                <td style="width:20%">Model</td>
                <td style="width:25%">Edit</td>
            </tr>
            <tr ng-repeat="equip in equipment" ng-controller="editRowController">
                <td>
                    <!-- editable equip name (text with validation) -->
                    <span editable-text="equip.name" e-name="name" e-form="rowform" onbeforesave="checkName($data, equip.id)" e-required>
                        {{ equip.name || 'empty' }}
                    </span>
                </td>
                <td>
                    <!-- editable make (select-local) -->
                    <span editable-select="equip.make" e-name="make" e-form="rowform" e-ng-options="s.value as s.name for s in makes">
                        {{ showMake(equip) }}
                    </span>
                </td>
                <td>
                    <!-- editable model (select-remote) -->
                    <span editable-select="equip.model" e-name="model" e-form="rowform" e-ng-options="g.id as g.name for g in models" onbeforesave="checkModel($data, equip.id)" e-required>
                        {{ showModel(equip) }}
                    </span>
                    <button type="button" ng-disabled="rowform.$waiting" ng-click="testClick()" class="btn btn-default">
                        test
                    </button>
                </td>
                <td style="white-space: nowrap">
                    <!-- form -->
                    <form editable-form name="rowform" onbeforesave="saveEquipment($data, equip.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == equip" onshow="setEditable(true)" onhide="setEditable(false)">
                        <button type="submit" ng-disabled="rowform.$waiting" class="btn btn-primary">
                            save
                        </button>
                        <button type="button" ng-disabled="rowform.$waiting" ng-click="rowform.$cancel()" class="btn btn-default">
                            cancel
                        </button>
                    </form>
                    <div class="buttons" ng-show="!rowform.$visible">
                        <button class="btn btn-primary" ng-click="rowform.$show()">edit</button>
                        <button class="btn btn-danger" ng-click="removeEquipment($index)">del</button>
                    </div>
                </td>
            </tr>
        </table>
    </div>
</div>
Pya answered 28/9, 2015 at 20:3 Comment(4)
Very nice tip on the editable, but the watch doesn't work when in edit mode. check out jsfiddle.net/Steve5877/32kzsvve Its the code you sent with a few extra models and an attempt to filter models when make is selected. But it doesn't trigger the watch when make changes while editing.Pennyroyal
I'm going to award the bounty because the system doesn't give me much choice. Can't extend the bounty, even though no one even looked at this until the last minute. This answer is NOT complete and NOT working yet, but its the closest and shows the most effort. If I don't award it, he gets half credit anyway (unless I downgrade him) and I get nothing. I'm hoping that Felix will upgrade his answer when he sees the issue I'm still having.Pennyroyal
@SteveWash I have updated my answer and your jsFiddle: jsfiddle.net/32kzsvve/1Pya
Excellent! Thanks, Felix! Your first answer is exactly what I had been trying on my own. Glad I gave you the bounty.Pennyroyal
M
2

If you simply want to $watch the make property of equipment, try changing to:

$scope.$watch('equipment.make', function(){(...)})
Mating answered 28/9, 2015 at 17:15 Comment(0)
J
1

You could write your own directive for this.

The main advantage is that directives have isolated scope and can have their own controller.

see the directive documentation to know if it's for you.

Jejunum answered 29/9, 2015 at 4:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.