How do I extend properties from a base 'class' in knockout.js?
I'm trying to setup knockout-validation in my view models but it doesn't work unless my 'sub-class' has an observable that is also extended. See the comments in the code snippet below:
// setup knockout validation
ko.validation.rules.pattern.message = 'Invalid.';
ko.validation.configure({
registerExtenders: true,
messagesOnModified: true,
insertMessages: true
});
// base 'class'
function userAddressModelBase(data) {
var self = this;
self.firstName = ko.observable(data.firstName);
self.lastName = ko.observable(data.lastName);
}
userAddressModel.prototype = new userAddressModelBase({});
userAddressModel.constructor = userAddressModel;
// 'sub-class'
function userAddressModel(data) {
var self = this;
userAddressModelBase.call(self, data);
// extend 'base-class' properties; this doesn't work unless the current 'sub-class' also has an observable who is also extended...
self.firstName.extend({ required: true });
self.lastName.extend({ required: true });
// the above statement only starts working if I extend an observable from this `sub-class`
self.city = ko.observable();//.extend({ required: true });
self.errors = ko.validation.group(self);
self.validate = function() {
console.log(self.errors().length);
if (self.errors().length > 0) {
self.errors.showAllMessages();
return;
}
};
}
var model = new userAddressModel({ firstName: "Ted" });
ko.applyBindings(model);
.validationMessage{
color: #ea033d;
font-weight: 700;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div>
<label>First Name: <input type="text" data-bind="value: firstName" /></label><br>
<label>Last Name: <input type="text" data-bind="value: lastName" /></label><br>
<label>City: <input type="text" data-bind="value: city" /></label><br>
<button data-bind="click: validate">Validate</button>
</div>
The recommended way to perform inheritance is to use ko.utils.extend(self, new userAddressModelBase(data));
function userAddressModelBase(data) {
var self = this;
self.firstName = ko.observable(data.firstName);
self.lastName = ko.observable(data.lastName);
}
Below I am using extend to add required validation to firstName and lastName.
function userAddressModel(data) {
var self = this;
ko.utils.extend(self, new userAddressModelBase(data))
self.firstName.extend({ required: true });
self.lastName.extend({ required: true });
};
Now in the example if you remove the firstName and then exit the text box you can see that the required message appears.
http://jsfiddle.net/d4vLqkx2/1/
Related
I'm trying to use knockout.validation on a number of parts of a form. Most of this all works fine and can use the built in message appender, but I've got 1 specific field where I need to add the message myself manually to the DOM.
I'm having a real problem getting it working though, because I simply can't get the text to display if there hasn't been a change to the field. I've mocked up a really small example to illustrate as close to the larger form as I can.
The behaviour I'm seeing:
Clicking submit correctly runs the validation (adds a red border around the input) but it doesn't display the error message like it should do.
Typing something into the password box, then emptying it out, then hitting submit works correctly displaying both error message and the red border.
Due to the fact I'm trying to introduce a required field - I can't guarantee that the field has ever been modified, so I need to ensure the first case works. Can anyone see what I'm doing wrong?
ko.validation.init({
registerExtenders: true,
messagesOnModified: true,
insertMessages: true,
decorateInputElement: true,
decorateElementOnModified: false,
errorElementClass: "is-invalid",
messageTemplate: "inline_error"
}, true);
const viewModel = {};
viewModel.submitted = ko.observable(false);
viewModel.clicked = () => { viewModel.submitted(true); };
const onlyIf = ko.computed(() => viewModel.submitted());
viewModel.user = {
password: ko.observable().extend({
required: {
message: `password is required.`,
onlyIf,
}
})
};
ko.applyBindings(viewModel);
.is-invalid {
border: thick solid red;
}
.text-danger {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js"></script>
<div id="domTest">
<button type="button" data-bind="click: clicked">Submit</button>
<input type="password" class="form-control" id="password" placeholder="Password" data-bind="validationOptions:{ insertMessages: false }, textInput: user.password">
<div class="text-danger error-message" data-bind="validationMessage: user.password" />
</div>
In this Github issue, validationMessage does not run on initial binding, stevegreatrex say that you need to change messagesOnModified to false:
messagesOnModified: false
And from the answer to this question Knockout Validation onlyif object no function and pattern validation
(by steve-greatrex) you need to change the onlyIf computed:
viewModel.onlyIf = ko.computed(() => viewModel.submitted());
And then change the property onlyIf in viewModel.user to point to the computed viewModel.onlyIf:
viewModel.user = {
password: ko.observable().extend({
required: {
message: `password is required.`,
onlyIf: viewModel.onlyIf(),
}
})
};
Hope this helps.
ko.validation.init({
registerExtenders: true,
//messagesOnModified: true,
messagesOnModified: false,
insertMessages: true,
decorateInputElement: true,
decorateElementOnModified: false,
errorElementClass: "is-invalid",
messageTemplate: "inline_error"
}, true);
const viewModel = {};
viewModel.submitted = ko.observable(false);
viewModel.clicked = () => { viewModel.submitted(true); };
viewModel.onlyIf = ko.computed(() => viewModel.submitted());
viewModel.user = {
password: ko.observable().extend({
required: {
message: `password is required.`,
onlyIf: viewModel.onlyIf(),
}
})
};
ko.applyBindings(viewModel);
.is-invalid {
border: thick solid red;
}
.text-danger {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js"></script>
<div id="domTest">
<button type="button" data-bind="click: clicked">Submit</button>
<input type="password" class="form-control" id="password" placeholder="Password" data-bind="validationOptions:{ insertMessages: false }, textInput: user.password">
<div class="text-danger error-message" data-bind="validationMessage: user.password" />
</div>
I am trying to figure out how it is possible to pass an array as the value for the property of an instance. I currently have the dataType set to STRING in my model and have values from jQuery fields insert each form field value into an array that I parse from the body and set to the property, discoverSource. Unfortunately I receive a string violation error that says I can't use an array or object. What does this mean and how can I change the dataType of the field or route to allow me to pass the comma separated values to the field?
E.x. For discoverySource I pass values to two fields (NJ, NY). On submit, the values are combined in an array as ["NJ", "NY"] and the error displays:
Error Message:
{"name":"SequelizeValidationError","message":"string violation: discoverySource cannot be an array or an object","errors":[{"message":"discoverySource cannot be an array or an object","type":"string violation","path":"discoverySource","value":["NJ","NY"]}]}
Here is my model:
module.exports = function(sequelize, DataTypes) {
var Organization = sequelize.define('organization', {
organizationId: {
type: DataTypes.INTEGER,
field: 'organization_id',
autoIncrement: true,
primaryKey: true
},
organizationName: {
type: DataTypes.STRING,
field: 'organization_name'
},
admin: DataTypes.STRING,
discoverySource: {
type: DataTypes.TEXT,
field: 'discovery_source'
},
members: DataTypes.STRING
},{
freezeTableName: true,
classMethods: {
associate: function(db) {
Organization.belongsToMany(db.User, { through: 'member', foreignKey: 'user_id' });
},
},
});
return Organization;
}
Here is the route:
var express = require('express');
var appRoutes = express.Router();
var passport = require('passport');
var localStrategy = require('passport-local').Strategy;
var models = require('../models/db-index');
appRoutes.route('/sign-up/organization')
.get(function(req, res){
models.User.find({
where: {
user_id: req.user.email
}, attributes: [ 'user_id', 'email'
]
}).then(function(user){
res.render('pages/app/sign-up-organization.hbs',{
user: req.user
});
})
})
.post(function(req, res, user){
models.Organization.create({
organizationName: req.body.organizationName,
admin: req.body.admin,
discoverySource: req.body.discoverySource
}).then(function(organization, user){
res.redirect('/app');
}).catch(function(error){
res.send(error);
console.log('Error at Post' + error);
})
});
Here is my view file:
<!DOCTYPE html>
<head>
{{> head}}
</head>
<body>
{{> navigation}}
<div class="container">
<div class="col-md-6 col-md-offset-3">
<form action="/app/sign-up/organization" method="post">
<p>{{user.email}}</p>
<input type="hidden" name="admin" value="{{user.email}}">
<input type="hidden" name="organizationId">
<label for="sign-up-organization">Company/Organization Name</label>
<input type="text" class="form-control" id="sign-up-organization" name="organizationName" value="" placeholder="Company/Organization">
Add Another Discovery Source
<div id="sign-up-organization-discovery-source">
<input type="text" id="discovery-source-field" placeholder="Discovery Source" name="discoverySource[0]">
</div>
<br />
<button type="submit">Submit</button>
</form>
Already have an account? Login here!
</div>
</div>
<script type="text/javascript">
$(function() {
var dataSourceField = $('#sign-up-organization-discovery-source');
var i = $('#sign-up-organization-discovery-source p').size();
var sourceCounter = 1;
$('#sign-up-add-discovery-source').on('click', function() {
$('<p><label for="discovery-source-field"><input type="text" id="discovery-source-field" size="20" name="discoverySource['+ sourceCounter++ +']" value="" placeholder="Discovery Source" /></label> Remove</p>').appendTo(dataSourceField);
i++;
return false;
});
$('#sign-up-organization-discovery-source').on('click', '.remove', function() {
if (i > 1) {
$(this).parent('p').remove();
i--;
}
return false;
});
});
</script>
</body>
To answer the last comment, I need to be able to make the code more readable, so I'm posting it here in a new answer.
Having thought about it a little more, it would make more sense to add it as custom 'getter' function. I'll also include the 'instanceMethods' to demonstrate how that works, as well.
var Organization = sequelize.define('organization', {
...
},{
freezeTableName: true,
classMethods: {
associate: function(db) {
Organization.belongsToMany(db.User, { through: 'member', foreignKey: 'user_id' });
},
},
// Here's where custom getters would go
getterMethods: {
discoverySources: function() {
return this.getDataValue('discoverySource');
}
},
// here's the instance methods
instanceMethods: {
getSourcesArray: function() {
return this.getDataValue('discoverySource');
}
}
});
Both of these options add the functions to each instance created by the Model. The main difference being in how they are accessed.
organization.discoverySources; // -> ['s1', 's2', etc...]
organization.getSourcesArray(); // -> ['s1', 's2', etc...]
note the additional () required on the instanceMethod. Those are added as functions of the instance, the getterMethods get added as properties.
setterMethods work the same way to allow you to define custom setters.
Hope that clarifies things a bit.
I have a Model as
var Info = Backbone.Model.extend({
defaults: {
name: '',
email :''
},
initialize: function(){
console.log('Object created');
},
validate: function(attr){
if(!attr.name){
return 'Name cannot be empty';
},
if(!attr.email){
return 'Email cannot be empty';
}
}
});
var model = new Info();
model.set({
name: '',
email: ''
});
var viewClass = Backbone.View.extend({
_modelBind: undefined,
initialize: function(){
this._modelBind = new Backbone.ModelBinder();
this.render();
},
render: function(){
var template = _.template($('#App1').html());
this.$el.html(template);
var bindings = {
name: '[name=name]',
email: '[name=email]'
};
this._modelBind.bind(model, this.el, bindings);
},
'events': {
'click #btnSubmit': 'Submit'
},
Submit: function(){
var response = {
name: model.get('name'),
email: model.get('email')
};
this.model.save(response);
}
});
html is
<script type="text/template" id="App1">
Name: <input type="text" id="name" name="name"/><br />
Email: <input type="text" id="email" name="email" /><br />
<input type="button" id="btnSubmit" value="Submit" />
</script>
On save event, it goes to validate method and after that it goes to the value specified in the url attribute of the model.
I don't want to specify a url parameter. Can i still use validate method without url ?
Call validate directly:
var error = this.model.validate(this.model.attributes);
If you don't specify url parameter, you will get an error on this.model.save():
Uncaught Error: A "url" property or function must be specified
So I'm using Knockout 2.3 and Knockout Validation 2.0.2. I have a viewModel with a property that is an observableArray of custom javascript "objects" (HRAdmin). An HRAdmin has 3 properties that need validation. I've having a hard time with how to manage this type of situation. I've tried different things, but this is where the code sits at this point.
For the sake of brevity, I've stripped out all of the other properties of my viewModel that ARE validating just fine. But what this also tells me is none of my other code is interfering.
You can run and step through the code here and see that even when you leave all fields blank and click the "Go to Step 3" link, this line of code always results in the object being valid.
if (!obj[i].isValid())
It shouldn't be valid. Ideas/Suggestions??
// check validity of each object in array
ko.validation.rules['collectionValidator'] = {
validator: function(obj, params) {
for (var i = 0; i < obj.length; i++) {
if (!obj[i].isValid()) {
obj[i].notifySubscribers();
return false;
}
}
return true;
}
};
// validate a certain number of object exist in array
ko.validation.rules['minArrayLength'] = {
validator: function(obj, params) {
return obj.length >= params.minLength;
},
message: "Must have at least {0} {1}"
};
ko.validation.registerExtenders();
// HRAdmin "object"
function HRAdmin() {
this.FirstName = ko.observable("").extend({
required: true,
minLength: 1,
maxLength: 50
});
this.LastName = ko.observable("").extend({
required: true,
minLength: 1,
maxLength: 50
});
this.Email = ko.observable("").extend({
required: true,
minLength: 1,
maxLength: 100,
email: true
});
}
var viewModel = function() {
var self = this;
// Must be at least one HRAdmin and all fields of EACH HRAdmin must validate
self.HrAdmins = ko.observableArray([ko.validatedObservable(new HRAdmin())]).extend({
minArrayLength: {
params: {
minLength: 1,
objectType: "Account Manager"
},
message: 'Must specify at least one Account Manager'
},
collectionValidator: {
message: 'Please check the Account Manager information'
}
});
self.AddHrAdmin = function(data, event) {
self.HrAdmins.push(new ko.validatedObservable(new HRAdmin()))
};
self.GoToStep3 = function(data, event) {
// validate at least one HRAdmin and ALL fields on each are valid
if (!self.HrAdmins.isValid()) {
self.HrAdmins.notifySubscribers();
return;
}
// on to step 3 ...
};
};
ko.applyBindings(new viewModel());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.3.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.2/knockout.validation.min.js"></script>
<div data-bind="foreach: HrAdmins">
<input type="text" placeholder="First Name" data-bind="value: FirstName" />
<input type="text" placeholder="Last Name" data-bind="value: LastName" />
<input type="text" placeholder="Email Address" data-bind="value: Email" />
</div>
<p data-bind="validationMessage: HrAdmins" class="validationMessage"></p>
Add Manager
Go to Step 3
So after a lot of trail and error I've found a solution. Not sure if it's the BEST solution, but it works fine.
I've gotten rid of ko.validation.rules['collectionValidator'] and added a validator grouping.
self.HrAdminsErrors = ko.validation.group(self.HrAdmins, {deep: true, live: true});
The operational code is at the following fiddle:
http://jsfiddle.net/3b3o87dy/6/
I am using validation knockout for my project
And i used template for all error messages:
<script type="text/html" id="errorTemplate">
<div class="errorContainer error_bottom" data-bind="visible: field.isModified() && !field.isValid()">
<div class="error_arrow">
<div class="error_arrow_inner"></div>
</div>
<span class="error" data-bind="text: field.error"></span>
</div>
</script>
<script>
// KO validastor setup:
ko.validation.configure({
decorateElement: true,
errorElementClass: 'error', // Add class to input
registerExtenders: true,
messagesOnModified: true,
insertMessages: true,
parseInputAttributes: true,
messageTemplate: 'errorTemplate' // Use template
});
function ReservationViewModel() {
var self = this;
self.email = ko.observable("");
self.name = ko.observable("");
self.phone = ko.observable("");
var validator = ko.validatedObservable({
email: self.email.extend({required: true, email: true}),
name: self.name.extend({required: true}),
phone: self.phone.extend({required: true}),
});
// continues ...
}
ko.applyBindings(new ReservationViewModel());
</script>
But It alway get default message: "This field is required." for custom error or email is not valid.
Can u help me fix it?
Thanks all.