【问题标题】:Angular.js directive formatter only called once - or how to tell ngModel to behave like watchCollectionAngular.js 指令格式化程序只调用一次 - 或者如何告诉 ngModel 表现得像 watchCollection
【发布时间】:2015-05-29 06:13:14
【问题描述】:

我正在尝试构建一个指令,该指令可以将二维数组解析并格式化为逗号和行分隔的文本。 (一行中的所有项目以逗号分隔,行以'\n'分隔)

解析部分正在工作。 格式化部分只工作一次。

我在格式化程序中有一个调试 console.log console.log("formatter called. value:", value); 我看到这只会打印一次。

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();
            });
            /**/

        }
    };
});

我认为问题在于我使用的数据是(2D)数组。 但我该如何解决这个问题? 我找到了scope.$watchCollection(),我可以验证每次更新 ngModel 时都会触发。 但是我怎样才能触发“渲染”过程/手动更新视图?

如果我走错了路,请给我提示要寻找什么!

我创建了一个Plunker 并将脚本示例添加为 sn-p:
打开控制台
然后更改两个文本区域之一中的数字。
第一个是指令
第二个只是一个文本区域,在 MainController
中有绑定和手动监视 在右侧您可以看到原始数据(JSON 格式)

如果您更改 csv 区域中的数字,所有内容都会得到更新。
如果您更改下部文本区域中的数字,则 csv 区域不会更新。
在控制台中,您可以看到 $watchCollection 触发。

/**************************************************************************************************/
/**   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>

标题:Angular.js 指令格式化程序只调用一次 -
或如何告诉 ngModel 表现得像 watchCollection
编辑:
我又遇到了原来的问题...
但有一个不同的例子..
我想我已经理解了 user3906922 的回答。
在这个plunker 你可以试试 -
我有一个显示 $scope 变量 ($scope.showdata) 的 json 的 textarea 这是由控制器中的两个手表函数手动绑定的

$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);
});

然后我尝试将此行为转换为指令 - 即第二个文本区域:

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;
                }
            });
        }
    };
});

我已经在链接函数(它在 plunker 中)中使用 watchCollection 进行了测试,每次发生变化时都会触发。

那么是否可以告诉 ngModel 更新/或喜欢它使用 watchCollection?

或者这是对这种情况的错误思考方式?

【问题讨论】:

    标签: javascript angularjs angularjs-directive


    【解决方案1】:

    因为您的 extendedDeep 函数替换了数组的内容,所以 Angular 不知道模型发生了变化,因此也不会调用格式化程序。

    我不确定您对extendDeepArrayOverwrite 函数的意图是什么,但是您可以在 isObject 和 isArray 之间切换 if-else 的顺序(因为数组也是一个对象),并将 concat 函数结果分配回去到dst[key](没有它,声明没有意义):

        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;
                }         
            });
    

    查看plunker

    【讨论】:

    • 感谢您指出系统设计中的错误:-) 我还必须将 extendDeep 交换为 extendDeepArrayOverwrite(因此它在嵌套结构中工作..)我使用了 extendDeep 的东西从 text/json 文件加载配置并想要“覆盖”旧的内部数据。
    • 你好 user3906922,我在原始问题中添加了第二个示例 - 与我的第一个体验相同的行为..(不更新)
    • 还是从编辑中创建一个新问题更好?
    • 一段时间后我找到了解决方案:stackoverflow.com/questions/31368313/…
    猜你喜欢
    • 1970-01-01
    • 2015-09-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-11
    • 2014-05-15
    相关资源
    最近更新 更多