Why is Zone.js changing how AngularJS evaluates attributes? - angularjs

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

Related

Compiling string htmlusing angular compile not working having ng-repeat in it

Sorry for bad question format before. Changing this now
I have a html string which uses the ngrepeat to render the required div..
var templateHTML = "<div ng-repeat='entity in createCtrl.finalCCList'>\
<span>{{entity.name}}</span>\
</div>";
Here "createCtrl.finalCCList" has list of entity object which has name and id properties in it.
Now when I try to compile this using -
var compiledTemplateHTML = $compile(templateHTML)($scope);
if (compiledTemplateHTML && compiledTemplateHTML[0]) {
return compiledTemplateHTML[0].outerHTML;
}
else {
return "";
}
I get nothing. Whereas i checked and $scope.createCtrl.finalCCList does have the required values.
Am I missing anything here.
Ok. After lot of research I figured issue was the DOM rendered by ng-repeat inside the string html after compiles takes time and I am assigning it before it complete. If i uses the assignment in timeout it works fine.
So instead of this-
var compiledTemplateHTML = $compile(templateHTML)($scope);
if (compiledTemplateHTML && compiledTemplateHTML[0]) {
return compiledTemplateHTML[0].outerHTML;
}
else {
return "";
}
I am not returning but assigning to scope in $timeout
var compiledTemplateHTML = $compile(templateHTML)($scope);
if (compiledTemplateHTML && compiledTemplateHTML[0]) {
$timeout(function () {
createCtrl.isHeaderContent = compiledTemplateHTML[0].outerHTML;
}, 0);
}
Thanks.

how to avoid URL encoding with ui-sref

I've a link like this:
<a ui-sref="someState(Param:'مثال')"> A localized param </a>
when compiling the Angular-ui-router, generates a href like this :
A localized param
how can I avoid this?
what i have tried:
creating a new type using $urlMatcherFactoryProvider
$urlMatcherFactoryProvider.type('decoded', {
encode: function (item) {
return decodeURIComponent(item) // i put this to decode personally
},
decode: function (item) {
return decodeURIComponent(item);
},
is: function (item) {
return true;
}
});
this actually happens inside 'angular' not 'ngRoute' , angular forces encoding all urls using encodeURICompenent,
so you need to change encodeUriQuery inside angular.js so it would pass arabic characters without encoding
function encodeUriQuery(val, pctEncodeSpaces) {
var r = /[\u0600-\u06ff]|[\u0750-\u077f]|[\ufb50-\ufc3f]|[\ufe70-\ufefc]/;
if(r.test(val)){
return val.replace(/\s/g,'-');
}else{
return encodeURIComponent(val).
replace(/%40/gi, '#').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%3B/gi, ';').
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
}
}
if you're using minified version or don't want to bother looking into code here is a monkey patch you can copy and paste this anywhere in your code
window.encode = window.encodeURIComponent;
window.encodeURIComponent = function(val){return /[\u0600-\u06ff]|[\u0750-\u077f]|[\ufb50-\ufc3f]|[\ufe70-\ufefc]/.test(val) ? val.replace(/\s/g,'-') : window.encode(val)};
notice that part replace(/\s/g,'-') it replaces whitespaces with a dash because in angular it causes some issue to have whitespace in the url
the solution it's very simple actually
you can use javaScript decodeURIComponent() function on $scope.item.title for me it's worked like charm .
in controller use it like
$state.go("single-page", { contentType: "portfolio", date: "139411", title: decodeURIComponent("اولین-نمونه-کار-تست") });
in inspect you see something like :
its better for google seo ، google understood farsi better that way
but when you click on the link in url place you see this

Return Promise Value from Page Object using TypeScript

I'm trying to return a value from a WebDriver promise within a Protractor solution using TypeScript, but I'm getting undefined as response.
get nameInput(): string {
var value: string;
this.nameElement.getAttribute('value').then(v => value = v);
return value;
}
In the above case it seems like the function is not waiting for the promise to return, therefore I tried moving away from the getter style and declared the return type as WebDriver's promise:
getNameInput(): webdriver.promise.Promise<string> {
var nameElement = element(by.id('name'));
return nameElement.getText().then(v => { return v });
}
But I'm getting Function as the return instead of the value of v. Seems like the promise is not being unwrapped by Jasmine's expect, as it happens when running it in JS.
I know I can run the promise directly within the expect, but ideally I would create all the function's logic outside of any expectations, so that I can then feed the expectation the function call with any (if any) parameters only - instead of polluting the test case with promise logic.
Any ideas?
You don't need to resolve the promises, just return them:
getNameInput(): webdriver.promise.Promise<string> {
var nameElement = element(by.id('name'));
return nameElement.getText();
}
Then you need to have a real value returned from the getNameInput() - resolve it in your test:
getNameInput().then(v => { console.log(v) });
Note that, you also let expect() resolve it implicitly:
expect(getNameInput()).toEqual("Expected Value");

How to use protractor to check if an element is visible?

I'm trying to test if an element is visible using protractor. Here's what the element looks like:
<i class="icon-spinner icon-spin ng-hide" ng-show="saving"></i>
When in the chrome console, I can use this jQuery selector to test if the element is visible:
$('[ng-show=saving].icon-spin')
[
<i class=​"icon-spinner icon-spin ng-hide" ng-show=​"saving">​</i>​
]
> $('[ng-show=saving].icon-spin:visible')
[]
However, when I try to do the same in protractor, I get this error at runtime:
InvalidElementStateError:
invalid element state: Failed to execute 'querySelectorAll' on 'Document':
'[ng-show=saving].icon-spin:visible' is not a valid selector.
Why is this not valid? How can I check for visibility using protractor?
This should do it:
expect($('[ng-show=saving].icon-spin').isDisplayed()).toBe(true);
Remember protractor's $ isn't jQuery and :visible is not yet a part of available CSS selectors + pseudo-selectors
More info at https://stackoverflow.com/a/13388700/511069
The correct way for checking the visibility of an element with Protractor is to call the isDisplayed method. You should be careful though since isDisplayed does not return a boolean, but rather a promise providing the evaluated visibility. I've seen lots of code examples that use this method wrongly and therefore don't evaluate its actual visibility.
Example for getting the visibility of an element:
element(by.className('your-class-name')).isDisplayed().then(function (isVisible) {
if (isVisible) {
// element is visible
} else {
// element is not visible
}
});
However, you don't need this if you are just checking the visibility of the element (as opposed to getting it) because protractor patches Jasmine expect() so it always waits for promises to be resolved. See github.com/angular/jasminewd
So you can just do:
expect(element(by.className('your-class-name')).isDisplayed()).toBeTruthy();
Since you're using AngularJS to control the visibility of that element, you could also check its class attribute for ng-hide like this:
var spinner = element.by.css('i.icon-spin');
expect(spinner.getAttribute('class')).not.toMatch('ng-hide'); // expect element to be visible
I had a similar issue, in that I only wanted return elements that were visible in a page object. I found that I'm able to use the css :not. In the case of this issue, this should do you...
expect($('i.icon-spinner:not(.ng-hide)').isDisplayed()).toBeTruthy();
In the context of a page object, you can get ONLY those elements that are visible in this way as well. Eg. given a page with multiple items, where only some are visible, you can use:
this.visibileIcons = $$('i.icon:not(.ng-hide)');
This will return you all visible i.icons
If there are multiple elements in DOM with same class name. But only one of element is visible.
element.all(by.css('.text-input-input')).filter(function(ele){
return ele.isDisplayed();
}).then(function(filteredElement){
filteredElement[0].click();
});
In this example filter takes a collection of elements and returns a single visible element using isDisplayed().
This answer will be robust enough to work for elements that aren't on the page, therefore failing gracefully (not throwing an exception) if the selector failed to find the element.
const nameSelector = '[data-automation="name-input"]';
const nameInputIsDisplayed = () => {
return $$(nameSelector).count()
.then(count => count !== 0)
}
it('should be displayed', () => {
nameInputIsDisplayed().then(isDisplayed => {
expect(isDisplayed).toBeTruthy()
})
})
To wait for visibility
const EC = protractor.ExpectedConditions;
browser.wait(EC.visibilityOf(element(by.css('.icon-spinner icon-spin ng-hide')))).then(function() {
//do stuff
})
Xpath trick to only find visible elements
element(by.xpath('//i[not(contains(#style,"display:none")) and #class="icon-spinner icon-spin ng-hide"]))
element(by.className('your-class-name'))
.isDisplayed()
.then(function (isVisible) {
if (isVisible) { // element is visible
} else { // element is not visible
}
})
.catch(function(err){
console.error("Element is not found! ", err);
})
Here are the few code snippet which can be used for framework which use Typescript, protractor, jasmine
browser.wait(until.visibilityOf(OversightAutomationOR.lblContentModal), 3000, "Modal text is present");
// Asserting a text
OversightAutomationOR.lblContentModal.getText().then(text => {
this.assertEquals(text.toString().trim(), AdminPanelData.lblContentModal);
});
// Asserting an element
expect(OnboardingFormsOR.masterFormActionCloneBtn.isDisplayed()).to.eventually.equal(true
);
OnboardingFormsOR.customFormActionViewBtn.isDisplayed().then((isDisplayed) => {
expect(isDisplayed).to.equal(true);
});
// Asserting a form
formInfoSection.getText().then((text) => {
const vendorInformationCount = text[0].split("\n");
let found = false;
for (let i = 0; i < vendorInformationCount.length; i++) {
if (vendorInformationCount[i] === customLabel) {
found = true;
};
};
expect(found).to.equal(true);
});
Something to consider
.isDisplayed() assumes the element is present (exists in the DOM)
so if you do
expect($('[ng-show=saving]').isDisplayed()).toBe(true);
but the element is not present, then instead of graceful failed expectation, $('[ng-show=saving]').isDisplayed() will throw an error causing the rest of it block not executed
Solution
If you assume, the element you're checking may not be present for any reason on the page, then go with a safe way below
/**
* element is Present and is Displayed
* #param {ElementFinder} $element Locator of element
* #return {boolean}
*/
let isDisplayed = function ($element) {
return (await $element.isPresent()) && (await $element.isDisplayed())
}
and use
expect(await isDisplayed( $('[ng-show=saving]') )).toBe(true);
waitTillElementIsPresent(locator: Locator): promise.Promise<boolean>
{
const EC = protractor.ExpectedConditions;
return browser.wait(EC.visibilityOf(element(by.id('xyz')), browser.params.explicitWaitTime, 'Element taking too long to appear in the DOM');
}
const isDisplayed = await $('div').isDisplayed().then(null, err => false)

Angularjs promise not binding to template in 1.2

After upgrading to 1.2, promises returned by my services behave differently...
Simple service myDates:
getDates: function () {
var deferred = $q.defer();
$http.get(aGoodURL).
success(function (data, status, headers, config) {
deferred.resolve(data); // we get to here fine.
})......
In earlier version I could just do, in my controller:
$scope.theDates = myDates.getDates();
and the promises returned from getDates could be bound directly to a Select element.
Now this doesn't work and I'm forced to supply a callback on the promise in my controller or the data wont bind:
$scope.theDates = matchDates.getDates();
$scope.theDates.then(function (data) {
$scope.theDates = data; // this wasn't necessary in the past
The docs still say:
$q promises are recognized by the templating engine in angular, which means that in templates you can treat promises attached to a scope as if they were the resulting values.
They (promises) were working in older versions of Angular but in the 1.2 RC3 automatic binding fails in all my simple services.... any ideas on what I might be doing wrong.
There are changes in 1.2.0-rc3, including one you mentioned:
AngularJS 1.2.0-rc3 ferocious-twitch fixes a number of high priority
issues in $compile and $animate and paves the way for 1.2.
This release also introduces some important breaking changes that in some cases could break your directives and templates. Please
be sure to read the changelog to understand these changes and learn
how to migrate your code if needed.
For full details in this release, see the changelog.
There is description in change log:
$parse:
due to 5dc35b52, $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and
if absolutely needed, it can be reenabled during transitional period
via $parseProvider.unwrapPromises(true) api.
due to b6a37d11, feature added in rc.2 that unwraps return values from functions if the values are promises (if promise unwrapping is
enabled - see previous point), was reverted due to breaking a popular
usage pattern.
As #Nenad notices, promises are no longer automatically dereferenced. This is one of the most bizarre decisions I've ever seen since it silently removes a function that I relied on (and that was one of the unique selling points of angular for me, less is more). So it took me quite a bit of time to figure this out. Especially since the $resource framework still seems to work fine. On top of this all, this is also a release candidate. If they really had to deprecate this (the arguments sound very feeble) they could at least have given a grace period where there were warnings before they silently shut it off. Though usually very impressed with angular, this is a big minus. I would not be surprised if this actually will be reverted, though there seems to be relatively little outcry so far.
Anyway. What are the solutions?
Always use then(), and assign the $scope in the then method
function Ctrl($scope) {
foo().then( function(d) { $scope.d = d; });
)
call the value through an unwrap function. This function returns a field in the promise and sets this field through the then method. It will therefore be undefined as long as the promise is not resolved.
$rootScope.unwrap = function (v) {
if (v && v.then) {
var p = v;
if (!('$$v' in v)) {
p.$$v = undefined;
p.then(function(val) { p.$$v = val; });
}
v = v.$$v;
}
return v;
};
You can now call it:
Hello {{ unwrap(world) }}.
This is from http://plnkr.co/edit/Fn7z3g?p=preview which does not have a name associated with it.
Set $parseProvider.unwrapPromises(true) and live with the messages, which you could turn off with $parseProvider.logPromiseWarnings(false) but it is better to be aware that they might remove the functionality in a following release.
Sigh, 40 years Smalltalk had the become message that allowed you to switch object references. Promises as they could have been ...
UPDATE:
After changing my application I found a general pattern that worked quite well.
Assuming I need object 'x' and there is some way to get this object remotely. I will then first check a cache for 'x'. If there is an object, I return it. If no such object exists, I create an actual empty object. Unfortunately, this requires you to know if this is will be an Array or a hash/object. I put this object in the cache so future calls can use it. I then start the remote call and on the callback I copy the data obtained from the remote system in the created object. The cache ensures that repeated calls to the get method are not creating lots of remote calls for the same object.
function getX() {
var x = cache.get('x');
if ( x == undefined) {
cache.put('x', x={});
remote.getX().then( function(d) { angular.copy(d,x); } );
}
return x;
}
Yet another alternative is to provide the get method with the destination of the object:
function getX(scope,name) {
remote.getX().then( function(d) {
scope[name] = d;
} );
}
You could always create a Common angular service and put an unwrap method in there that sort of recreates how the old promises worked. Here is an example method:
var shared = angular.module("shared");
shared.service("Common", [
function () {
// [Unwrap] will return a value to the scope which is automatially updated. For example,
// you can pass the second argument an ng-resource call or promise, and when the result comes back
// it will update the first argument. You can also pass a function that returns an ng-resource or
// promise and it will extend the first argument to contain a new "load()" method which can make the
// call again. The first argument should either be an object (like {}) or an array (like []) based on
// the expected return value of the promise.
// Usage: $scope.reminders = Common.unwrap([], Reminders.query().$promise);
// Usage: $scope.reminders = Common.unwrap([], Reminders.query());
// Usage: $scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
// Usage: $scope.reminders.load();
this.unwrap = function(result, func) {
if (!result || !func) return result;
var then = function(promise) {
//see if they sent a resource
if ('$promise' in promise) {
promise.$promise.then(update);
}
//see if they sent a promise directly
else if ('then' in promise) {
promise.then(update);
}
};
var update = function(data) {
if ($.isArray(result)) {
//clear result list
result.length = 0;
//populate result list with data
$.each(data, function(i, item) {
result.push(item);
});
} else {
//clear result object
for (var prop in result) {
if (prop !== 'load') delete result[prop];
}
//deep populate result object from data
$.extend(true, result, data);
}
};
//see if they sent a function that returns a promise, or a promise itself
if ($.isFunction(func)) {
// create load event for reuse
result.load = function() {
then(func());
};
result.load();
} else {
then(func);
}
return result;
};
}
]);
This basically works how the old promises did and auto-resolves. However, if the second argument is a function it has the added benefit of adding a ".load()" method which can reload the value into the scope.
angular.module('site').controller("homeController", function(Common) {
$scope.reminders = Common.unwrap([], Reminders.query().$promise);
$scope.reminders = Common.unwrap([], Reminders.query());
$scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
function refresh() {
$scope.reminders.load();
}
});
These were some good answers, and helped me find my issue when I upgraded angular and my auto-unwrapping of promises stopped working.
At the risk of being redundant with Peter Kriens, I have found this pattern to work for me (this is a simple example of simply putting a number of famous people's quotes onto a page).
My Controller:
angular.module('myModuleName').controller('welcomeController',
function ($scope, myDataServiceUsingResourceOrHttp) {
myDataServiceUsingResourceOrHttp.getQuotes(3).then(function (quotes) { $scope.quotes = quotes; });
}
);
My Page:
...
<div class="main-content" ng-controller="welcomeController">
...
<div class="widget-main">
<div class="row" ng-repeat="quote in quotes">
<div class="col-xs-12">
<blockquote class="pull-right">
<p>{{quote.text}}</p>
<small>{{quote.source}}</small>
</blockquote>
</div>
</div>
</div>
...

Resources