validation with onlyIf doesn't work - asp.net-mvc-3

I am running into issue when trying to do :
newNoteText: ko.observable().extend({ required: { onlyIf: function () { return this.ShowNote == true } } })
I noticed that this doesn’t work, but when I put the code back like this, it works fine:
newNoteText: ko.observable().extend({ required: true })
http://sdrv.ms/WJC3fS
https://skydrive.live.com/redir?resid=33048714B5BF3B4B!2027

The proper syntax to use the onlyIf option on a rule is :
newNoteText: ko.observable().extend({
required: {
onlyIf: function(){
return someFlagIsTrue;
}
}
Cf. this answer to one of your previous questions (by Eric Barnard, main contributor to Knockout Validation).
Regarding, your code, apart from Knockout Validation's syntax, there are two other things to worry about :
return something == true is the same as return something (not mentioning JavaScript's way of handling == and === operators, see more about this here).
In your function the value of this isn't what you seem to think it is (here it refers to the parameter between the parenthesis of extend()).
If you want to access the value of one of the other observables of your view model, you should be doing something like :
newNoteText: ko.observable().extend({
required: {
onlyIf: function(){
return self.ShowNote();
}
}
Where self is defined at the top of your view model constructor, like var self = this;. See more about this pattern here.

The params option mentioned by ThibWeb isn't needed - knockout.validation.js sets this by default to true in the addExtender method:
if (params.message || params.onlyIf) {
return ko.validation.addRule(observable, {
rule: ruleName,
message: params.message,
params: utils.isEmptyVal(params.params) ? true : params.params,
condition: params.onlyIf
});
The Eric Barnard answer was in 2011, presumably before this was defaulted to true.
However, if your code sets any validation defaults, you might be writing an HTML5 required attribute into the HTML, or reading one that is set in the HTML:
ko.validation.configure({
parseInputAttributes: true, //default is false
writeInputAttributes: true //default is false
});
I've found this causes an onlyIf required condition to be effectively ignored, since you are adding an extra required rule which is always applied.

Related

Cypress conditional statement for a disabled button element

Trying to click a button based on another element with enabled/disabled status. For some reason, my disabled check code is not working and it always ends in another statement ('No existing routes found') even though the UI has a select button enabled.
cy.get('voyage-suggested-routes')
.find('button.selectButton')
.then(($routes) => {
if ($routes.is(":disabled")) {
cy.log("No existing routes found...")
} else {
cy.log("Deleting......")
cy.get('.delete-button').click({ force: true, multiple: true })
}
});
This is the DOM: (There are 3 elements by default and a delete option will be there for each Select button if it is not disabled.)
<button class="selectButton" disabled route="1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="..."></path></svg>
SELECT
</button>
Tried the jquery method as well but the same result.
var btnStatus = Cypress.$('.selectButton')
.is(":disabled");
if (btnStatus == true) {
cy.log("Deleting......")
cy.get('.delete-button').click({ force: true, multiple: true })
} else {
cy.log("No existing routes found...")
}
What am I missing?
Update 1:
After Electron's input, my new code is:
cy.get('voyage-suggested-routes')
.find('button.selectButton')
.then(($routes) => {
if ($routes.is(":disabled").length === 0) {
cy.log("No existing routes found...")
} else {
cy.log("Deleting......")
cy.get('.delete-button').click({ force: true, multiple: true })
}
});
From the docs jQuery .is()
Description: Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments.
So if only one route is disabled, the delete will not go ahead.
Try using a filter to see if any are disabled.
cy.get('voyage-suggested-routes')
.find('button.selectButton')
.then(($routes) => {
const disabled = $routes.filter(":disabled")
if ($routes.length === disabled.length) {
cy.log("No existing routes found...")
} else {
cy.log("Deleting......")
cy.get('.delete-button').click({ force: true, multiple: true })
}
})
It's because you need each instead of then, like this:
.each(($routes) => {
in order to perform as many actions as there are button elements.
Edit: as Electron stated in the comments, the suggestion below will fail a test if all buttons are disabled, so take care if you use it.
And to better optimize your code, your can set the selector as .find('button.selectButton:not(:disabled)') then you don't need if block at all, just the delete statement.
Here's a custom command which conditionally runs a callback, depending on the result of filtering by given selector.
Not a lot of difference to .then(($routes) => { const disabled = $routes.filter(":disabled") pattern. Unfortunately ending part of a chain is quite difficult, as the whole test is considered one chain.
Cypress.Commands.add('maybe', {prevSubject:true}, (subject, selector, callback) => {
const result = subject.filter(selector)
if (result.length > 0) {
cy.log(`Maybe: Found ${result.length} "${selector}"`)
cy.wrap(result).then(callback)
return
}
cy.log(`Maybe: Not found: "${selector}"`)
})
cy.get('button.selectButton')
.maybe(':not(:disabled)', ($result) => {
// can use result of filter here
console.log('result is', $result))
// or conditionally run some other commands
cy.log(`Deleting...`)
cy.get('.delete-button').click({ force: true, multiple: true })
})
// runs either way
cy.wrap('something')
.then(x => console.log('next', x))

Sails.js and Waterline: dynamic validation by DB request

I use Sails 11.1 and Waterline 2.11.2 with a MongoDB database.
I would like to validate data inserted in my "Article" model using a in validator for 1 attribute.
Before, I was doing the job with lifecycle callbacks (beforeCreate and beforeUpdate especially), but it makes double code.
Here you have the model, truncated with just the attribute in question :
module.exports =
{
schema: true,
autoCreatedAt: false,
autoUpdatedAt: false,
attributes:
{
theme:
{
model: 'Theme',
required: true
}
}
}
I know how to define it statically:
in: ['something', 'something other']
I know how to call constants I defined in my constants.js file :
defaultsTo: function ()
{
return String(sails.config.constants.articleDefaultTheme);
}
But I would like to get all themes in my DB, to have a dynamic in validation. So, I wrote this :
theme:
{
model: 'Theme',
required: true,
in: function ()
{
Theme.find()
.exec(function (err, themes)
{
if (err)
{
return next({ error: 'DB error' });
}
else if (themes.length === 0)
{
return next({ error: 'themes not found' });
}
else
{
var theme_ids = [];
themes.forEach(function (theme, i)
{
theme_ids[i] = theme.theme_id;
});
return theme_ids;
}
});
}
}
But it's not working, I have always the "1 attribute is invalid" error. If I write them statically, or if I check in the beforeCreate method with another DB request, it works normally.
If I sails.log() the returned variable, all the themes ids are here.
I tried to JSON.stringify() the returned variable, and also to JSON.parse(JSON.stringify()) it. I also tried to convert the theme.theme_id as a string with the String() function, but nothing else...
What am I doing wrong? Or is it a bug?
You can also check my question here : Waterline GitHub issues
Models's configuration at your attributes scope at in field of course will throw an error, because it should not use a function, especially your function is not return anything, also if you force it to return something, it will return Promise that Theme.find()... did.
Try use different approach. There are exist Model Lifecycle Callbacks. You can use something like beforeCreate, or beforeValidate to manually checking your dynamic Theme, if it's not valid, return an error.
Or if it's achievable using standard DB relation, just use simple DB relation instead.

How to set mandatory route parameters

I want to make a route with has a mandatory parameter. If not, it should fall into
$urlRouterProvider.otherwise("/home");
Current route:
function router($stateProvider) {
$stateProvider.state("settings", {
url: "^/settings/{id:int}",
views: {
main: {
controller: "SettingsController",
templateUrl: "settings.html"
}
}
});
}
Currently both the routes below are valid:
http://myapp/settings //Should be invalid route
http://myapp/settings/123
Any ideas?
Use a state change start listener to check if params were passed:
$rootScope.$on('$stateChangeStart',
function (event, toState, toParams, fromState, fromParams) {
if(toState.name==="settings")
{
event.preventDefault(); //stop state change
if (toParams.id===undefined)
$state.go("home");
else
$state.go(toState, toParams);
}
});
The following solution is valid for ui-router 1.0.0:
.config(($stateProvider, $transitionsProvider) => {
//Define state
$stateProvider.state('verifyEmail', {
parent: 'portal',
url: '/email/verify/:token/:optional',
component: 'verifyEmail',
params: {
token: {
type: 'string',
},
optional: {
value: null,
squash: true,
},
},
});
//Transition hooks
$transitionsProvider.onBefore({
to: 'verifyEmail',
}, transition => {
//Get params
const params = transition.params();
//Must have token param
if (!params.token) {
return transition.router.stateService.target('error', {
type: 'page-not-found',
});
}
});
})
The above will make the :token parameter mandatory and the :optional parameter optional. If you try to browse to the page without the token parameter it will fail the transition and redirect to your error page. If you omit the :optional parameter however, it will use the default value (null).
Remember to use squash: true on the trailing optional parameters, because otherwise you'll also get a 404 if you omit the trailing / in the URL.
Note: the hook is required, because if you browse to email/verify/ with a trailing slash, ui-router will think the token parameter is an empty string. So you need the additional handling in the transition hook to capture those cases.
In my app I had to make required parameters for a lot of routes. So I needed a reusable and DRY way to do it.
I define a constants area in my app to access global code. I use for other things as well.
I run this notFoundHandler at app config time. This is setting up a router state for handling errors. It is setting the otherwise route to this error route. You could define a different route for when a required parameter is missing, but for us this was defined as being the same as a 404 experience.
Now at app run time I also define a stateChangeErrorHandler which will look for a rejected route resolve with the 'required-param' string.
angular.module('app')
.constant('constants', constants)
.config(notFoundHandler)
.run(stateChangeErrorHandler);
// use for a route resolve when a param is required
function requiredParam(paramName) {
return ['$stateParams', '$q', function($stateParams, $q) {
// note this is just a truthy check. if you have a required param that could be 0 or false then additional logic would be necessary here
if (!$stateParams[paramName]) {
// $q.reject will trigger the $stateChangeError
return $q.reject('required-param');
}
}];
}
var constants = {
requiredParam: requiredParam,
// define other constants or globals here that are used by your app
};
// define an error state, and redirect to it if no other route matches
notFoundHandler.$inject = ['$stateProvider', '$urlRouterProvider'];
function notFoundHandler($stateProvider, $urlRouterProvider) {
$stateProvider
//abstract state so that we can hold all our ingredient stuff here
.state('404', {
url: '/page-not-found',
views: {
'': {
templateUrl: "/app/error/error.tpl.html",
}
},
resolve: {
$title: function () { return 'Page Not Found'; }
}
});
// redirect to 404 if no route found
$urlRouterProvider.otherwise('/page-not-found');
}
// if an error happens in changing state go to the 404 page
stateChangeErrorHandler.$inject = ['$rootScope', '$state'];
function stateChangeErrorHandler($rootScope, $state) {
$rootScope.$on('$stateChangeError', function(evt, toState, toParams, fromState, fromParams, error) {
if (error && error === 'required-param') {
// need location: 'replace' here or back button won't work on error page
$state.go('404', null, {
location: 'replace'
});
}
});
}
Now, elsewhere in the app, when I have a route defined, I can make it have a required parameter with this route resolve:
angular.module('app')
.config(routeConfig);
routeConfig.$inject = ['$stateProvider', 'constants'];
function routeConfig($stateProvider, constants) {
$stateProvider.state('app.myobject.edit', {
url: "/:id/edit",
views: {
'': {
template: 'sometemplate.html',
controller: 'SomeController',
controllerAs: 'vm',
}
},
resolve: {
$title: function() { return 'Edit MyObject'; },
// this makes the id param required
requiredParam: constants.requiredParam('id')
}
});
}
I'd like to point out that there shouldn't be any problem with accessing the /settings path, since it doesn't correspond to any state, unless you've used inherited states (see below).
The actual issue should happen when accessing the /settings/ path, because it will assign the empty string ("") to the id parameter.
If you didn't use inherited states
Here's a solution in plunker for the following problem:
accessing the /state_name/ path, when there's a state with url /state_name/:id
Solution explanation
It works through the onBefore hook (UI router 1.x or above) of the Transition service, which prevents transitioning to states with missing required parameters.
In order to declare which parameters are required for a state, I use the data hash like this:
.state('settings', {
url: '/settings/:id',
data: {
requiredParams: ['id']
}
});
Then in app.run I add the onBefore hook:
transitionService.onBefore({}, function(transition) {
var toState = transition.to();
var params = transition.params();
var requiredParams = (toState.data||{}).requiredParams || [];
var $state = transition.router.stateService;
var missingParams = requiredParams.filter(function(paramName) {
return !params[paramName];
});
if (missingParams.length) {
/* returning a target state from a hook
issues a transition redirect to that state */
return $state.target("home", {alert: "Missing params: " + missingParams});
}
});
If you used inherited states
You could implement the same logic via inherited states:
function router($stateProvider) {
$stateProvider
.state('settings', {
url: '/settings'
})
.state('settings.show", {
url: '/:id'
});
}
then you'd need to add the abstract property to the parent declaration, in order to make /settings path inaccessible.
Solution explanation
Here's what the documentation says about the abstract states:
An abstract state can never be directly activated. Use an abstract state to provide inherited properties (url, resolve, data, etc) to children states.
The solution:
function router($stateProvider) {
$stateProvider
.state('settings', {
url: '/settings',
abstract: true
})
.state('settings.show", {
url: '/:id'
});
}
Note: that this only solves the issue with /settings path and you still need to use the onBefore hook solution in order to also limit the access to /settings/.
it is not very well documented, but you can have required and optional parameters, and also parameters with default values.
Here is how you can set required params:
function router($stateProvider) {
$stateProvider.state("settings", {
url: "^/settings/{id:int}",
params: {
id: {}
},
views: {
main: {
controller: "SettingsController",
templateUrl: "settings.html"
}
}
});
}
I never used params with curly brackets, just with the semicolon, like this url: "^/settings/:id", but from what I read, those are equivalent.
For other types of parameters, please see the other half of my answer here: AngularJS UI Router - change url without reloading state
Please note that when I added that answer, I had to build ui-router from source, but I read that functionality has been added to the official release by now.

Angular UI Router state.go parameters not available in stateChangeStart

I'm using Angular UI Router and I need to send a parameter with the state.go method. Like this:
$state.go('myState', { redirect : true });
I also need to check that parameter in the event stateChangeStart. Like this:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
//redirect parameter is not available here.
//should be in toParams right?
}
Edit: here's my statedefinition:
$stateProvider.state('customer.search', {
url: '/search',
views: {
"right-flyover#": {
templateUrl: 'customer/search/search.tpl.html',
controller: 'CustomerSearchCtrl'
}
},
data: {
pageTitle: 'Sök användare',
hidden: true
}
});
The ui-router will pass parameters which were defined for a state hierarchy - we are navigating to. Please check:
URL Parameters (cite:)
Often, URLs have dynamic parts to them which are called parameters. There are several options for specifying parameters. A basic parameter looks like this:
$stateProvider
.state('contacts.detail', {
url: "/contacts/:contactId",
templateUrl: 'contacts.detail.html',
controller: function ($stateParams) {
// If we got here from a url of /contacts/42
expect($stateParams).toBe({contactId: 42});
}
})
So, if you want to work with param redirect, your state should look like this:
$stateProvider.state('customer.search', {
url: '/search/:redirect',
...
});
then we can use:
$state.go('customer.search', { redirect : true });
and that would be part of $statePrams
But maybe you try to use sent options, not parameters:
$state.go(to [, toParams] [, options]) (cite:)
options
Object - If Object is passed, object is an options hash. The following options are supported:
location Boolean or "replace" (default true), If true will update the url in the location bar, if false will not. If string "replace", will update url and also replace last history record.
inherit Boolean (default true), If true will inherit url parameters from current url.
relative stateObject (default $state.$current), When transitioning with relative path (e.g '^'), defines which state to be relative from.
notify Boolean (default true), If true will broadcast $stateChangeStart and $stateChangeSuccess events.
reload v0.2.5 Boolean (default false), If true will force transition even if the state or params have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd use this when you want to force a reload when everything is the same, including search params.
And that would be then the third param (reload instead of redirect):
$state.go('myState', null, { reload: true });
I recently solved the problem by using cache attribute is stateProvider and set it to false
like
.state('home.stats', {
cache: false,
url:'/stats',
templateUrl: 'views/stats.html',
controller: 'StatsCtrl',
controllerAs: 'stats'
})
$state.go actually returns a promise, so you can do something like:
$state.go('myState').then(() => {
if (redirect) {
// do something
}
});
I realize this isn't a generalized solution to the problem which is what I was looking for as well, but it allowed me to get the job done. You could easily wrap this in a method on the $state object if you need to reuse the logic repeatedly.

Mongoose conditional required validation

I'm using mongoose and trying to set a custom validation that tells the property shall be required (ie. not empty) if another property value is set to something. I'm using the code below:
thing: {
type: String,
validate: [
function validator(val) {
return this.type === 'other' && val === '';
}, '{PATH} is required'
]}
If I save a model with {"type":"other", "thing":""} it fails correctly.
If I save a model with {"type":"other", "thing": undefined} or {"type":"other", "thing": null} or {"type":"other"} the validate function is never executed, and "invalid" data is written to the DB.
As of mongoose 3.9.1, you can pass a function to the required parameter in the schema definition. That resolves this problem.
See also the conversation at mongoose: https://github.com/Automattic/mongoose/issues/941
For whatever reason, the Mongoose designers decided that custom validations should not be considered if the value for a field is null, making conditional required validations inconvenient. The easiest way I found to get around this was to use a highly unique default value that I consider to be "like null".
var LIKE_NULL = '13d2aeca-54e8-4d37-9127-6459331ed76d';
var conditionalRequire = {
validator: function (value) {
return this.type === 'other' && val === LIKE_NULL;
},
msg: 'Some message',
};
var Model = mongoose.Schema({
type: { type: String },
someField: { type: String, default: LIKE_NULL, validate: conditionalRequire },
});
// Under no condition should the "like null" value actually get persisted
Model.pre("save", function (next) {
if (this.someField == LIKE_NULL) this.someField = null;
next()
});
A complete hack, but it has worked for me so far.
Try adding this validation to the type attribute, then adjust your validation accordingly. E.g.:
function validator(val) {
val === 'other' && this.thing === '';
}
thing: {
type: String,
required: function()[{
return this.type === 'other';
}, 'YOUR CUSTOM ERROR MSG HERE']
}

Resources