Understanding static methods inside the Validation class in angular4 - angular-reactive-forms

i am starting to learn model driven forms in angular and when i was going through the documentation of the model driven forms i found this
component.ts
this.myForm= this.fb.group({
'contact':['',Validators.required]
});
now when i went to the definition of the validator class i found this
export declare class Validators {
...
static required(control: AbstractControl): ValidationErrors | null;
...
}
which explains required is a static method in the validator class and it requires a AbstractControl as a parameter. but why then i am allowed to use it without passing any parameter inside it.

The required method returns an error map with the 'required' property: {'required':true} if the value of control: AbstractControl is empty and null if its not.
.
From the angular source code: https://github.com/angular/angular/blob/6.1.9/packages/forms/src/validators.ts#L133-L154
static required(control: AbstractControl): ValidationErrors|null {
return isEmptyInputValue(control.value) ? {'required': true} : null;
}
.
The reason why you can pass Validators.required without parenthesis and parameters is because Typescript is a superset of Javascript, which can store functions as variables:
var Foo = function (control: AbstractControl)
{
return anyVal;
};
Is the same as:
Foo(control: AbstractControl): any
{
return anyVal;
};
So doing this is completely valid
var Bar = Foo;
And because a function is just a variable holding executable code, we can store the function in multiple variables or pass it as a parameter, which is what is done in the FormControl.
So basically, when you do
const control = new FormControl('', Validators.required);
You are not executing the required method, because a method is executed only when parenthesis and parameters are added. Instead, you are passing the Validator function itself.

Related

Cypress get attribute value and assign it to variable in function

I am trying to get attribute value and return it from a function.
Here is the code that is working and can be used in a normal test class (into the integration folder).
describe('Example shows how to get attribute value.', () => {
// 'it' is used to create test case. You can add a name of the test case. You can have multiple test cases in one JS class.
it('Get attribute value.', () => {
// Cypress is not able to work with new tabs. It is not possible to switch between tabs. Cypress can manipulate the DOM tree, so we can change the element attributes and open the hyperlink in the same browser tab.
// 'visit()' method is used for navigating to URL address.
cy.visit('https://demoqa.com/links')
cy.xpath('//*[#id="simpleLink"]').then(function (element) {
// 'prop()' method is used to get the attribute value.
const url = element.prop('href')
cy.visit(url)
})
// Assert URL.
cy.url().should('include', 'demoqa.com').should('eq', 'https://demoqa.com/');
})
If I use the code that way - everything is working as expected.
But if I want to re-use the code and create a function like this:
// Give a value of the variable to use it for next function.
functionName = 'addAttribute';
// Declare a Cypress child custom command.
Cypress.Commands.add(functionName, { prevSubject: 'element' }, (subject: any, attributeName: string, attributeValue: string) => {
// Create a try-catch statement. If the function fails - we will recieve the error message.
try {
// Create the function steps after this comment.
cy
.wrap(subject)
.invoke('attr', attributeName, attributeValue)
.should('have.attr', attributeName, attributeValue)
} catch (error) {
// Create the error log and show it to the UI. Show the function name, the class where the function is located and catched error.
let errorMessage = `----------ERROR! It seems that we have an error. Please review the "${functionName}" function from "${__filename.split(__dirname + "/").pop()}" . The error is: ${error}`;
cy.log(errorMessage);
console.log(errorMessage);
}
})
The result is 'object' and I am not sure how to process it.
Here is the rest of the code:
describe("'getAttribute' custom child command example.", () => {
it("example shows how to use 'getAttribute' custom child command.", () => {
cy.visit('https://demoqa.com/buttons');
let attributeValue = cy.element('xpath','(//*[contains(text(),"Click Me")])[3]').getAttribute('class');
cy.log(`The attribute values is: ${attributeValue}`)
});
});
You have to do as below to have a return value:
let attributeValue = '';
cy.element('xpath', '(//*[contains(text(),"Click Me")])[3]')
.getAttribute('class')
.then((attr) => {
attributeValue = attr;
});
cy.log('The attribute values is:' + attributeValue)

How to trigger visitInputObject method on custom directive?

I'm building a custom directive in which I'm hoping to validate entire input objects. I'm using the INPUT_OBJECT type with the visitInputObject method on SchemaDirectiveVisitor extended class.
Every time I run a mutation using the input type then visitInputObject does not run.
I've used the other types/methods like visitObject and visitFieldDefinition and they work perfectly. But when trying to use input types and methods they will not trigger.
I've read all the available documentation I can find. Is this just not supported yet?
Some context code(Not actual):
directive #validateThis on INPUT_OBJECT
input MyInputType #validateThis {
id: ID
someField: String
}
type Mutation {
someMutation(myInput: MyInputType!): SomeType
}
class ValidateThisDirective extends SchemaDirectiveVisitor {
visitInputObject(type) {
console.log('Not triggering');
}
}
All the visit methods of a SchemaDirectiveVisitor are ran at the same time -- when the schema is built. That includes visitFieldDefinition and visitFieldDefinition. The difference is that when we use visitFieldDefinition, we often do it to modify the resolve function for the visited field. It's this function that's called during execution.
You use each visit methods to modify the respective schema element. You can use visitInputObject to modify an input object, for example to add or remove fields from it. You cannot use it to modify the resolution logic of an output object's field. You should use visitFieldDefinition for that.
visitFieldDefinition(field, details) {
const { resolve = defaultFieldResolver } = field
field.resolve = async function (parent, args, context, info) {
Object.keys(args).forEach(argName => {
const argDefinition = field.args.find(a => a.name === argName)
// Note: you may have to "unwrap" the type if it's a list or non-null
const argType = argDefinition.type
if (argType.name === 'InputTypeToValidate') {
const argValue = args[argName]
// validate here
}
})
return resolve.apply(this, [parent, args, context, info]);
}
}

Using Config.skip with a React-Apollo Query

I'm having some trouble making use of the Config.skip property inside of my graphql() wrapper.
The intent is for the query to be fired with an argument of currentGoalID, only after a user has selected an item from the drop-down (passing the associated currentGoalID) , and the (Redux) state has been updated with a value for currentGoalID.
Otherwise, I expect (as per Apollo documentation) that:
... your child component doesn’t get a data prop at all, and the options or props methods are not called.
In this case though, it seems that my skip property is being ignored based upon the absence of a value for currentGoalID, and the option is being called because the webpack compiler/linter throws on line 51, props is not defined...
I successfully console.log the value of currentGoalID without the graphql()
wrapper. Any idea why config.skip isn't working? Also wish to be advised on the proper use of this in graphql() function call. I've excluded it here, but am unsure of the context, thanks.
class CurrentGoal extends Component {
constructor(props) {
super(props)
}
render (){
console.log(this.props.currentGoalID);
return( <p>Current Goal: {null}</p>
)
}
}
const mapStateToProps = (state, props) => {
return {
currentGoal: state.goals.currentGoal,
currentGoalID: state.goals.currentGoalID,
currentGoalSteps: state.goals.currentGoalSteps
}
}
const FetchGoalDocByID = gql `
query root($varID:String) {
goalDocsByID(id:$varID) {
goal
}
}`;
const CurrentGoalWithState = connect(mapStateToProps)(CurrentGoal);
const CurrentGoalWithData = graphql(FetchGoalDocByID, {
skip: (props) => !props.currentGoalID,
options: {variables: {varID: props.currentGoalID}}
})(CurrentGoalWithState);
// export default CurrentGoalWithState
export default CurrentGoalWithData
See the answer here: https://stackoverflow.com/a/47943253/763231
connect must be the last decorator executed, after graphql, in order for graphql to include the props from Redux.

Jasmine testcase for covering link parameter of the directive

I have been working with the jasmine test cases for the directives but with the template parameter where I would directly check the output of the directive however I do not know how to cover the directive with no template section defined. Like the one given below:
appDirective.directive('linkscriptContainer', [ 'config', function(config) {
return {
restrict : 'A',
scope : {
'value' : '#'
},
link : function(scope, elem, attrs) {
//creating custom script tag
var customScript = document.createElement("script");
customScript.type = "text/javascript";
//checking if property value is available in Config object
//then reading the attribute value and constructing the src tag
if (config[attrs.value] != undefined) {
customScript.src = config[attrs.value];
}
//appending the script tag in linkScriptContainer
elem.append(customScript);
}
};
}]);
You don't need to do anything special for testing the link function. Once, you $compile your element and $scope.$digest() (just like you do for any other directive), link function will be executed by itself.
If your code var customScript = document.createElement("script"); is not getting covered, you might need to mock that function like this,
spyOn(document, "createElement").and.returnValue({});

Passing parameters to i18n model within XML view

How can we pass parameters to the i18n model from within a XML view?
Without parameters
<Label text="{i18n>myKey}"/>
works but how can we pass a parameter in that expression?
The only piece of information I've found so far is http://scn.sap.com/thread/3586754. I really hope that this is not the proper way to do it since this looks more like a (ugly) hack to me.
The trick is to use the formatter jQuery.sap.formatMessage like this
<Label text="{parts:['i18n>myKey', 'someModel>/someProperty'],
formatter: 'jQuery.sap.formatMessage'}"/>
This will take the value /someProperty in the model someModel and just stick it in myKey of your i18n resource bundle.
Edit 2020-05-19:
jQuery.sap.formatMessage is deprecated as of UI5 version 1.58. Please use sap/base/strings/formatMessage. See this answer on usage instructions.
At the moment this is not possible. But you can use this simple workaround, that works for me.
Preparations
First of all we create a general i18n handler in our Component.js. We also create a JSONModel with a simple modification, so that immediatly the requested path is returned.
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/model/json/JSONModel"
], function(UIComponent, JSONModel) {
"use strict";
return UIComponent.extend("your namespace", {
/**
* Add a simple "StringReturnModel" to the components' models
*/
init: function() {
// [...] your other code in the init method
// String Return Model
var stringModel = new JSONModel({});
stringModel.getProperty = function(sPath) {
return sPath;
};
this.setModel(stringModel, "string");
},
/**
* Reads out a string from our text domain.
* The model i18n is defined in your manifest.json
*
* #param param text parameter
* #param arr array for parameters
* #return string
*/
i18n: function(param, arr) {
var oBundle = this.getModel("i18n").getResourceBundle();
return oBundle.getText(param, arr);
},
});
});
Now, a model with the context {string>} exists. To use the i18n function in the XML view, we create a formatter function. This function parses the parts of the binding and returns the localized string.
sap.ui.define([
], function() {
"use strict";
var formatter = {
/**
* First argument must be the property key. The other
* one are the parameters. If there are no parameters, the
* function returns an empty string.
*
* #return string The localized text
*/
i18n: function() {
var args = [].slice.call(arguments);
if (args.length > 1) {
var key = args.shift();
// Get the component and execute the i18n function
return this.getOwnerComponent().i18n(key, args);
}
return "";
}
};
return formatter;
});
How To Use:
Together with the string-model you can use the formatter to pass paramaters to your i18n:
<Text text="{ parts: ['string>yourProperty', 'string/yourFirstParamter', 'anotherModel/yourSecondParamter'], formatter: '.model.formatter.i18n' }" />
You can pass how many paramaters as you want, but be sure that the first "part" is the property key.
What is written at the link is correct for complex formatting case.
But if you want to combine two strings you can just write
<Label text="{i18n>myKey} Whatever"/>
or
<Label text="{i18n>myKey1} {i18n>myKey2}"/>
create file formatter.js
sap.ui.define([
"sap/base/strings/formatMessage"
], function (formatMessage) {
"use strict";
return {
formatMessage: formatMessage
};
});
View
<headerContent>
<m:MessageStrip
text="{
parts: [
'i18n>systemSettingsLastLoginTitle',
'view>/currentUser',
'view>/lastLogin'
],
formatter: '.formatter.formatMessage'
}"
type="Information"
showIcon="true">
</m:MessageStrip>
</headerContent>
Controller
var oBundle = this.getModel("i18n").getResourceBundle();
MessageToast.show(this.formatter.formatMessage(oBundle.getText("systemSettingsLastLoginTitle"), "sInfo1", "sInfo2"));
i18n
systemSettingsLastLoginTitle=You are logged in as: {0}\nLast Login: {1}
As ugly as it may seem, the answer given in the link that you mentioned is the way to go. However it may seem complicated(read ugly), so let's break it down..
Hence, you can use the following for passing a single parameter,
<Label text="{path: 'someParameter', formatter: '.myOwnFormatter'}"/>
Here, the someParameter is a binding of a OData model attribute that has been bound to the whole page/control, as it is obvious that you wouldn't bind a "hardcoded" value in a productive scenario. However it does end with this, as you see there isn't a place for your i18n text. This is taken care in the controller.js
In your controller, add a controller method with the same formatter name,
myOwnFormatter : function(someParameter)
{
/* the 'someParameter' will be received in this function */
var i18n = this.i18nModel; /* However you can access the i18n model here*/
var sCompleteText = someParameter + " " + i18n.getText("myKey")
/* Concatenate the way you need */
}
For passing multiple parameters,
Use,
<Label text="{parts:[{path : 'parameter1'}, {path :'parameter2'}], formatter : '.myOwnFormatter'}" />
And in your controller, receive these parameters,
myOwnFormatter : function(parameter1, parameter2) { } /* and so on.. */
When all this is done, the label's text would be displayed with the parameter and your i18n text.
In principle it is exactly as described in the above mentioned SCN-Link. You need a binding to the key of the resource bundle, and additional bindings to the values which should go into the parameters of the corresponding text. Finally all values found by these bindings must be somehow combined, for which you need to specify a formatter.
It can be a bit shortened, by omitting the path-prefix inside the array of bindings. Using the example from SCN, it also works as follows:
<Text text="{parts: ['i18n>PEC_to',
'promoprocsteps>RetailPromotionSalesFromDate_E',
'promoprocsteps>RetailPromotionSalesToDate_E'}],
formatter: 'retail.promn.promotioncockpit.utils.Formatter.formatDatesString'}"/>
Under the assumption, that you are using {0},{1} etc. as placeholders, a formatting function could look like the following (without any error handling and without special handling of Dates, as may be necessary in the SCN example):
formatTextWithParams : function(textWithPlaceholders, any_placeholders /*Just as marker*/) {
var finalText = textWithPlaceholders;
for (var i = 1; i < arguments.length; i++) {
var argument = arguments[i];
var placeholder = '{' + (i - 1) + '}';
finalText = finalText.replace(placeholder, arguments[i]);
}
return finalText;
},

Resources