I have a custom image upload validator. This validator makes sure the mime type is correct, checks the file size, and that the dimensions are correct. Part of this validator creates an image element and gives it the datauri (as src) to check the width/height. Because of that last check, it uses the img.onload event, and thus, this is an async validator.
My validator will accept urls as well as data uris.
Here is the source of my validators. I've created a FileValidator base class that checks mimeType and file size while the ImageValidator checks dimensions.
The validation classes are as follows. I made these classes because they require state given the ValidationRules. The validator method is the actor
FileValidator
export class FileValidator {
constructor(protected _rules: FileValidationRules) {}
public validator(control: FormControl): Promise<any> {
return new Promise((resolve, reject) => {
var value = control.value;
if (value) {
if (this.isDataUri(value)) {
if (this._rules.acceptedMimeTypes && this._rules.acceptedMimeTypes.length) {
var mimeType = this.getMimeType(value);
var allowedMimeType = false;
for (var i = 0; i < this._rules.acceptedMimeTypes.length; i++) {
if (this._rules.acceptedMimeTypes[i] === mimeType) {
allowedMimeType = true;
break;
}
}
if (!allowedMimeType) {
resolve({
fileValidator: `File type not allowed: ${mimeType}. Allowed Types: ${this._rules.acceptedMimeTypes}`
});
}
if (this._rules.maxSize) {
var blob = this.dataURItoBlob(value);
blob.size > this._rules.maxSize;
resolve({
fileValidator: `File is too large. File Size: ${this.getFriendlyFileSize(blob.size)}, Max Size: ${this.getFriendlyFileSize(this._rules.maxSize)}`
});
}
}
} else if (!this.isUrl(value)) {
resolve({
fileValidator: 'Unknown format'
});
}
}
resolve();
});
}
... Helper Methods
}
ImageValidator
export class ImageValidator extends FileValidator {
constructor(_rules: ImageValidationRules) {
super(_rules);
}
public validator(control: FormControl): Promise<any> {
return new Promise((resolve, reject) => {
super.validator(control).then((results) => {
if (results && results.fileValidator) {
resolve({
imageValidator: results.fileValidator
});
}
var value = control.value;
if (value) {
var rules = <ImageValidationRules>this._rules;
if (this.isDataUri(value)) {
if (rules.width || rules.height) {
var img: HTMLImageElement = document.createElement('img');
img.onload = () => {
var validSize = true;
if (rules.width && img.width !== rules.width) {
validSize = false;
}
if (rules.height && img.height !== rules.height) {
validSize = false;
}
if (!validSize) {
resolve({
imageValidator: `Image must be ${rules.width || 'any'}x${rules.height || 'any'}. Actual Size: ${img.width}x${img.height}`
});
}
};
img.src = value;
}
}
}
resolve();
});
});
}
}
This all works. If I select an image that doesn't meet requirements I get the proper error message.
But this image uploader is in a tabbed area.
If I cycle between tabs I get this error-
Error: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
Simply put, my tabbed area markup is-
<li *ngFor="let page of pages"
[ngClass]="{active: page === activePage}">
<a (click)="setActivatePage(page)">
<i *ngIf="getFormGroup(page).invalid" class="fa fa-exclamation-circle font-red"></i>
{{page.title}}
</a>
</li>
And my tabbed content is (simply)-
<element *ngFor="let input of activePage.inputs"
... etc>
</element>
This line-
<i *ngIf="getFormGroup(page).invalid" class="fa fa-exclamation-circle font-red"></i>
Is causing the error. But I can't figure out why. I think it has something to do with the fact that I'm using async validators.
If I comment out all resolve calls in my async validators it works fine (but obviously my validation stops working).
So from what I can gather, changing the active page reapplies validators. And the async validators are updating the validity after the validity was checked. And for some reason this is a problem for angular, like it doesn't consider asynchronous tasks will update state.. asynchronously.
Has anyone any idea what that might be. Sorry I couldn't provide a simplified working example, my system is complex.
EDIT
Additionally, it only occurs if the value for the input is set. If I call reset on the control or set the value to null (in the validator), then this does not occur when cycling tabs.
The answer was that I was resolving the validator when nothing changed-
resolve();
I figured the async validator should be resolved no matter what. But that doesn't seem to be the case. Removing this empty resolve fixed the problem.
Related
In my react app I need to return a line which will be created based on a list.
Here is the object,
searchCriteria: {
op_company: "039",
doc_type: "ALL"
}
and in my UI, i need to show it as a paragraph with bold values. So the hard coded code would be like below
<p>Download request for op_company: <b>{searchCriteria.op_company}</b>, doc_type: <b>{searchCriteria.doc_type}</b></p>
But the object(searchCriteria) will be changed based on the user request. So I tried like below.
const getSearchCriteria = (criteria) => {
let searchCriteria = []
searchCriteria.push('Download request for')
Object.keys(criteria).forEach((key) => {
if(criteria[key] !== '') {
searchCriteria.push(` ${key}: ${criteria[key]},`)
}
});
return searchCriteria;
}
return (
<p>
{getSearchCriteria(searchCriteria).map((item) => <span key = {item}>{item}</span>)}
</p>
);
here i'm getting the expected output. But I can't get the value as bold (highlighted). Is there another way to directly deal with html elements?
I have a long set of checkboxes. I would like two groups of three of them to behave as radio buttons. Now, leaving aside my UX choices, how can I make this work?
The checkboxes are implemented as properties on a single object, layer:
data() {
return {
layer: {},
}
},
watch: {
layer: {
handler(val, oldval) {
mapping.updateLayers(val)
},
deep: true,
},
},
That works fine. But intercepting val and updating this.layer inside handler() doesn't:
handler: function(val, oldval) {
if (val.FutureYear) { this.layer.NextYear = false; this.layer.ExistingYear = false; }
if (val.ExistingYear) { this.layer.NextYear = false; this.layer.FutureYear = false; }
if (val.NextYear) { this.layer.ExistingYear = false; this.layer.FutureYear = false; }
mapping.updateFoiLayers(val);
},
How can I achieve this result? (I'd prefer not to have to implement actual radio buttons because that makes managing all the layers and UI more complex.)
Example: https://codepen.io/jacobgoh101/pen/NyRJLW?editors=0010
The main problem is the logic in watch.
If FutureYear is selected already, other field becomes unchangeable. Because if (val.FutureYear) is always the first one being triggered and the other 2 field will always be set to false immediately.
Another thing about watch is that it will be triggered when
user changed the value
program changed the value (this is unnecessary and make things harder to handle)
Therefore, handling #change event is more appropriate in this scenario.
JS
methods: {
handleChange: function(e) {
const name = e.target.name;
if (this.layer[name]) {
Object.keys(this.layer).map((key)=>{
if(key != name) {
this.layer[key] = false;
}
})
}
}
}
html
<div id="app">
<input type="checkbox"/ v-model="layer.FutureYear" name="FutureYear" #change="handleChange($event)">FutureYear<br/>
<input type="checkbox"/ v-model="layer.NextYear" name="NextYear" #change="handleChange($event)">NextYear<br/>
<input type="checkbox"/ v-model="layer.ExistingYear" name="ExistingYear" #change="handleChange($event)">ExistingYear<br/>
</div>
Using Protractor, I'm struggling with the syntax for selecting the parent element of a found item. We have a paginated list of records, and what I'd like to do is see if the right-arrow for that list is disabled. The trick though, is that the arrow gets disabled by its parent div element.
Here is the abbreviated markup.
<px-page-navigation id="myList" name="myList" page-number="vm.pageNumber" total-page-count="vm.totalPageCount" show-first-last-page="vm.showFirstLastPage" class="ng-isolate-scope">
<div>
<!-- Other pagination button markup omitted for clarity -->
<div class="btn-group px-margin-top-xx-small px-margin-left-xx-small px-font-link-disabled" ng-class="vm.arrowFont(vm.pageNumber + 1)" ng-click="vm.setPage(vm.pageNumber + 1)">
<!-- Find this "right arrow" element, then get the parent div and determine if it has a class of "px-font-link-disabled" -->
<i class="fa fa-chevron-right px-image-font bold"></i>
</div>
</div>
</px-page-navigation>
I've been able to successfully grab this element and click it.
<i class="fa fa-chevron-right px-image-font bold"></i>
But after capturing that element, I need to examine the parent container div to determine if that div has a class of "px-font-link-disabled". In cases where it's disabled, then the test will not click the right arrow. Otherwise the right-arrow icon gets clicked.
These are a couple of attempts that I've made to find the parent element.
// Attempt A
function findNewInstrument(newInstrumentName: string) {
element(by.linkText(newInstrumentName)).isPresent().then(function (exists) {
if (exists === true) {
console.log('found it');
return element(by.linkText(newInstrumentName));
} else {
var RIGHT_ARROW = '#myList i.fa-chevron-right';
// TODO: Is the right arrow enabled?
element(by.css('#myList > i.fa-chevron-right: parent')).then(function (arrowContainer) {
if (page.hasClass(arrowContainer, 'px-font-link-disabled')) {
// Paginated through all pages, and didn't find the instrument.
} else {
// Instrument was not on this page. Click the right-arrow and look for it again.
element(by.css(RIGHT_ARROW)).getLocation().then(function (location) {
browser.executeScript('window.scrollTo(' + location.x + ',' + location.y + ')').then(function () {
element(by.css(RIGHT_ARROW)).click();
});
});
findNewInstrument(newInstrumentName);
}
});
}
});
}
// Attempt B
function findNewInstrument(newInstrumentName: string) {
element(by.linkText(newInstrumentName)).isPresent().then(function (exists) {
if (exists === true) {
console.log('found it');
return element(by.linkText(newInstrumentName));
} else {
// TODO: Is the right arrow enabled?
var RIGHT_ARROW = '#myList i.fa-chevron-right';
element(by.css(RIGHT_ARROW)).then(function (rightArrow) {
var parentDiv = rightArrow[0].findElement(by.xpath('ancestor::div'));
if (page.hasClass(parentDiv, 'px-font-link-disabled')) {
// The right arrow is disabled.
} else {
// Click the right arrow, then inspect the grid again.
element(by.css(RIGHT_ARROW)).getLocation().then(function (location) {
browser.executeScript('window.scrollTo(' + location.x + ',' + location.y + ')').then(function () {
element(by.css(RIGHT_ARROW)).click();
});
});
findNewInstrument(newInstrumentName);
}
});
}
});
}
This hasClass function gets called by the above. This is in our base pageObject.
public hasClass(elm: protractor.ElementFinder, theClass: string) {
return elm.getAttribute('class').then(function (classes) {
return classes.split(' ').indexOf(theClass) !== -1;
});
};
In both cases (attempts A and B) the error that appears is:
TypeError: element(...).then is not a function
Once I've captured the right-arrow element, what syntax would I use to examine the classes of the parent div to determine if that div has the disabled class? If there is a simpler approach than the path I'm pursuing, then I'd be open to that as well. Thanks for your help.
TypeError: element(...).then is not a function
You cannot resolve the ElementFinder with then() anymore - this was an intentional breaking change in Protractor 2.0.0.
The following is how I would check the class of the parent div element:
var rightArrow = $('#myList i.fa-chevron-right');
var parentDiv = rightArrow.element(by.xpath('ancestor::div'));
expect(parentDiv).toHaveClass("px-font-link-disabled");
where toHaveClass is a convenient custom matcher:
beforeEach(function() {
jasmine.addMatchers({
toHaveClass: function() {
return {
compare: function(actual, expected) {
return {
pass: actual.getAttribute("class").then(function(classes) {
return classes.split(" ").indexOf(expected) !== -1;
})
};
}
};
},
});
});
We have just upgraded our textangular to 1.2.2, as this now supports drag and drop.
have seen defaultFileDropHandler within the textAngualrSetup, how ever, struggling to find any documentation to support this or how to use it.
defaultFileDropHandler:
/* istanbul ignore next: untestable image processing */
function (file, insertAction)
{
debugger;
var reader = new FileReader();
if(file.type.substring(0, 5) === 'image'){
reader.onload = function() {
if(reader.result !== '') insertAction('insertImage', reader.result, true);
};
reader.readAsDataURL(file);
return true;
}
return false;
}
Basically, we want to allow users to drag multiple pdf's, word docs etc and to upload on submit.
We could prob get this working in a fashion adding in functionality into defaultFileDropHandler within the settings page,
we implement ta by :-
<div text-angular data-ng-model="NoteText" ></div>
however, is there a cleaner way to achieve this?
Sorry about the lack of docs!
Basically the defaultFileDropHandler is triggered when the HTML element.on("drop") event is fired.
Implementing this via the textAngularSetup file is fine, but will be globally applied to all instances. To apply a handler for just one instance of textAngular use the ta-file-drop attribute which should be the name of a function on the scope with the same signature as defaultFileDropHandler. For Example:
JS In Controller
$scope.dropHandler = function(file, insertAction){...};
HTML
<div text-angular data-ng-model="NoteText" ta-file-drop="dropHandler"></div>
Both great answer, thank you!
I would just like to put the full code out to cover the global case since the code was only a snippet...
app.config( function( $provide ) {
$provide.decorator( 'taOptions', [ '$delegate', function( taOptions ) {
taOptions.defaultFileDropHandler = function( file, insertAction ) {
// validation
if( file.type.substring( 0, 5 ) !== "image" ) {
// add your own code here
alert( "only images can be added" );
return;
}
if( file.size > 500000 ) {
// add your own code here
alert( "file size cannot exceed 0.5MB" );
return;
}
// create a base64 string
var reader = new FileReader();
reader.onload = function() {
reader.result && insertAction( "insertImage", reader.result, true );
};
reader.readAsDataURL(file);
return true;
};
return taOptions;
}]);
});
My HTML structure is this:
<li ng-repeat="m in members">
<div class="col-name">{{m.name}}</div>
<div class="col-trash">
<div class="trash-button"></div>
</div>
</li>
What I want to be able to do is using protractor, click on the trash when m.name equals a specific value.
I've tried things like:
element.all(by.repeater('m in members')).count().then(function (number) {
for (var i = 0 ; i < number ; i++) {
var result = element.all(by.repeater('m in members').row(i));
result.get(0).element(by.binding('m.name')).getAttribute('value').then(function (name) {
if (name == 'John') {
result.get(0).element(by.className('trash-button')).click();
};
});
};
});
This seems like it should work, however, it seems like my function does not even run this.
I've also looked into promises and filters though have not been successful with those either. Keep getting errors.
var array = element.all(by.repeater('m in members'));
array.filter(function (guy) {
guy.getText().then(function (text) {
return text == 'John';
})
}).then(function (selected) {
selected.element(by.className('trash-button')).click()
}
All I would like to do is click on the corresponding trash when looking for a specific member list!
Any help would be much appreciated.
EDIT: suggested I use xpath once I find the correct element, which is fine, the problem is I cannot get the filter function's promise to let me use element(by.xpath...)
If I try:
var array = element.all(by.repeater('m in members'));
array.filter(function (guy) {
guy.getText().then(function (text) {
return text == 'John';
})
}).then(function (selected) {
selected.element(by.xpath('following-sibling::div/div')).click()
}
I get the error:
Failed: Object has no method 'element'
Figured it out. Following the Protractor filter guideline in the API reference and using alecxe's xpath recommendation, this code will click on any element you find after filtering it.
element.all(by.className('col-name')).filter(function (member, index) {
return member.getText().then(function (text) {
return member === 'John';
});
}).then( function (elements) {
elements[0].element(by.xpath('following-sibling::div/div/')).click();
});
You can change the xpath to select different stuff incase your HTML does not look like mine.
It looks like you can avoid searching by repeater, and use by.binding directly. Once you found the value you are interested in, get the following sibling and click it:
element.all(by.binding('m.name')).then(function(elements) {
elements.filter(function(guy) {
guy.getText().then(function (text) {
return text == 'John';
})
}).then(function (m) {
m.element(by.xpath('following-sibling::div/div')).click();
});
});
Or, a pure xpath approach could be:
element(by.xpath('//li/div[#class="col-name" and .="John"]/following-sibling::div/div')).click();