2

iam trying to build a directive that can parse and format an 2dimensional array as comma and line separated text. (all items in one line are comma separated and lines are '\n' separated)

the parsing part is working. the formating part only works once.

i have a debug console.log in the formatter console.log("formatter called. value:", value); and i see that this prints only one time.

app.directive('mycsvparser', function() {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attrs, ngModelCtrl) {

            // View --> Model 
            ngModelCtrl.$parsers.push(function(viewValue) {
                if (viewValue) {
                    // parse input text:
                    var resultArray = [];

                    // first split into lines:
                    var arrayLines = viewValue.split("\n");

                    // parse every line
                    for (var line_index in arrayLines) {
                        .....
                        resultArray.push(lineArray);
                    }

                    return resultArray;
                } else {
                    // console.log("invalid");
                    return ngModelCtrl.$modelValue;
                }
            });

            // Model --> View
            ngModelCtrl.$formatters.push( function formatter(value) {
                console.log("formatter called. value:", value);
                if (value) {
                    var resultString = "";
                    var arrayLines = value;

                    for (var line_index in arrayLines) {
                        var arrayItems = arrayLines[line_index];
                        resultString += arrayItems.join(", ");
                        resultString += "\n";
                    }

                    return resultString;
                }
            });

            /**/
            scope.$watchCollection(attrs.ngModel, function(newData) {
                console.log('mycsvparser newData', newData);
                ngModelCtrl.$render();
            });
            /**/

        }
    };
});

i think the problem is that the data i use is a (2D) array. but how can i fix this? i found scope.$watchCollection() and i can verify that this fires every time the ngModel is updated. but how can i fire the 'rendering' process / update the view manually?

if iam on the wrong way pleas give me a hint what to look for!

i have made a Plunker and added the script example as snippet:
open up the console
and then change the numbers in one of the two textareas.
the first is the directive
the second one is just a textarea with bind and manual watches in the MainController
on the right you see the raw data (in JSON format)

if you change a number in the csv area all things get the updates.
if you change a number in the lower textarea the csv area does not update.
in the console you can see that the $watchCollection fires.

/**************************************************************************************************/
/**   angularjs                                                                                  **/



var app = angular.module('myTestApp', []);

// quick JSON formating
app.filter('prettyprint', function() {
    // sames as json filter...but with 4 spaces...
    return function(input) {
        return JSON.stringify(input, null, 4);
    };
});

/** mycsvparser
  * this directive can parse and format a 2d array
  * this directive only updates the model if a valid value is entered!!
  **/
app.directive('mycsvparser', function() {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attrs, ngModelCtrl) {
            // console.log("directive date");
             console.log("scope", scope);
            // console.log("element", element);
             console.log("attrs", attrs);
             console.log("ngModelngModelCtrl", ngModelCtrl);
            
            // View --> Model 
            ngModelCtrl.$parsers.push(function(viewValue) {
                if (viewValue) {
                    // console.log("valid");
                    // console.log("ngModelCtrl.$modelValue", ngModelCtrl.$modelValue);
                    // console.log("viewValue", viewValue);
                    
                    // parse input text:
                    
                    var resultArray = [];
                    
                    // first split into lines:
                    var arrayLines = viewValue.split("\n");
                    
                    // parse every line
                    for (var line_index in arrayLines) {
                        var line = arrayLines[line_index];
                        // split line into items
                        var arrayItems = line.split(",");
                        
                        var lineArray = [];
                        for (var item_index in arrayItems) {
                            var item = arrayItems[item_index];
                            // trim string to bare content
                            item = item.trim();
                            // here you can check for a special item type.. 
                            // convert to number
                            var itemAsNumber = Number(item);
                            // if (!isNaN(itemAsNumber)) {
                                // item = "";
                            // }
                            // add item to lineArray
                            lineArray.push(itemAsNumber);
                        }
                        // add content of line (in Array form) to resultArray
                        resultArray.push(lineArray);
                    }
                    
                    console.log("resultArray", resultArray);
                    ngModelCtrl.$render();
                    return resultArray;
                } else {
                    // console.log("invalid");
                    return ngModelCtrl.$modelValue;
                }
            });
            
            // Model --> View
            ngModelCtrl.$formatters.push( function formatter(value) {
                console.log("formatter called. value:", value);
                if (value) {
                    var resultString = "";
                    var arrayLines = value;
                    
                    for (var line_index in arrayLines) {
                        var arrayItems = arrayLines[line_index];
                        resultString += arrayItems.join(", ");
                        resultString += "\n";
                    }
                    
                    return resultString;
                }
            });
            
            /**/
            scope.$watchCollection(attrs.ngModel, function(newData) {
                console.log('mycsvparser newData', newData);
                ngModelCtrl.$render();
            });
            /**/
            
        }
    };
});
/***/


//http://stackoverflow.com/questions/15310935/angularjs-extend-recursive
// only needed for AngularJS < v1.4
// https://docs.angularjs.org/api/ng/function/angular.merge
function extendDeep(dst) {
    angular.forEach(arguments, function(obj) {
        if (obj !== dst) {
            angular.forEach(obj, function(value, key) {
                if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
                    extendDeep(dst[key], value);
                } else {
                    dst[key] = value;
                }     
            });   
        }
    });
    return dst;
}

// special variant that overwrites arrays.
function extendDeepArrayOverwrite(dst) {
    angular.forEach(arguments, function(obj) {
        if (obj !== dst) {
            angular.forEach(obj, function(value, key) {
                /*
                if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
                  extendDeep(dst[key], value);
                } else if (dst[key] && dst[key].constructor && dst[key].constructor === Array) {
                  dst[key].concat(value);
                } else if(!angular.isFunction(dst[key])) {
                  dst[key] = value;
                }
                */
                if (dst[key] && angular.isObject(dst[key])) {
                  extendDeep(dst[key], value);
                } else if (dst[key] && angular.isArray(dst[key])) {
                  // we want to overwrite the old array content.
                  // reset array
                  dst[key].length = 0;
                  // combine new content in old empty array
                  dst[key].concat(value);
                } else if(!angular.isFunction(dst[key])) {
                  dst[key] = value;
                }        
            });   
        }
    });
    return dst;
}



// MAIN Controller
app.controller('MainController', ['$scope', '$filter', function($scope, $filter) {

    //////////////////////////////////////////////////
    // internal data structure 

    $scope.systemdata = {
        name : "sun",
        lastsaved : new Date().toISOString(),
        patch : [
            [1],
            [4, 20,],
        ],
    };
    
    $scope.systemdata_text = '{\n\
    "name": "sun",\n\
    "lastsaved": "1",\n\
    "patch": [\n\
        [\n\
            1\n\
        ],\n\
        [\n\
            4,\n\
            20\n\
        ]\n\
    ]\n}';
    
    //////////////////////////////////////////
    // functions
    
        $scope.saveConfig = function() {
            console.log();
        }
    
    // watch example/info http://stackoverflow.com/a/15113029/574981
 $scope.$watch('systemdata_text', function() {
     if ($scope.systemdata_text.length >0) {
      // convert to JS-Object
            var tempData = JSON.parse($scope.systemdata_text);
            // normally you should now verify that the resulting object
            // is of the right structure.
            // we skip this and just believe in good will ;-)
            
            // merge data
            // angular.merge does not work with the arrays as we need it.
            //angular.merge($scope.systemdata, tempData);
            extendDeepArrayOverwrite($scope.systemdata, tempData);
     }
 });
 
 $scope.$watchCollection('systemdata', function() {
     $scope.systemdata_text = JSON.stringify($scope.systemdata, null, 4);
 });
 
    
}]);










/**************************************************************************************************/
/**   helper                                                                                     **/

function handleToggle(event) {
    this.parentNode.children[1].classList.toggle('hide');
}

function initSystem(){
    console.groupCollapsed("system init:");
    
    // toggle script
    console.log("add click event to 'div.toggle':");
    var toggleContainers = document.querySelectorAll("div.toggle .caption")
    for (var i = 0; i < toggleContainers.length; ++i) {
        var item = toggleContainers[i];  // Calling datasets.item(i) isn't necessary in JavaScript
        item.addEventListener('click', handleToggle, false);
    }
    console.log("\t found " + toggleContainers.length + " toggleContainers");
    
    console.log("finished.");
    console.groupEnd();
}

/* * pure JS - newer browsers...* */
document.addEventListener('DOMContentLoaded', initSystem, false);
/* Styles go here */

.raw {
    float:right; 
    padding:    0.5em;
    background-color:       rgb(100, 100, 100);
    background-color:       rgba(0, 0, 0, 0.1);
    border:     none;
 border-radius:   0.5em;
 box-shadow:    0px 0px 10px rgba(0,0,0,0.5) inset;
 color:     inherit;
}



.toggle .caption {
 cursor:     pointer;
}

div .hide{
 height:     0;
 margin:     0;
 padding:    0;
 overflow:    hidden;
}



input, textarea, select {
 padding:    0.2em;
 background-color:  rgba(0, 0, 0, 0.1);
 border:     none;
 border-radius:   0.5em;
 box-shadow:    0px 0px 10px rgba(0,0,0,0.5) inset;
 color:     inherit;
 font-size:    inherit;
 line-height:   inherit;
 font-family:   inherit;
 text-align:    right;
 text-shadow:   inherit;
 white-space:   pre;
}

textarea {
 display:    block;
 margin:     1em;
 padding:    0.5em;
 background-color:  rgba(0, 0, 0, 0.2);
 border-radius:   1em;
 box-shadow:    0px 0px 10px rgba(0,0,0,0.5) inset;
 text-align:    left;
 white-space:   pre;
}
<!DOCTYPE html>
<html ng-app="myTestApp">

  <head>
    <script src="https://code.angularjs.org/1.4.0-beta.6/angular.js" data-semver="1.4.0-beta.6" data-require="angular.js@*"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body>
    <div ng-controller="MainController" class="app">
        <div style="" class="toggle raw">
            <h4 class="caption">raw:</h4>
            <pre class="">{{systemdata | prettyprint}}</pre>
        </div>
        <div>
            Hello {{systemdata.name}}<br><br>
            
            <label>CSV List:</label>
            <textarea
    id="patch_text" 
    name="patch_text" 
    class="output_text" 
    cols="25" 
    rows="4" 
    wrap="off"
    type="text"
    ng-model="systemdata.patch"
    ngTrim="false"
    mycsvparser
   ></textarea>
   <label>raw json</label>
   <textarea
    id="output_text" 
    name="output_text" 
    class="output_text" 
    cols="50" 
    rows="15" 
    wrap="off"
    type="text"
    ng-model="systemdata_text"
    ngTrim="false"
   ></textarea> 
        </div>
    </div>
  </body>

</html>

Title: Angular.js directive formatter only called once -
or how to tell ngModel to behave like watchCollection
edit:
i have the original problem again...
but with an different example..
i think i have understand the answer from user3906922.
in this plunker you can try it out -
i have a textarea showing the json of an $scope variable ($scope.showdata) this is manually binded by two watch functions in the controller

$scope.$watch('testdata_text', function() {
    if ($scope.testdata_text.length >0) {
        try {
            var tempData = JSON.parse($scope.testdata_text);
            extendDeepArrayOverwrite($scope.showdata, tempData);
        }
        catch(error) {
            console.error("testdata_text no valid JSON:  ", error.message)
        }
    }
});

$scope.$watchCollection('showdata', function() {
    $scope.testdata_text = JSON.stringify($scope.showdata, null, 4);
});

i then have tried to convert this behavior to a directive - that is the second textarea:

app.directive('rawjson', function() {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attrs, ngModelCtrl) {
            // View --> Model 
            ngModelCtrl.$parsers.push(function(viewValue) {
                if (viewValue) {
                    try {
                        var tempData = JSON.parse(viewValue);
                        extendDeepArrayOverwrite(ngModelCtrl.$modelValue, tempData);
                        ngModelCtrl.$setValidity("isJson", true);
                    }
                    catch(error) {
                        ngModelCtrl.$setValidity("isJson", false);
                    }
                    return ngModelCtrl.$modelValue;
                } else {
                    return ngModelCtrl.$modelValue;
                }
            });

            // Model --> View
            ngModelCtrl.$formatters.push( function formatter(value) {
                if (value) {
                    var resultString = JSON.stringify(value, null, 4);
                    return resultString;
                }
            });
        }
    };
});

i have tested with an watchCollection inside the link function (its in the plunker) and this one fires every time something changes.

so is it possible to tell ngModel to update on/ or like it uses watchCollection?

or is this the wrong way to think of the situation?

1 Answers1

0

Because your extendedDeep function replaces the contents of the array, angular doesn't know there was a change in the model, and the formatter isn't invoked.

I'm not sure what's your intention with the extendDeepArrayOverwrite function, but you can switch the order of the if-else between the isObject and isArray (because array is also an object), and assign the concat function result back to dst[key] (without it there is no meaning to the statement):

    angular.forEach(obj, function(value, key) {
            /*
            if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
              extendDeep(dst[key], value);
            } else if (dst[key] && dst[key].constructor && dst[key].constructor === Array) {
              dst[key].concat(value);
            } else if(!angular.isFunction(dst[key])) {
              dst[key] = value;
            }
            */
            if (dst[key] && angular.isArray(dst[key])) {
              // we want to overwrite the old array content.
              // reset array
              dst[key].length = 0;
              // combine new content in old empty array
              dst[key] = dst[key].concat(value);
            }
            else if (dst[key] && angular.isObject(dst[key])) {
              extendDeep(dst[key], value);
            } else if(!angular.isFunction(dst[key])) {
              dst[key] = value;
            }         
        });

Check this plunker

eladcon
  • 5,815
  • 1
  • 16
  • 19
  • thanks for point out the bugs in the system design:-) i have also had to exchange the `extendDeep` to `extendDeepArrayOverwrite` (so its working in nested structures..) i use the extendDeep thing to load a config from a text/json file and want to 'overwrite' the old internal data. – Stefan Krüger s-light Mar 26 '15 at 11:58
  • hello user3906922, i have added a second example to the original question - with the same behavior as my first expririence.. (not updating) – Stefan Krüger s-light Mar 29 '15 at 20:55
  • or is it better to create a new question from the edits? – Stefan Krüger s-light Mar 29 '15 at 21:03
  • some time later i found a solution: http://stackoverflow.com/questions/31368313/how-to-manually-rerun-formatter-chain-in-angularjs-directive-with-ngmodel – Stefan Krüger s-light Jul 12 '15 at 13:54