I'm trying to construct a sentence in an AngularJS view. For example, with variables {overdue: 5, name: "Kasper"}, I would like to have "{{overdue}} days overdue. Employee: {{name}}".
I tried using a function:
function renderLine() {
var results = new Array();
if (overdue) {
result.push("{{overdue}} days overdue");
}
if (overdue) {
result.push("{{points}} points");
}
/* combine into a string */
var result = "";
for (var i = 0; i < results.length; i+=1) {
if (result.length != 0) {
result += ", ";
}
result += results[i];
}
if (result.length > 0) {
result += ". ";
}
/* add name */
result += "Name: {{name}}";
return result,
}
More specifically, my question is: how can I use angular directives like {{variable}} in strings that are constructed programmatically and have angular process the directives? I don't want to construct the strings without using directives because the strings are translated into different languages, where the placing of variables within sentences might change.
I ended up creating an angular directive. The ui-if and ngRepeat directives were good starting points for a DOM-manipulating directive. There is the code for the somewhat modified directive:
angular.module("hk").directive("myDirective",
[ "$interpolate", "$log",
function($interpolate, $log) {
return {
transclude: 'element',
replace: false,
restrict: 'A',
terminal: true,
compile: function(tElement, tAttrs, linker) {
return function(scope, elem, attr) {
var lastElement;
var lastScope;
var expression = attr.myDirective;
scope.$watch(expression, function (item) {
if (lastElement) {
lastElement.remove();
lastElement = null;
}
if (lastScope) {
lastScope.$destroy();
lastScope = null;
}
lastScope = scope.$new();
lastScope.item = item;
linker(lastScope, function (clone) {
lastElement = clone;
var results = [];
if (item.isactive) {
results.push("++{{item.createdtime | age}} active");
if (item.status == 'started') {
results.push("++{{item.startedtime | age}} started: {{item.startedby_displayname}}");
}
}
if (item.islate) {
results.push("++{{item.latetime | age}} past due");
}
var result = "";
for (var i = 0; i < results.length; i+=1) {
if (result.length != 0) {
result += ", ";
}
result += results[i];
}
if (result.length > 0) {
result += ". ";
}
if (!item.startedby_displayname) {
if (item.assignedto_displayname) {
result += "++Assigned to {{item.assignedto_displayname}}.";
}
}
var interpolated = $interpolate(result)(lastScope);
elem.after(interpolated);
});
});
};
}
};
}]);
I think you could use $scope.$eval for your purposes. See this fiddle
In you could create a message like this:
$scope.$eval('"Hello "+name');
And then let the string to be evaluated change per language.
{de: '"Hallo " + name', it: '"Buon giorno "+ name', fr: '"Salut " +name'}
Or something along those lines (of course you'll want to have those translations checked).
You could also create a directive and use $compile to keep the exact strings you have now working.
Related
I wrote a litlle program in angular using ui-select. And I wrote a filter that do an OR search in different fields.
Here is my original filter : (whic works perfectly)
app.filter('orSearchFilter', function($parse) {
return function(items, props) {
var out = [];
if (angular.isArray(items)) {
var keys = Object.keys(props);
items.forEach(function(item) {
var itemMatches = false;
for (var i = 0; i < keys.length; i++) {
var prop = $parse(keys[i])(item);
var text = props[keys[i]].toLowerCase();
if (prop && prop.toString().toLowerCase().indexOf(text) !== -1) {
itemMatches = true;
break;
}
}
if (itemMatches) {
out.push(item);
}
});
} else {
out = items;
}
return out;
};
});
And here is my original plunker (which works) : http://plnkr.co/edit/IdqO5dtLXmC6gtqLxRdP?p=preview
The problem is that my filter won't be generic and I will use it in my final code just inside its controller. So, I want to attach it.
Here is the new version of the filter which is attached to the controller : (I didn't do any change...)
$scope.orSearchFilter = function($parse) {
return function(items, props) {
var out = [];
if (angular.isArray(items)) {
var keys = Object.keys(props);
items.forEach(function(item) {
var itemMatches = false;
for (var i = 0; i < keys.length; i++) {
var prop = $parse(keys[i])(item);
var text = props[keys[i]].toLowerCase();
if (prop && prop.toString().toLowerCase().indexOf(text) !== -1) {
itemMatches = true;
break;
}
}
if (itemMatches) {
out.push(item);
}
});
} else {
out = items;
}
return out;
};
};
Finally, in my html, I called this new filter by using this line :
<ui-select-choices group-by="groupByLetter"
repeat="contract in (contracts |
filter : orSearchFilter(contracts, {id.id: $select.search, policy.info.name : $select.search } ) |
orderBy: 'name') track by contract.name">
{{contract.name}} - {{contract.value}} ---- {{contract.id.id}} *** {{contract.policy.info.name }}
</ui-select-choices>
Can you help me please to fix that problem and help me to attach this filter to the scope of the controller?
Thank you !
Use the $filter service to programmatically fetch your filter function.
//Don't forget to inject $filter in your controller ofcourse
$scope.orSearchFilter = $filter('orSearchFilter');
Attach the filter directly to scope:
/* REMOVE constructor function
$scope.orSearchFilter = function($parse) {
return function(items, props) {
*/
// INSTEAD
$scope.orSearchFilter = function(items, props) {
var out = [];
//...
return out;
};
//};
Of course, also be sure that $parse is added to the injectables of the controller construction function.
referring to this plunker:
https://plnkr.co/edit/kBoDTXmm2XbxXjpDA4M9?p=preview
I am trying to create a directive that takes an array of json objects, syntax highlights them and its that syntax highlighted element in the page.
I have had mixed results with different watching methods, but what I cant figure out in this one is why the same id:1 is showing all the way down the list, why not id:1, id:2, id:2, id:3 etc.
angular.module("ngjsonview",[]).directive('ngjsoncollection',['$sce', function(){
'use strict';
return {
transclude: false
,restrict:'E'
,scope:{ d:'=',o:"=" }
,templateUrl: "./template.htm"
,controller:function($scope,$sce){
var fnPretty = function(objData){
if (typeof objData != 'string') { var strOut = JSON.stringify(objData, undefined, 2); }
strOut = strOut.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
return strOut.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) { cls = 'key';}
else { cls = 'string'; }
} else if (/true|false/.test(match)) { cls = 'boolean';}
else if (/null/.test(match)) {cls = 'null'; }
return '<span class="' + cls + '">' + match + '</span>';
});
};
$scope.html=$sce.trustAsHtml(fnPretty($scope.d));
$scope.$watchCollection('d',function(arrData){
$scope.arrHTML=[];
for (var i = 0, len = arrData.length; i < len; i++) {
$scope.arrHTML.unshift($sce.trustAsHtml(fnPretty(arrData[i])));
}
});
}
};
}]);
moving the timeout into a function helped with the specific problem I had
$scope.arrData=[];
function addIt(x) {
$timeout(function(){
$scope.arrData.push({id:x});
}, 100);
}
for(var i=0; i < 100; i++){
addIt(i)
}
Using this code, does angularjs supports binding that will also reflects the changes in the current tab you're working with into the other tab
<input type="text" ng-model="name"><span ng-bind="name"></span>
No, not using just that code.
However, I just wrote a nice directive for you to use with ng-model and ng-bind (does not work with just {{ inline expressions }}, though).
Here it is in action
And here is the code:
/**
* sync-between-tabs directive.
* Use in conjunction with ng-model or ng-bind to synchronise the contents
* between tabs. The value is synced using localStorage, so each thing to sync
* needs a unique key. Specify the key using the sync-between-tabs attribute
* value, or leave blank to use the ng-model or ng-bind attributes.
* Example usage:
* <input ng-model="some.thing" sync-between-tabs></input>
* uses the key "some.thing"
* <input ng-model="some.thing" sync-between-tabs="UNIQUEKEY25"></input>
* uses the key "UNIQUEKEY25"
* <span ng-bind="some.other.thing" sync-between-tabs></span>
* uses the key "some.other.thing"
* <span ng-bind="name" sync-between-tabs="UNIQUE_KEY_12"></span>
* uses the key "UNIQUE_KEY_12"
*/
app.directive('syncBetweenTabs', ['$window', '$parse',
function($window, $parse) {
var callbacks = {}, keysToWatch = [];
var localStorage = {
key: function(key) {
return '__syncValue_' + (key || '').replace('__syncValue_', '');
},
getItem: function(key) {
return $window.localStorage.getItem(localStorage.key(key));
},
setItem: function(key, val) {
$window.localStorage.setItem(localStorage.key(key), val);
},
onItemChange: function(key, callback) {
key = localStorage.key(key);
var keyAlreadyExists = false;
if (keysToWatch.indexOf(key) < 0) {
keysToWatch.push(key);
callbacks[key] = [callback];
} else {
callbacks[key].push(callback);
keyAlreadyExists = true;
}
return function deregister() {
if (!keyAlreadyExists)
keysToWatch = without(keysToWatch, key);
callbacks[key] = without(callbacks[key], callback);
if (callbacks.length === 0)
delete callbacks[key];
};
}
};
function without(arr, value) {
var newArr = [];
for (var i = 0, len = arr.length; i < len; ++i) {
if (arr[i] !== value)
newArr.push(arr[i]);
}
return newArr;
}
if ($window.addEventListener) {
$window.addEventListener("storage", handle_storage, false);
} else {
$window.attachEvent("onstorage", handle_storage);
}
function handle_storage(e) {
if (!e) e = $window.event;
if (callbacks[e.key])
angular.forEach(callbacks[e.key], function(callback) {
callback(e.newValue);
});
}
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elem, attrs, ngModelCtrl) {
var key = attrs['syncBetweenTabs'],
mode = 'unknown',
dereg, ngBindExpr
if (ngModelCtrl) {
mode = 'ngModel';
if (!key)
key = attrs['ngModel'];
} else if (attrs['ngBind']) {
mode = 'ngBind';
if (!key)
key = attrs['ngBind'];
} else {
throw new Error('sync-between-tabs only works for ng-model and ng-bind at present');
}
if (mode == 'ngModel') {
ngModelCtrl.$viewChangeListeners.push(function() {
localStorage.setItem(key, ngModelCtrl.$viewValue);
});
var currentValue = localStorage.getItem(key);
if (currentValue && currentValue !== ngModelCtrl.$viewValue) {
ngModelCtrl.$setViewValue(currentValue);
ngModelCtrl.$render();
}
dereg = localStorage.onItemChange(key, function(value) {
ngModelCtrl.$setViewValue(value);
ngModelCtrl.$render();
});
} else {
ngBindExpr = $parse(attrs['ngBind']);
dereg = localStorage.onItemChange(key, function(value) {
ngBindExpr.assign(scope, value);
scope.$digest();
});
}
scope.$on('$destroy', dereg);
}
}
}
]);
assuming both tabs have the same site loaded under the same domain, you can use local/session storage or cookies to post the shared data and make it available to any tab, the downside you have to constantly check for changes. but it should work
Let's say I have a following template:
"foo['x'] = '{{ myVar }}';"
Is there an angular way of checking if evaluating this against my current scope will give myVar some value ? I've got an array of such small templates and I only want to include them in the document when values are truthy. I was hoping either $interpolate, $parse or $eval might come in handy here. I know for sure that $interpolate is useless. What about the other two ? Maybe it's at least possible to get the name of the assigned value/expression ?
EDIT
I wasn't specific enough. What I was trying to achieve, was checking in advance if for example template '{{ myVar }}' evaluated against the current scope will return an empty string or value of the scope variable (if it exists). The case was really specific - when traversing an array of short templates I wanted to know if a template will return as an empty string or not, and only include it in my final html if it doesn't.
I'm not sure what are you trying to achieve, but to if you want to check if myVar is truthy in current scope, you can:
{{myVar ? "aw yiss" : "nope"}}
Evaluates to "aw yiss" if myVar is truthy and "nope" otherwise.
I ended up with a modified $interpolate provider but maybe someone knows a shorter solution :
app.provider('customInterpolateProvider', [
function $InterpolateProvider() {
var startSymbol = '{{';
var endSymbol = '}}';
this.startSymbol = function(value){
if (value) {
startSymbol = value;
return this;
} else {
return startSymbol;
}
};
this.endSymbol = function(value){
if (value) {
endSymbol = value;
return this;
} else {
return endSymbol;
}
};
this.$get = ['$parse', '$sce', function($parse, $sce) {
var startSymbolLength = startSymbol.length,
endSymbolLength = endSymbol.length;
function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
allOrNothing = !!allOrNothing;
var startIndex,
endIndex,
index = 0,
expressions = [],
parseFns = [],
textLength = text.length,
exp;
var getValue = function (value) {
return trustedContext ?
$sce.getTrusted(trustedContext, value) :
$sce.valueOf(value);
};
var stringify = function (value) {
if (value == null) {
return '';
}
switch (typeof value) {
case 'string':
break;
case 'number':
value = '' + value;
break;
default:
value = angular.toJson(value);
}
return value;
};
var parseStringifyInterceptor = function(value) {
try {
return stringify(getValue(value));
} catch(err) {
console.err(err.toString());
}
};
while(index < textLength) {
if ( ((startIndex = text.indexOf(startSymbol, index)) !== -1) &&
((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) !== -1) ) {
exp = text.substring(startIndex + startSymbolLength, endIndex);
expressions.push(exp);
parseFns.push($parse(exp, parseStringifyInterceptor));
index = endIndex + endSymbolLength;
} else {
break;
}
}
if (!expressions.length && !text.contains(startSymbol) && !text.contains(endSymbol)) {
expressions.push(text);
}
if (!mustHaveExpression) {
var compute = function(values) {
for(var i = 0, ii = expressions.length; i < ii; i++) {
if (allOrNothing && angular.isUndefined(values[i])) {
return;
}
expressions[i] = values[i];
}
return expressions.join('');
};
return angular.extend(function interpolationFn(context) {
var i = 0;
var ii = expressions.length;
var values = new Array(ii);
try {
if (ii && !parseFns.length) {
return expressions[0];
} else {
for (; i < ii; i++) {
values[i] = parseFns[i](context);
}
return compute(values);
}
} catch(err) {
console.err(err.toString());
}
}, {
exp: text,
expressions: expressions,
$$watchDelegate: function (scope, listener, objectEquality) {
var lastValue;
return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
var currValue = compute(values);
if (angular.isFunction(listener)) {
listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
}
lastValue = currValue;
}, objectEquality);
}
});
}
}
return $interpolate;
}];
}
]);
Lines below were added because in some cases I have a predefined text in my short template and I always want to render it :
if (!expressions.length && !text.contains(startSymbol) && !text.contains(endSymbol)) {
expressions.push(text);
}
if (ii && !parseFns.length) {
return expressions[0];
} else {
I am writing a directive, and I would like this directive to behave differently based on a value I pass to it, this value would be static. Ideally I would like to pass the value via an attribute in the DOM, like <div my-directive directive-value="value"> or even better <div my-directive="directive-value"> I know I could just add an attribute and check if it exists in the attr object in the link function, but is there a better way, what is the best practice here?
Here is my code:
layout.directive('columnDirective', function() {
return {
require: 'ngModel',
// scope: {
// directive_type: '=test'
// },
link: function (scope, el, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function (viewValue) {
var columns = scope.row.columns;
var column_total_before = 0;
angular.forEach(columns, function(column) {
var column_size = parseInt(column.size);
column_total_before = column_total_before + column_size;
});
if(attrs.typeOf != "add") {
//Remove current (prior to edit) value from total
column_total_before = column_total_before - ngModel.$modelValue;
var default_value = 1;
} else {
if (column_total_before == 12) {
var default_value = 0;
}
else {
var default_value = (12 - column_total_before);
}
}
//Convert string to integer
viewValue = parseInt(viewValue);
//Add total existing columns with new value
column_total = column_total_before + viewValue;
scope.column_total = column_total;
if(column_total >= 1 && column_total <= 12 && viewValue > 0) {
return viewValue;
} else if (isNaN(viewValue)) {
ngModel.$setViewValue(default_value);
return default_value;
} else if (column_total_before == 12 && viewValue == 0) {
return viewValue;
} else {
ngModel.$setViewValue(default_value);
ngModel.$render();
return default_value;
}
});
}
}
});
Currently using attrs.typeOf , in my html this works fine,but I don't know if it's best practice?