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?