AngularJs - Rebinding ng-model - javascript

So my use case is:
cols = [{field="product.productId"},{field="product.productPrice"}];
data = {products:[{product:{productId:1,productPrice:10}, {product:{productId:2, productPrice:15}}]}
What I would like to do is:
<div ng-repeat="product in data.products">
<div ng-repeat="col in cols">
<input type="text" ng-model="product[col.field]"></>
</div>
</div>
Now if col.field was just 'someField' and not 'some.deep.field' this would work. Because the field has many elements, the correct way to do ng-model would be "product[some][deep][field]" if I didn't want to be generic and allow my data and columns to change. I've tried this approach and it worked for a non-generic use case.
What I have tried to make it generic:
Recompiling my 'input' element. This creates the perfect HTML E.G it has ng-model="product['some']['deep']['field']" on it, but in no way is the field bound whatsoever. Perhaps I am compiling with the wrong scope here? I have tried addingAttribute ng-init="hello='Hey'" ng-model="hello" at this point and it worked and bound properly... so I feel I am missing something regarding scope here.
compile: function (templateElement) {
templateElement[0].removeAttribute('recursive-model');
templateElement[0].removeAttribute('recursive-model-accessor');
return {
pre: function (scope, element, attrs) {
function convertDotToMultiBracketNotation(dotNote) {
var ret = [];
var arrayOfDots = dotNote.split('.');
for (i = 0; i < arrayOfDots.length; i++) {
ret.push("['" + arrayOfDots[i] + "']");
}
return ret.join('');
}
if (attrs.recursiveModel && attrs.recursiveModelAccessor) {
scope[scope.recursiveModel] = scope.ngModel;
element[0].setAttribute('ng-model', scope.recursiveModel + convertDotToMultiBracketNotation(scope.recursiveModelAccessor));
var res = $compile(element[0])(scope);
console.info('new compiled element:', res);
return res;
}
}
}
}
Messing with the NgModelController to format and parse. In this case I have put the entire 'row' object into ng-model and then used formatter/parser to only mess with the 1 field I was interested in. This works until you clear the field. At that point it seems to wipe out modelCtrl.$modelValue completely.
In other words - my console.log says:
Setting field to val 'Text' on row [object]
Setting field to val 'Tex' on row [object]
Setting field to val 'Te' on row [object]
Setting field to val 'T' on row [object]
Setting field to val '' on row [object]
Setting field to val 'A' on row undefined
link: function (scope, element, attrs, ctrls) {
if(ctrls[2] && scope.recursiveModelAccessor){
var modelCtrl = ctrls[2];
modelCtrl.$formatters.push(function (inputValue) {
function getValue(object, string){
var explodedString = string.split('.');
for (i = 0, l = explodedString.length; i < l; i++) {
object = object[explodedString[i]];
}
return object;
};
function getValueRecursive (row, field) {
if (field instanceof Array) {
var ret = [];
for (var i = 0; i < col.field.length; i++) {
ret.push(getValue(row, field[i]));
}
return ret.join('/');
} else {
return getValue(row, field);
}
};
return getValueRecursive(modelCtrl.$modelValue, scope.recursiveModelAccessor);
});
modelCtrl.$parsers.push(function (inputValue) {
function setValueRecursive (row, field, newValue) {
if (field instanceof Array) {
var firstField = field.shift();
if(field.length==1){
field = field[0];
}
setValueRecursive(row[firstField], field, newValue);
} else {
console.log("Setting "+field+" to val:"+newValue+" on row:"+row);
row[field]=newValue;
}
};
setValueRecursive(modelCtrl.$modelValue, scope.recursiveModelAccessor.split('.'), modelCtrl.$viewValue);
return modelCtrl.$modelValue;
});

Long story short (8 solid hours wasted on this) - don't put ng-model="something" on your object, if you then plan to re-compile after modifying the ng-model attribute.
A working directive for rebinding the ngModel (Just don't have the attribute already on your object!)
<div ng-repeat="product in data.products">
<div ng-repeat="col in cols">
<input type="text" recursive-model="product" recursive-model-accessor="some.deep.field"></input>
</div>
</div>
Just make sure you don't have ng-model="something".
Of course - a 100% perfect solution would throw exception if ng-model attribute was present :)
module.directive('rebindModel', ['$compile','$parse',function($compile,$parse){
return {
restrict:'A',
compile: function (templateElement) {
templateElement[0].removeAttribute('recursive-model');
templateElement[0].removeAttribute('recursive-model-accessor');
return {
post: function (scope, element, attrs) {
function convertDotToMultiBracketNotation(dotNote) {
var ret = [];
var arrayOfDots = dotNote.split('.');
for (i = 0; i < arrayOfDots.length; i++) {
ret.push("['" + arrayOfDots[i] + "']");
}
return ret.join('');
}
if (attrs.recursiveModel && attrs.recursiveModelAccessor) {
var parsedModelAccessor = $parse(attrs.recursiveModelAccessor)
var modelAccessor = parsedModelAccessor(scope);
element[0].setAttribute('ng-model', attrs.recursiveModel + convertDotToMultiBracketNotation(modelAccessor));
var res = $compile(element[0])(scope);
return res;
}
}
}
},
}
}]);

Related

JavaScript: Why getting last inserted values?

I am getting familiar with the prototype world of JavaScript and this keyword. I am new to Web-world. Today when I started playing with prototype I saw some strange behavior but I am not able to get why this is happening. I've created a constructor Group as following:
// Code goes here
function Group(config) {
this.config = config;
this.getId = function() {
return this.config.id;
};
this.setId = function(id) {
this.config.id = id;
};
}
I use it in one MyGroup constructor like this:
function MyGroup(config) {
var myAttrs = ['id', 'name'];
this.g = new Group(config);
addGetterSetter(MyGroup, this.g, myAttrs)
}
addGetterSetter is the function I wrote to add getter and setter dynamically to the attributes of MyGroup.
var GET = 'get',
SET = 'set';
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function addGetterSetter(constructor, target, attrs) {
function addGetter(constructor, target, attr) {
var method = GET + capitalize(attr);
constructor.prototype[method] = function() {
return target[method]();
};
}
function addSetter(constructor, target, attr) {
var method = SET + capitalize(attr);
constructor.prototype[method] = function(value) {
return target[method](value);
};
}
for (var index = 0; index < attrs.length; index++) {
addGetter(constructor, target, attrs[index]);
addSetter(constructor, target, attrs[index]);
}
}
Now when I use MyGroup,Group like this:
var items = [{
id: 123,
name: 'Abc'
}, {
id: 131,
name: 'Bca'
}, {
id: 22,
name: 'bc'
}];
var groups = [];
items.forEach(function(item) {
var g = new MyGroup(item);
groups.push(g);
});
groups.forEach(function(g) {
console.log(g.getId()); //don't know why this logs 22 three times instead of all ids
});
In group.forEach I don't know why the id of the last item is getting logged. I am not able to understand what is going wrong. And how will I be able to get of the group for which g.getId() is invoked. Here is the plunkr
It's because you're adding methods to prototype and you overwrite in the loop each time the previous function so the function hold reference to last object when forEach loop finishes. What you need is to add function to this object:
function MyGroup(config) {
var myAttrs = ['id', 'name'];
this.g = new Group(config);
addGetterSetter(this, this.g, myAttrs)
}
function addGetterSetter(object, target, attrs) {
function addGetter(object, target, attr) {
var method = GET + capitalize(attr);
object[method] = function() {
return target[method]();
};
}
function addSetter(object, target, attr) {
var method = SET + capitalize(attr);
object[method] = function(value) {
return target[method](value);
};
}
for (var index = 0; index < attrs.length; index++) {
addGetter(object, target, attrs[index]);
addSetter(object, target, attrs[index]);
}
}
JSFIDDLE

Disable that acceess key sets focus to button

I have created a access key Angular directive.
angular.module('tcne.common').directive("accessKey", function () {
return {
restrict: "A",
scope: {
},
link: function (scope, element, attrs) {
var $element = $(element);
$element.attr("accesskey", attrs.accessKey);
var content = $element.html();
for (var i = 0; i < content.length; i++) {
var char = content[i];
if (char.toLowerCase() === attrs.accessKey.toLowerCase()) {
content = content.substr(0, i) + "<u>" + char + "</u>" + content.substr(i + 1);
break;
}
}
$element.html(content);
},
replace: false
};
});
It underscores the access key in the button Label and adds the access key attribute to the element. Can I somehow prevent the accesskey from setting the button in focus? It kills the purpose of keyboard short cuts
edit: Rolled my own acccess key
angular.module('tcne.common').directive('accessKey', ['$compile', '$interval', function ($compile, $interval) {
var modifierPressed = false;
$("body").keyup(function (e) {
if (modifierPressed && !e.altKey) {
modifierPressed = false;
digestScopes();
}
});
$("body").keydown(function (e) {
modifierPressed = e.altKey;
if (modifierPressed && scopes.hasOwnProperty(String.fromCharCode(e.which).toLowerCase())) {
var scope = scopes[String.fromCharCode(e.which).toLowerCase()];
scope.handle();
return;
}
if (modifierPressed) {
e.preventDefault();
digestScopes();
}
});
function digestScopes() {
for (var index in scopes) {
if (scopes.hasOwnProperty(index)) {
var scope = scopes[index]
scope.$digest();
}
}
}
function isModifierPressed() {
return modifierPressed;
}
var scopes = {};
return {
restrict: 'A',
scope: {
},
link: function (scope, element, attrs) {
var key = attrs.accessKey.toLowerCase();
var content = element.html();
var char;
for (var i = 0; i < content.length; i++) {
char = content[i];
if (char.toLowerCase() === key) {
content = content.substr(0, i) + '<u><strong ng-if="highlight()">{{char}}</strong><span ng-if="!highlight()">{{char}}</span></u>' + content.substr(i + 1);
break;
}
}
element.html(content);
var underscoreScope = scope.$new();
underscoreScope.char = char;
underscoreScope.highlight = isModifierPressed;
underscoreScope.handle = element.click.bind(element);
scopes[key] = underscoreScope;
scope.$on('$destroy', function () {
delete scopes[key];
});
$compile(element.find("u"))(underscoreScope);
},
replace: false
};
}]);
It also highlights the access key button when alt key is pressed which is nice
Any pit falls with this code? Thanks
Found a pitfall, element.html and then $compile will break any directives inside the element that is already compiled. So I changed to
var captionElement = element.contents().first(":text");
var content = captionElement.text();
And then I add my custom content like
var view = $("<span>").html(content);
captionElement.replaceWith(view);
$compile(view)(vm);
Please let me know if this is considered bad practice

Stopping a function inside a namespace

I have a namespace with a method to stop some characters being entered on a keypress event for inputs. If the character is detected, it returns false but I think that because it's called when the keypress is fired, the character still gets entered. How do I fix this problem?
If I take the function out of the namespace it works as intended but I don't want that.
HTML
<div id="personal-info">
<input id="first-name" class="personal-info" autofocus/><p id="error-first-name" class="error-text"></p>
<input id="last-name" class="personal-info"/><p id="error-last-name" class="error-text"></p>
</div>
Javascript:
fsa = (function() {
//OK - get selected element ID and write ID
var inputId = "";
var sigId = "";
var errId = "";
function GetAndSetLoc() {
inputId = document.activeElement.id;
sigId = "sig-" + document.activeElement.id;
errId = "error-" + document.activeElement.id;
}
var thisId = "";
// on button down, if the character is illegal, change the css of the error box
function showError(keyCode) {
var keys = [13,
"<".charCodeAt(0),
">".charCodeAt(0),
"$".charCodeAt(0),
"(".charCodeAt(0),
")".charCodeAt(0),
"?".charCodeAt(0),
"{".charCodeAt(0),
"}".charCodeAt(0),
"/".charCodeAt(0),
"#".charCodeAt(0),
"&".charCodeAt(0),
"*".charCodeAt(0),
"#".charCodeAt(0),
"~".charCodeAt(0)
];
var index;
for (index = 0; index < keys.length; ++index) {
if (keys[index] === keyCode) {
errorObject = $('#' + errId);
errorObject.html("Sorry, invalid character.").addClass('error');
setTimeout(function() {
errorObject.html("Sorry, invalid character.").removeClass('error');
}, 2000);
return false;
}
}
}
return {
GetAndSetLoc: GetAndSetLoc,
showError: showError
}
})();
// --------- Call the functions
//OK - get current read and write ids
$('.personal-info').focus(function(){
fsa.GetAndSetLoc();
});
$("input").keypress(function(e) {
fsa.showError(e.keyCode);
});
return false from keypress event in case of error
$("input").keypress(function(e) {
return fsa.showError(e.keyCode);
});

Custom star rating Angularjs

I am trying to do the custom star rating with angular.js, where I will have different set of images. I need to change it dynamically on hover the image. I am having 5 images
X X X X X
if I move the mouse pointer to 4th X I should be able to dynamically change
X
I used directive to achieve it.
.directive('fundooRating', function () {
return {
restrict: 'A',
template: '<ul class="rating">' +
'<li ng-repeat="star in stars" ng-class="star"
ng-click="toggle($index)"><img ng-mouseenter="hoveringOver($index)"
ng-src="{{con}}" />' +
'',
scope: {
ratingValue: '=',
max: '=',
readonly: '#',
onRatingSelected: '&'
},
link: function (scope, elem, attrs) {
var updateStars = function() {
scope.stars = [];
for (var i = 0; i < scope.max; i++) {
scope.stars.push({filled: i < scope.ratingValue});
}
};
scope.con = "Images/Rating/empty.png";
scope.hoveringOver = function(index){
var countIndex = index+1;
scope.Condition = "Good.png"
console.log("Hover " + countIndex);
};
scope.toggle = function(index) {
if (scope.readonly && scope.readonly === 'true') {
return;
}
scope.ratingValue = index + 1;
scope.onRatingSelected({rating: index + 1});
};
scope.$watch('ratingValue', function(oldVal, newVal) {
if (newVal) {
updateStars();
}
});
}
} });
How can I able to find which image my mouse pointer is and how to change the rest of Images. I want to do the custom rating option.
Angular UI gives you premade directives for the same purpose, did you try it?
http://angular-ui.github.io/bootstrap/
Go down to the Rating Title in the same page, i think it might solve your purpose.
You'll need a condition for each star in your updateStars function, either as a property for each, or a separate array. Then, you can do something like this:
scope.hoveringOver = function(index){
for (var i = 0; i <= index; i++) {
scope.stars[i].Condition = "Good.png";
}
};
Or the separate array way (assuming the array is scope.conditions):
scope.hoveringOver = function(index){
for (var i = 0; i <= index; i++) {
scope.conditions[i] = "Good.png";
}
};
You also need a function opposite of hoveringOver to remove the states to default/previous versions.

AngularJS directive to format model value for display but hold actual value in model

In angularjs I am trying to build a directive for currency textbox which takes values (amount and currency code) from the model in scope and then apply currency formatting to it. I am really having hard build the directive using ngModelController's parsers and formatters.
The parser is called first but the modelValue is undefined which means that data has not been returned from the server yet. So how do I make sure that parser is called when model is populated? Similarly my calculation also depends on currency code from db so how do I use these two values in parser?
I am unclear how the render function would look like in my case.
Here is the html:
<numericbox caption="RecurringAmount" controltype="currency" currencycode="{{Model.CurrencyCode}}" value="{{Model.RecurringAmount}}" />
Here is the directive:
export class NumericTextbox
{
constructor ()
{
var directive: ng.IDirective = {};
directive.restrict = "E";
directive.require = '^ngModel';
directive.replace = true;
directive.template = '<input type="text" />';
directive.scope = true;
directive.link = function ($scope: any, element: JQuerySE, attributes: any, ngModel: ng.INgModelController) {
var injector = angular.element(document.getElementById('app')).injector();
var currencyCacheService: CurrenciesCacheService;
currencyCacheService = injector.get('currenciesCacheService');
var currency = new Currency();
var currencySymbol: string;
ngModel.$formatters.push(function (modelValue) {
if (modelValue) {
var amount: number = modelValue || 0;
var currencyCode: string = attributes.currency || "";
if (currencyCode) {
currency = currencyCacheService.GetItem(currencyCode);
currencySymbol = currency.CurrencySymbol || currency.ISOCurrencyCode;
var formattedNumber = accounting.formatMoney(modelValue,
currencySymbol,
currency.NumberDecimalDigits,
currency.CurrencyGroupSeparator,
currency.CurrencyDecimalSeparator);
return formattedNumber;
}
return modelValue;
}
return 0;
});
ngModel.$parsers.push(function (viewValue: string) {
if (attributes.currenycode) {
var num = viewValue.substring(attributes.currenycode.len, viewValue.length - attributes.currenycode.len);
return num;
}
return viewValue;
});
$scope.$watch(function () {
var amount: any = {};
amount.currencycode = attributes.currencycode;
amount.value = attributes.value;
return amount;
}, function (newVal, oldVal, scope) {
debugger;
if (newVal != oldVal) {
var amount: number = newVal.value || 0;
var currencyCode: string = newVal.currencycode || "";
ngModel.$setViewValue({ num: amount, curr: currencyCode });
}
});
ngModel.$render = function () {
//$scope.value =
console.log(ngModel.$viewValue);
};
}
return directive;
}
}
The code is in typescript.
If you want to change the way a value is "displayed" then you need to think in terms of a filter and not a directive. Like this:
<div>model.amount|currencyFilter</div>

Categories