Knockout Validation on Nested View Model - javascript

I am looking to use a combination of the KnockoutJS libarary, the Knockout.Mapping plugin, and the Knockout-Validation plugin to display some data that the user can manipulate.
My data is coming over as a nested object from an AJAX call, and I run that data through the mapping plugin to create a Knockout view model, customizing the validation rules with the mapping options object in ko.mapping.fromJS.
I have been successful in getting objects at the first layer (name in the Fiddle below) to show a message if the field is empty, however objects that are nested (IntroData.PlanName) do not show the validation message. Do I need to setup the mapping object differently for these nested objects?
ViewModel (sample of what is coming in my AJAX call):
var stuff = {
IntroData: {
PlanName: 'Test'
},
name: 'tes2s3t'
};
Mapping:
var validationMapping = {
IntroData: {
PlanName: {
create: function (options) {
return ko.observable(options.data).extend({
required: true
});
}
}
},
name: {
create: function (options) {
return ko.observable(options.data).extend({
required: true
});
}
}
};
Hookup:
ko.validation.init({
registerExtenders: true,
messagesOnModified: true,
insertMessages: true,
parseInputAttributes: true,
messageTemplate: null,
grouping: {
deep: true
}
}, true);
window.viewModel = ko.validatedObservable(ko.mapping.fromJS(stuff, validationMapping));
ko.applyBindings(window.viewModel);
Fiddle: http://jsfiddle.net/odxv53g9/5/
Thanks!

The documentation is not clear on this, but apparently ko.mapping.fromJS() ignores nested mappings, so the "create" method for PlanName never gets called.
You could add an explicit mapping for IntroData instead:
IntroData: {
create: function (options) {
var nestedMapping = {
PlanName: {
create: function (options) {
return ko.observable(options.data).extend({
required: true
});
}
}
}
return ko.mapping.fromJS(options.data, nestedMapping);
}
}
Here is a working fiddle: http://jsfiddle.net/odxv53g9/6/

Related

Vue Watchers for multiple button clicks

I have multiple filters that are set to false and if clicked turn to true and must implement the specific filter. How can I use a watchers to look for the changing of my filter from false to true. Currently as I have all my functions in one watcher if one filter is clicked all filters are implemented. Whereas I need it to be only the specific filter that is related to the filter button.
<sankey-filter
label="string1"
v-model="filters.string1"
/>
<sankey-filter
label="string2"
v-model="filters.string2"
/>
<sankey-filter
label="string3"
v-model="filters.string3"
/>
data() {
return {
filters: {
statusString1: false,
statusString2: false,
statusString3: false,
}
watch: {
filters: {
handler: function(){
this.filterString1(),
this.filterString2(),
this.filterString2(),
},
deep: true
Vue $watch provide callback function two parameters (newValue, oldValue) so U can check new value each filter if true for each filter action
watch: {
filters: {
handler: function({statusString1, statusString2, statusString3}){
if (statusString1)
this.filterString1()
if (statusString2)
this.filterString2()
if (statusString3)
this.filterString3()
},
deep: true
}
}
Use a dot-delimited path as the watcher key.
watch: {
'filters.string1' () {
this.filterString1(),
},
'filters.string2' () {
this.filterString2(),
},
'filters.string3' () {
this.filterString3(),
},
}

Unable to submit custom field, which extends dataview

I have a fiddle which demonstrates this strange behaviour. In a nutshell, I have a custom field which extends Ext.DataView. I need to extend dataview, because this field is supposed to have dynamic contents. This is how my field is defined:
Ext.define('Ext.ux.SimpleList', {
extend: 'Ext.DataView',
alias: 'widget.simplelist',
requires: [
'Ext.XTemplate'
],
itemSelector: 'div.record',
isFormField: true,
getFieldIdentifier: function () {
return this.name;
},
getModelData: function() {
var me = this;
var data = {};
data[me.getFieldIdentifier()] = me.getValue();
return data;
},
getSubmitData: function() { return {}; },
submitValue: true,
isValid: function () { return true; },
isDirty: function () { return true; },
validate: function () { return true; },
isFileUpload: function() { return false; },
constructor: function(config) {
Ext.applyIf(config, {
tpl: [
'{% var name = this.owner.name; %}',
'<tpl for=".">',
'<div class="record"><input record-id="{id}" type="checkbox" name="{[name]}" /> {label}</div>',
'</tpl>',
{compiled: true}
]
});
this.callParent([config]);
},
getValue: function () {
var cb = this.el.select("input").elements, i, res = [];
for (i in cb) {
if (cb.hasOwnProperty(i) && cb[i].checked) {
res.push(cb[i].getAttribute("record-id"));
}
}
return res;
},
setValue: function (values) {
//not yet implemented
}
});
And this is how I add this field to a form:
Ext.create("Ext.form.Panel",{
renderTo: Ext.getBody(),
items:[{
xtype: "textfield",
name: "text"
},{
xtype: "simplelist",
name: "list",
store: {
fields: ["id", "label", "checked"],
data: [{"id": "1", "label": "One"},{"id": "2", "label": "Two"}]
}
},{
xtype: "button",
text: "Submit",
handler: function () {
var frm = this.up("form").getForm();
console.log(frm.getFieldValues()); // it' ok
//simplelist field is not submitted
this.up("form").getForm().submit({
url: "/"
});
}
}]
});
As you can see, when I submit the form I log to the console form field values. And what is interesting about that, is that I see my custom field among those field values. So, I have a field with isFormField set to true, this field is in the list returned by form getFields() method and this field is also among those values returned by form getFieldValues() method, but still this field is not submitted. What is wrong with that and how can I fix it?
Your code uses basicForm.getFieldValues(), which calls basicForm.getValues() with some parameters, while the form while submitting uses the same method with different parameters. One of those parameters is useDataValues, which decides whether to use the getModelData or getSubmitData.
You are returning empty object in your getSubmitData method, which prevents it to correctly get the values.
All you need to change, for both methods to work in your current state, is this:
getSubmitData: function() {
return this.getModelData();
}

Bind knockoutjs to javascript object property

I'm new to Knockoutjs, so please bear with me.
I want to knocoutjs bind a DxForm (DevExpress) to an javascript object property, but I get an error ... "Cannot read property 'items' of undefined".
I am uncertain if this is a knockout problem, DevExpress problem or just incufficient coding skills from my part.
Here's my code...
HTML:
<div data-bind="dxForm: frm.options"></div>
Javascript:
var viewModel = function() {
var that = this;
// -----------------------------------------------------------------
// Faste...
// -----------------------------------------------------------------
that.frm = {
items: ko.observable(undefined),
data: ko.observable(undefined),
instance: ko.observable({}),
options: {
items: that.frm.items,
formData: that.frm.data,
onInitialized: function(e) {
that.frm.instance(e.component);
},
},
};
return {
frm: that.frm,
};
};
var vm = new viewModel();
ko.applyBindings(vm);
var items = [{
"dataField": "test",
"editorOptions": {
"type": "date",
"pickerType": "calendar",
},
"editorType": "dxDateBox",
"name": "Test",
"visible": true
}];
var data = {
test: 10,
};
vm.frm.data(data);
vm.frm.items(items);
JSFiddle: https://jsfiddle.net/MojoDK/ke395v2c/3/
I want to bind to objects since I'm going to use several DxForm objects and I like to keep the code to each DxForm in an object (easier to read).
Any idea why it fails?
Thanks.
You just have a problem with closure in your frm.
The that property in frm object do not exist you should use this...
But in your onInitialized function, this and that will not target your viewModel object...
So this way, the easiest is to declare options object later :
that.frm = {
items: ko.observable(undefined),
data: ko.observable(undefined),
instance: ko.observable({})
};
that.frm.options = {
items: that.frm.items,
formData: that.frm.data,
onInitialized: function(e) {
that.frm.instance(e.component);
},
};
Updated jsfiddle

KnockoutJS Validation template and multi models

I have two pages, the first is a login page, simple :
Model KnockoutJs
function Login(){
var self=this;
self.email = ko.observable().extend({ email: true, required: true });
self.password = ko.observable().extend({ required: true});
}
Model Binding
$(function () {
ko.validation.configure({
insertMessages: true,
decorateElement: true,
errorElementClass: 'validation',
messageTemplate: "ValidationTemplate",
errorsAsTitle: false
});
var login = new Login();
ko.applyBindings(login);
});
Template Definition
<script type="text/html" id="ValidationTemplate">
<span data-bind="attr: { error: field.error },
visible: field.isModified() && !field.isValid(),
event: { mouseover: layout.errorTooltip }"
class="glyphicon glyphicon-exclamation-sign f-validation-message"></span>
</script>
Everything works fine, the little icon appears over the input which gets red borders.
Then the other page, with models hierarchy :
Model KnockoutJs Parent
function Parent()
{
var self=this;
self.child= new Child();
}
Model KnockoutJs Child
function Child()
{
var self=this;
self.val1= ko.observable().extend({ required: true });
self.val2= ko.observable().extend({ required: true });
}
Model Parent Binding
$(function () {
ko.validation.configure({
insertMessages: true,
decorateElement: true,
errorElementClass: 'validation',
messageTemplate: "ValidationTemplate",
errorsAsTitle: false
});
var parent= new Parent();
ko.applyBindings(parent);
});
The inputs are in this case included in a with block
<div data-bind="with:$root.child">
...
</div>
The validation template is the same.
So, the icon does not appear but the borders, yes.
When I check the code, KnouckoutJs did not "spread" the template on each input.
The only difference is the multi models system, but not sure how it impacts the binding?
Thank you for your help.
Yoann
Ok, I found the problem, it was not linked at all with the multi model or validation template.
I was binding data like follow :
//data: JS object
self.obsProp(ko.mapping.fromJS(data));
self.obsProp().value1.extend({required:true});
self.obsProp().value2.extend({required:true});
WRONG Way, the correct way to map the data with validation :
var validationMapping = {
value1: {
create: function(options) {
return ko.observable(options.data).extend( {required: true} );
}
},
value2: {
create: function(options) {
return ko.observable(options.data).extend( {required: true} );
}
}
};
self.obsProp(ko.mapping.fromJS(data,validationMapping));
And everything works fine.
Thanks :)

Backbone Collection Can't Remove Items

I've got a Backbone Model called Delivery. I then create a collection of Deliveries called DeliveryList backed by LocalStorage. In my Marionette.ItemView for displaying items in the collection, I have a method to remove items:
removeDeliveryOption: function() {
Deliveries.remove(this.model.get("id"));
}
For some reason, this removes the item from the Marionette.CompositeView when I click the remove button, but when I reload the page the same number of items always reappear.
It's worth noting that when I delete the item, it always reappears with the default optionName "Free Delivery". I'm using both defaults and a schema in the model because I'm using the Backbone-forms plugin (https://github.com/powmedia/backbone-forms).
Any help is greatly appreciated!
var Delivery = Backbone.Model.extend({
defaults: function () {
return {
order: Deliveries.nextOrder(),
optionName: "Free Delivery",
shipToState: "Hawaii",
zipCodes: "96813",
perOrderFee: "0.00",
perItemFee: "0.00"
};
},
schema: {
optionName: { type: 'Text', validators: ['required'] },
shipToState: { type: 'Select', options: getStateNames(), validators: ['required'] },
zipCodes: { type: 'Text', validators: ['required'] },
perOrderFee: { type: 'Text', validators: ['required'] },
perItemFee: { type: 'Text', validators: ['required'] },
}
});
var DeliveryList = Backbone.Collection.extend({
model: Delivery,
localStorage: new Backbone.LocalStorage("deliverylist-backbone"),
nextOrder: function () {
if (!this.length) return 1;
return this.last().get('order') + 1;
},
comparator: 'order'
});
var Deliveries = new DeliveryList;
var deliveryView = Marionette.ItemView.extend({
//tagName: "li",
template: "#delivery-item-template",
events: {
"click #removeThis": "removeDeliveryOption",
},
removeDeliveryOption: function() {
Deliveries.remove(this.model.get("id"));
}
});
var DeliveriesView = Marionette.CompositeView.extend({
initialize: function() {
Deliveries.fetch();
},
template: '#deliveries-view-template',
itemView: deliveryView,
events: {
"click #addShipping": "addDeliveryOption",
},
addDeliveryOption: function() {
var editDeliveryForm = new Backbone.Form({
template: _.template($("#editDeliveryTemplate").html()),
model: Deliveries.create()
}).render();
this.$el.append(editDeliveryForm.el);
$("#triggerEditDelivery").fancybox({
'afterClose': function () {
commitForm(editDeliveryForm);
//Wait do display the inlineModel until here
// Once we've bound the form to the model, put the saving logic with the collection
//Deliveries.last().save();
}
}).trigger('click');
},
// Specify a jQuery selector to put the itemView instances in to
itemViewContainer: "#deliveries",
});
EDIT
Thanks to #ejosafat! Had to destroy the model instead of just removing from collection.
removeDeliveryOption: function() {
this.model.destroy();
}
The remove method only affects the collection loaded in the browser, not in the permanent storage (local or server). That's why it dissappears from the view but when you reload the page it appears again.
If you want to get rid of that model in the storage too, use its destroy method.
(btw, it's a common convention in Javascript to use initial capital letter only for constructor functions, as clue that it should be used with the new operator, or be extended to create a derived constructor/class, so it's a bad idea to use Deliveries as a collection var name)

Categories

Resources