I have this situation:
<div ng-bind-html="model"></div>
<div ng-if="!model">Echo me if model is empty</div>
And in controller:
model = $sce.trustAsHtml(model);
How can I render second <div> only if model is (was before trusting it) empty string? I tried ng-if="!model" and ng-if="model == ''" but neither of these does work.
As #MMhunter said, it should work (if model is empty string). I didn't include some details because I thought these are not relevant.
Basically, model is fetched via RESTful service, and empty model were saved in database as null. Because of that, neither !model nor model == '' worked. Now, ng-if is:
<div ng-if="model == '' || model == null">Echo me if model is empty</div>
and it works.
Conclusion: $sce.trustAsHtml(str) preserves null value (as well as undefined and empty string). AngularJS source:
function trustAs(type, trustedValue) {
var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null);
if (!Constructor) {
throw $sceMinErr('icontext',
'Attempted to trust a value in invalid context. Context: {0}; Value: {1}',
type, trustedValue);
}
if (trustedValue === null || isUndefined(trustedValue) || trustedValue === '') {
return trustedValue; // <<< THIS IS RESPONSIBLE >>>
}
// All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting
// mutable objects, we ensure here that the value passed in is actually a string.
if (typeof trustedValue !== 'string') {
throw $sceMinErr('itype',
'Attempted to trust a non-string value in a content requiring a string: Context: {0}',
type);
}
return new Constructor(trustedValue);
}
Related
I have an app with both Angular (2+) and AngularJS (1.x). We are using a third party AngularJS library that reads an object from its attrs array in a link function, like so:
//3rd party lib code:
module.directive('test', () => ({
template: `Look at the console`,
link(elt, scope, attrs) {
console.log('link attrs.props', attrs.props);
}
}))
Template:
<!-- someObject = {name: 'foo'} -->
<test props="{{someObject}}"></test>
We just upgraded to the latest version of AngularJS and we noticed a problem. Normally, attrs.props evaluates to a string representation of the object. Instead of getting a stringified object, we're getting "[object Object]"
I attempted a minimal reproduction but I couldn't reproduce the problem, until I tried importing Zone.js as you can see on this stackblitz:
https://stackblitz.com/edit/angularjs-attrs-test?file=app.js
If Zone.js is imported (which we need for Angular 2+), then attrs.props is "[object Object]". Without it, attrs.props is {name: 'foo'}.
Is this a known issue? Is there a workaround?
It is a good practice to always load ZoneJS before anything else, otherwise some strange problems like that can happen. In you example, if you simple move the ZoneJS import to the first line, it solves the problem.
ZoneJS overrides Object.prototype.toString method which leads to unexpected behavior in AngularJS stringify function:
function stringify(value) {
if (value == null) { // null || undefined
return '';
}
switch (typeof value) {
case 'string':
break;
case 'number':
value = '' + value;
break;
default:
if (hasCustomToString(value) && !isArray(value) && !isDate(value)) {
\/
true
value = value.toString(); // will be called since zone js overrided this method
} else {
value = toJson(value); // will be called without zonejs
}
}
return value;
}
In order to work around it you can disable this patch:
window.__Zone_disable_toString = false;
import 'zone.js/dist/zone';
Forked Stackblitz
I've got an application where I show a list of items with AngularJS. I'm trying to make an easy search on this list but it searches on everything inside that item. An example of an item:
{
id: 283727893,
name: 'Item A',
parent: {
id: 239495838,
name: 'Item B'
},
user: 'User C'
}
In the list I'm only writing the name of the root item, so parent.name does not appear.
The problem
However, if I search using an AngularJS filter by 'Item B', it still appears, because an inner property has this string.
What I've tried
I've put an object reference such as the following (where val is the text of the input):
vm.searchObj = {
name: val,
user: val
};
And then, on my DOM:
<li data-ng-repeat="[...] | filter: Ctrl.searchObj | orderBy: Ctrl.orderBy">
...
</li>
However, this works with conditional AND, so it only shows if both, name and user have val.
What I need
I'd like to take an input text and make a search to filter this list on given object properties (in plural).
So, in this example, this item should appear if I write 'Item A' or 'User C' (or just 'A' or just 'C' and so on), but not if I write 'B', because none of name and user have this letter.
I'd need it to take the reference object, in this case Ctrl.searchObj, and check if any of the given properties of the object contains val into the same structure of the filtered objects.
That, of course, should work into deeper levels of the object, so I could define the searchObj as follows and still be able to get if the filtered object has that structure inside:
vm.searchObj = {
parent: {
name: 'Item'
}
};
In this case, it should show those items where its parent.name property contains the word 'Item'.
If you need more information, please, let me know!
Thank you!
If I understand correctly, an object being searched (we'll call it target) is a match if:
both searchObj and target share at least one property name at the same level
The value of target's property is equal to or contains the value of searchObj's property
The code below (which includes your edits) should do what you need. I'll leave it to you to fill in any edge cases (checks for hasOwnProperty, comparison of different types, etc.). I think filtering like this should only happen when necessary, like when a user types. Creating an Angular filter out of this could be too expensive, since it runs on each digest cycle:
function objectContains(searchObj, target) {
for (var property in searchObj) {
var searchVal = searchObj[property];
var targetVal = target[property];
if (isObject(searchVal) && isObject(targetVal)) {
if(objectContains(searchVal, targetVal)){
return true;
}
}
if (
!isObject(searchVal) && !isObject(targetVal) &&
!isUndefinedOrNull(targetVal) &&
!isUndefinedOrNull(searchVal) &&
targetVal.toString().indexOf(searchVal.toString()) !== -1
) {
return true;
}
}
return false;
}
function isUndefinedOrNull(x) {
return typeof x === "undefined" || x === null;
}
function isObject(x) {
return typeof x === "object";
}
var filtered = list.filter(function(item) {
return objectContains(searchObj, item);
});
Try this way. Use true property on filter to get exactly what you search!
<li data-ng-repeat="[...] | filter: Ctrl.searchObj:true | orderBy: Ctrl.orderBy">
...
</li>
So, taking as the base the code by Frank Modica (thank you), I've come up with some editions on its compare function to look up not only for the first property, but if this does not match, keep up searching. This is the code modified:
function objectContains(searchObj, target) {
for (var property in searchObj) {
var searchVal = searchObj[property];
var targetVal = target[property];
if (isObject(searchVal) && isObject(targetVal)) {
if(objectContains(searchVal, targetVal)){
return true;
}
}
if (
!isObject(searchVal) && !isObject(targetVal) &&
!isUndefinedOrNull(targetVal) &&
!isUndefinedOrNull(searchVal) &&
targetVal.toString().indexOf(searchVal.toString()) !== -1
) {
return true;
}
}
return false;
}
In case it matches, it returns true because it needs only one match to be what we are looking for. In case it does not match, it keeps going.
We check for the second condition not to be an object neither of the values because they turn to [object OBJECT] when we apply toString(), so it returns true always. This way, if it's an object, it will ignore it (no need to do further checkings because we already done that in the first if).
Thank you Frank because I couldn't come up with this!
Now, it works perfectly!
I gave an object as followed
{
key1: [{...}, {...} ....],
key2: [{...}, {...} ....],
.........so on .....
}
I have an ng-repeat ng-repeat="(key, values) in data" and then inside that ng-repeat="val in values"
I want to set up an filter based on some property of objects stored in the array. I have set up below filter
.filter('objFilter', function () {
return function (input, search,field) {
if (!input || !search || !field)
return input;
var expected = ('' + search).toLowerCase();
var result = {};
angular.forEach(input, function (value, key) {
result[key] = [];
if(value && value.length !== undefined){
for(var i=0; i<value.length;i++){
var ip = value[i];
var actual = ('' + ip[field]).toLowerCase();
if (actual.indexOf(expected) !== -1) {
result[key].push(value[i]);
}
}
}
});
console.log(result);
return result;
};
The filter seems to work fine when I use ng-repeat="(date, values) in data| objFilter:search:'url'" but for some reason it is called too many times and causes Infinite $digest Loop.
Any solutions??
Edit:
I have created below plunker to show the issue. The filter works but look in the console for the errors. http://plnkr.co/edit/BXyi75kXT5gkK4E3F5PI
Your filter causes an infinite $digest loop because it always returns a new object instance. With the same parameters it returns a new object instance (it doesn't matter if the data inside the object is the same as before).
Something causes a second digest phase. I'm guessing it's the nested ng-repeat. Angular calls filters on every digest phase and because you filter returns a new value it causes the framework to reevaluate the whole outer ng-repeat block which causes the same on the inner ng-repeat block.
Option 1 - modify the filter
One fix you can do is to "stabilize" the filter. If it's called 2 times in a row with the same value it should return the same result.
Replace your filter with the following code:
app.filter('objFilter', function () {
var lastSearch = null;
var lastField = null;
var lastResult = null;
return function (input, search, field) {
if (!input || !search || !field) {
return input;
}
if (search == lastSearch && field == lastField) {
return lastResult;
}
var expected = ('' + search).toLowerCase();
var result = {};
angular.forEach(input, function (value, key) {
result[key] = [];
if(value && value.length !== undefined){
for(var i=0; i<value.length;i++){
var ip = value[i];
var actual = ('' + ip[field]).toLowerCase();
if (actual.indexOf(expected) !== -1) {
//if(result[key]){
result[key].push(value[i]);
//}else{
// result[key] = [value[i]];
//}
}
}
}
});
// Cache params and last result
lastSearch = search;
lastField = field;
lastResult = result;
return result;
};
});
This code will work but it's bad and prone to errors. It might also cause memory leaks by keeping the last provided arguments in memory.
Option 2 - move the filter on model change
Better approach will be to remember the updated filtered data on model change. Keep you JavaScript as is. Change only the html:
<body ng-controller="MainCtrl">
<div ng-if="data">
Search:
<input type="text"
ng-model="search"
ng-init="filteredData = (data | objFilter:search:'url')"
ng-change="filteredData = (data | objFilter:search:'url')">
<div ng-repeat="(date, values) in filteredData">
<div style="margin-top:30px;">{{date}}</div>
<hr/>
<div ng-repeat="val in values" class="item">
<div class="h-url">{{val.url}}</div>
</div>
</div>
</div>
</body>
First we add a wrapper ng-if with a requirement that data must have a value. This will ensure that our ng-init will have "data" in order to set the initial filteredData value.
We also change the outer ng-repeat to use filteredData instead of data. Then we update filtered data on the model change with the ng-change directive.
ng-init will fire once after data value is set
ng-change will be executed only when the user changes the input value
Now, no matter how many consecutive $digest phases you'll have, the filter won't fire again. It's attached on initialization (ng-init) and on user interaction (ng-change).
Notes
Filters fire on every digest phase. As a general rule try avoiding attaching complex filters directly on ng-repeat.
Every user interaction with a field that has ng-model causes a $digest phase
Every call of $timeout causes a $digest phase (by default).
Every time you load something with $http a digest phase will begin.
All those will cause the ng-repeat with attached filter to reevaluate, thus resulting in child scopes creation/destruction and DOM elements manipulations which is heavy. It might not lead to infinite $digest loop but will kill your app performance.
assume a filter like
app.filter('unread', function () {
return function (note) {
console.log(note);
return (note.status == 'unread');
};
});
I use this filter on an array in $rootScope
<span ng-class="(note| unread).length == 0 ? '' : 'active'">{{value.length}}</span>
in which $rootScope.note is an array. the span element is outside of ng-view and it's not related to $scope and I have many arrays of objects in the $scope.
I thought that the filter would log the note in the number of $rootScope.note.length. but It logs much more of it and I can't figure out a reasonable relation between elements of $rootScope, $scope and the logs in the console. may you please explain about this?
Edit:
filter corrected.
Isn't your html call to the filter incorrect ?
Try removing the :note like this :
<span ng-class="(note| unread).length == 0 ? '' : 'active'">{{value.length}}</span>
What's after the semicolon are additional arguments. For example if you wanted to filter important unread notes only :
app.filter('unread', function () {
return function (note, type) {
console.log(note);
return (note.status == 'unread' && note.type == type);
};
});
<span ng-class="(note| unread:'important').length == 0 ? '' : 'active'">{{value.length}}</span>
I want to create a single function that will be used for total project.
It will work based on the ng-model passed into it.
For ex:-
$scope.checkBoxValueChanged=function(model) {
if($scope.model=="AA") {
$scope.model="BB";
}
else {
$scope.model="BB";
}
};
});
If i have the passed model's value as "AA" then i need to assign the passed model's value as "BB"
But what i am getting is the model value instead of model name.
Can anyone tell me how to get the model instead of model value.
Any help will be highly appreciated.
You should pass the property name (what you refer to as "model") as a string parameter.
Then you can access it using the object[key] syntax.
$scope.checkBoxValueChanged = function(propName) {
if($scope[propName] === 'AA') {
$scope[propName] = 'BB';
} else {
$scope[propName] = 'AA';
}
};
BTW, in JS, scope.model is equivalent to scope['model'], so the dot syntax won't work as you want it to.