knockout validating multiple levels deep observable array - javascript

Hi I need to create a custom validator that will be aplyed for each element of an observable array using knockout validation plugin. The structure of my object will look something like this when I post it to the server:
var viewModel = {
evaluationFormDataContract: {
studentAssignmentInstanceId: value,
evaluationType: value,
categories: array[
CategoriesOnEvaluationDataContract1 = {
memo: value,
categoryId: value,
title: value,
// Fields needed for validation
hasMemo: value,
memoIsMandatory: value
questions: array[
QuestionsOnEvalCategoryDataContract1 = {
memo: value,
grade: value,
hasGrade: value,
hasMemo: value,
showOnlyMemo: value
},
QuestionsOnEvalCategoryDataContract2 = {
memo: value,
grade: value,
hasGrade: value,
hasMemo: value,
showOnlyMemo: value
}]
},
CategoriesOnEvaluationDataContract2 = {
memo: value,
categoryId: value,
title: value,
// Fields needed for validation
hasMemo: value,
memoIsMandatory: value
questions: array[
QuestionsOnEvalCategoryDataContract1 = {
memo: value,
grade: value,
hasGrade: value,
hasMemo: value,
showOnlyMemo: value
},
QuestionsOnEvalCategoryDataContract2 = {
memo: value,
grade: value,
hasGrade: value,
hasMemo: value,
showOnlyMemo: value
},
QuestionsOnEvalCategoryDataContract3 = {
memo: value,
grade: value,
hasGrade: value,
hasMemo: value,
showOnlyMemo: value
}]
}, ]
}
}
Now the validation will have to be applyed only on the two nested arrays and will be done based on some properties.
The first validation has to be done on each object of the categories array and it will check if hasMemo and memoIsMandatory if this is the case memo will be required.
The second validation will be done on each object of questions array and it will check if hasGrade if that is the case grade will be required.
The last validation will be done on hasMemo and showOnlyMemo and are will be used for the memo value on the questions object.
Reading the documentation for the validation plugin I found how I would extend a simple observable .Witch it seems to be done something like this:
ko.validation.rules['mustEqual'] = {
validator: function (val, otherVal) {
return val === otherVal;
},
message: 'The field must equal {0}'
};
But I do not think this will work for the structure of my viwmodel.How can I create validators for each observable in my observableArrays?

First off, I agree with Tomalak. Rather than posting a bunch of nonsense that your code "looks something like", you should post some actual code that is readable. For instance, I can't tell if you are using any observable, computed or observableArray members, so I just have to assume that everything is observable or observableArray and there are no computed members.
Now, you said:
The first validation has to be done on each object of the categories array and it will check if hasMemo and memoIsMandatory if this is the case memo will be required.
Let me just say that naming a property hasMemo and that mean that the memo field is required is TERRIBLE! If you call something hasMemo, it should mean that the thing in question has a memo. And why would you need to look at both hasMemo and memoIsMandatory to see if memo is required? Ditto for hasGrade.
Regardless, what you need is just to add validation to each of the observables on your classes. Wait, that's another assumption. You are using classes, right? You're not just creating a single object and giving it a bunch of nested arrays and objects without using constructors, are you? Well I'll proceed assuming that you're creating constructors and leave it at that.
I'll just focus on your first validation because the second one is just like it and the third one is unintelligible to me. So let's say your "CategoriesOnEvaluationDataContract1" object uses the following constructor:
function Category() {
var self = this;
self.categoryId = ko.observable();
self.hasMemo = ko.observable();
self.memoIsMandatory = ko.observable();
self.memo = ko.observable();
//etc etc...
}
You need to extend memo with a validator, in this case you want the required validator. That would look like this:
self.memo = ko.observable().extend({ required: true });
This makes it so that memo is always required. But that is not what you want, you want it to be required when hasMemo and memoIsMandatory are both true, right?. So this is what you need to do:
self.memo = ko.observable().extend({ required: { onlyIf: function() {
return self.hasMemo() && self.memoIsMandatory();
} } });
There. That's all there is to it. You should be able to figure out the rest. And if not, just let me know. :)

Related

Prisma/React Query Dependent undefined type challenges

I would like to take the output of one query (a TRPC query on Prisma) and use this as the dependent input in a future query.
I followed the dependent documentation for React Query but running into type errors that the return of the first may possibly be undefined (e.g. product is possibly 'undefined'):
const { data: product } = api.product.getUnique.useQuery({ id: pid });
const options = api.option.getAll.useQuery(
{
product: product.productSize,
region: product.productRegion,
},
{ enabled: !!product }
);
Does the inclusion of enabled not already handle this? If not, what is the correct way to adapt for Typescript.
Just casting the product value as a boolean return any truthy value (f.e if product will be equal to {} it will still result in true, that means that product won't necessarily have the productSize or productRegion properties, I would change it first to:
{ enabled: !!product && product.productSize && product.productRegion }
If that doesn't fix the typescript error, you as a developer can know for sure that the values are actually there so what you can use the as keyword in typescript to tell it that you know for sure that the type is what you want it to be:
(In this example I assumed that the values are string but you can change it to number or whatever the true value of them are)
const options = api.option.getAll.useQuery(
{
product: product.productSize as string,
region: product.productRegion as string,
},
{ enabled: !!product && product.productSize && product.productRegion }
);

Ajv library: Accessing keys in the nested schema validation javascript

I'm working on the use case where I need to validate the schema using ajv lib of the provided data which can be nested.
Now the problem is, Schema could change based on the value of a particular variable, which is not in the scope where this check is to be done.
how do I achieve it through ajv.
I tried using if-else & data & const but no luck.
I've recently encountered similar use case with ajv validator.
By looking at your issue I believe you need to access some other scope of the actual object passed for validation, In my case it was lying in the root of the object.
So I used the validate function in ajv's User Defined Keyword section, which in turn is giving me the whole object from top level in the first index of arguments itself, that way I accessed my dependent key in validate function and used it in my ajv IF: condition, eg
ajv.addKeyword({
keyword: "isRegular",
validate: (...test) => {
const test1 = test[1]
return test1.dependentKeyFromRootOfObject === "REGULAR"
},
})
and used the then created keyword isRegular in the nested object's IF: condition like
if: { isRegular: true },
then: {
properties: {
rcType: { type: "string" },
date: { type: "string" },
},
required: ["rcType", "date"],
additionalProperties: false,
},
else: {
properties: {
rcType: { type: "string" },
},
required: ["rcType"],
additionalProperties: false,
},
Hope this helps.

How to prevent alpaca from generating radio field when the search select has 3 or less options

I have a div and a following javascript:
let usersNotContributingIds = [ 19, 20, 21 ];
let usersNotContributingNames = [ "Flavius K.", "Pogchamp", "Lazy Gopnik" ];
let contributorToBeAddedId; // the value that will be used for further actions
$("#alpaca-search-contributing-users").alpaca({
data: null,
schema: {
type: "object",
enum: usersNotContributingIds,
},
options: {
name: "pls",
label: 'Contributor Fullname',
optionLabels: usersNotContributingNames,
helper: "Select user sou want to add as a contributor",
id: "select2-search",
focus: false,
events: {
change: function() {
console.log(this.getValue().value);
contributorToBeAddedId = this.getValue().value
},
focus: function() {
console.log(this.name);
},
blur: function() {
console.log(this.name + ": blur");
},
ready: function() {
console.log(this.name);
}
},
},
postRender: function(control) {
$('#select2-search').select2();
}
});
Obviously, I want to get the newly set value, or anyhow access the selected value and use it. For example with AJAX and a button.
The problem is, that when I have 3 or less options, Alpaca render the field not as a search, but as a radio-something and the this.getValue() is null.
Is there a way to force Alpaca to NOT USE THE RADIO BUTTONS? I dont want to use them, even if I had only 1 option. Documentation just promtly states, that if there are 3 or less options, it will generate radio buttons instead of select, but it says nothing about the fact, that it breaks everything and that I would not be able to retrieve the value the same way as with select field.
If I am doing something inefficiently or wrong, please tell me, I am new with Alpaca and I just want a neat select dropdown with search, that I can use to pick users from a list with any length. Also, I would like the "null" or "none" option to not be there.
To have your select component rendered you should use the option type and set it to "select".
The issue with the value is because you're using it wrong, to get the value in alpaca you only do this.getValue() and there's no need to add .value.
FYI: If you see the error "This field should have one of the values in Flavius K., Lazy Gopnik, Pogchamp. Current value is: 19" you should update your enum array to have strings instead of ints let usersNotContributingIds = [ "19", "20", "21" ];.
Here's a working fiddle for this.

Knockout JS not setting all members observable

What I am trying to do is to get data from the server and then putting it all in an observable and then make all the properties observable. The issue I am facing is that it does not make all my properties observable and I need them all to be observable as sometimes depending on the data it makes some properties observable and sometimes it doesn't.
var viewModel = this;
viewModel.Model = ko.observable();
viewModel.SetModel = function (data) {
viewModel.Model(ko.mapping.fromJS(data));
}
The data that I am receiving from the server is like this for example: normaldata,items(this is an array with unknown number of elements).
so if i try to access data like viewModel.Model().Items[0]().Layer() i sometimes have Layer as a function and sometimes it is a normal element with observable elements.I want all my objects inside Items to have Layer as a function.
Server data example:
Name: "test"
Items: [Layer[ID: 132]]
In this example Name,Items and ID are observable but Layer is not.
Fiddle example:
jsfiddle.net/98dv11yz/3
So the problem is that sometimes the layer is null resulting in ko making the property observable but sometimes that property has id and ko makes only the child elements observable. The problem is that i have if's in the code and i want it to be a function so i can always reffer to it as layer() because now it is sometimes layer or layer()
An explenation for what's happening:
When the ko.mapping plugin encounters an object in your input, it will make the object's properties observable, not the property itself.
For example:
var myVM = ko.mapping.fromJS({
name: "Foo",
myObject: {
bar: "Baz"
}
});
Will boil down to:
var myVM = {
name: ko.observable("Foo"),
myObject: {
bar: ko.observable("Baz")
}
}
and not to:
var myVM = {
name: ko.observable("Foo"),
myObject: ko.observable({
bar: ko.observable("Baz")
})
}
The issue with your data structure is that myObject will sometimes be null, and sometimes be an object. The first will be treated just as the name property in this example, the latter will be treated as the myObject prop.
My suggestion:
Firstly: I'd suggest to only use the ko.mapping.fromJS method if you have a well documented and uniform data structure, and not on large data sets that have many levels and complexity. Sometimes, it's easier to create slim viewmodels that have their own mapping logic in their constructor.
If you do not wish to alter your data structure and want to keep using ko.mapping, this part will have to be changed client-side:
Items: [
{ layer: {id: "0.2"} },
{ layer: null}
]
You'll have to decide what you want to achieve. Should the viewmodel strip out the item with a null layer? Or do you want to render it and be able to update it? Here's an example of how to "correct" your data before creating a view model:
var serverData = {
Name: "Example Name",
Id: "0",
Items: [
{layer: {id: "0.2"} },
{layer: null}
]
};
var correctedData = (function() {
var copy = JSON.parse(JSON.stringify(serverData));
// If you want to be able to render the null item:
copy.Items = copy.Items.map(function(item) {
return item.layer ? item : { layer: { id: "unknown" } };
});
// If you don't want it in there:
copy.Items = copy.Items.filter(function(item) {
return item.layer;
});
return copy;
}());
Whether this solution is acceptable kind of relies on how much more complicated your real-life use will be. If there's more complexity and interactivity to the data, I'd suggest mapping the items to their own viewmodels that deal with missing properties and what not...

Meteor Autoform - disable select options in an array of Object fields

I'm wanting to disable an option if it has already been selected in one of the object groups.
So, if I selected "2013" then added another sample, "2013" would not be available in that group, unless that option is changed in the original group.
Is there an easy way to do this that I'm missing? Do I need to reactively update the schema when a selection is made?
samples:{
type: Array,
optional: true,
maxCount: 5
},
"samples.$":{
type: Object,
optional: true
},
"samples.$.sample":{
type:[String],
autoform: {
type: "select",
options: function () {
return [
{
optgroup: "Group",
options: [
{label: "2013", value: 2013},
{label: "2014", value: 2014},
{label: "2015", value: 2015}
]
}
];
}
}
},
Proof of Concept
I know this post is about 3 years old. However, I came across the same issue and want to provide an answer for all those who also stumbled over this post.
This answer is only a proof of concept and does not provide a full generic and performant solution, that could be used on production apps.
A fully generic solution would require a deep change in the code of how select field options are generated and updated in AutoForm.
Some prior notes.
I am using Autoform >=6 which provides a good API to instantly obtain field and form values in your SimpleSchema without greater trouble. SimpleSchema is included as npm package and Tracker has to be passed to it in order to ensure Meteor reactivity.
Functions like AutoForm.getFieldValue are reactive, which is a real great improvement. However, reactively changing the select options based on a reactive value causes a lot of update cycles and slows the performance (as we will see later).
Using AutoForm.getFormValues is not working, when using it within options of an Object field. While working within Array field, it will not behave reactively in Object fields, thus not update the filtering on them.
Manipulating Options for Arrays of Select Inputs (failing)
You can't use it with array types of fields. It's because if you change the select options, it applies for all your select instances in the array. It will therefore also apply to your already selected values and strips them away, too. This makes your select looks like it is always "not selected"
You can test that yourself with the following example code:
new SimpleSchema({
samples:{
type: Array,
optional: true,
maxCount: 5
},
"samples.$":{
type: String,
autoform: {
type: "select",
options: function () {
const values = AutoForm.getFormValues('sampleSchemaForm') || {};
const samples = values && values.insertDoc && values.insertDoc.samples
? values.insertDoc.samples
: [];
const mappedSamples = samples.map(x => x.sample);
const filteredOpts = [
{label: "2013", value: "2013"},
{label: "2014", value: "2014"},
{label: "2015", value: "2015"}
].filter(y => mappedSamples.indexOf(y.value) === -1);
return [
{
optgroup: "Group",
options:filteredOpts,
}
];
}
}
},
}, {tracker: Tracker});
Using fixed values on an Object Field
when taking a closer look at the schema, I saw the maxCount property. This made me think, that if you anyway have a list of max options, you could solve this by having fixed properties on a samples object (by the way: maxCount: 5 makes no sense, when there are only three select options).
This causes each select to have it's own update, that does not interfere the others. It requires an external function, that keeps track of all selected values but that came out be very easy.
Consider the following code:
export const SampleSchema = new SimpleSchema({
samples:{
type: Object,
optional: true,
},
"samples.a":{
type: String,
optional:true,
autoform: {
type: "select",
options: function () {
const samples = AutoForm.getFieldValue("samples");
return getOptions(samples, 'a');
}
}
},
"samples.b":{
type: String,
optional:true,
autoform: {
type: "select",
options: function () {
const samples = AutoForm.getFieldValue("samples");
return getOptions(samples, 'b');
}
}
},
"samples.c":{
type: String,
optional:true,
autoform: {
type: "select",
options: function () {
const samples = AutoForm.getFieldValue("samples");
return getOptions(samples, 'c');
}
}
},
}, {tracker: Tracker});
The code above has three sample entries (a, b and c) which will let their options be computed by an external function.
This function needs to fulfill certain requirements:
filter no options if nothin is selected
filter not the option, that is selected by the current samples select
filter all other options, if they are selected by another select
The code for this function is the following:
function getOptions(samples={}, prop) {
// get keys of selections to
// determine, for which one
// we will filter options
const sampleKeys = Object.keys(samples);
// get sample values to
// determine which values
// to filter here
const sampleValues = Object.values(samples);
const filteredOptiond = [
// note that values are stored as strings anyway
// so instead of parsing let's make them strings
{label: "2013", value: "2013"},
{label: "2014", value: "2014"},
{label: "2015", value: "2015"}
].filter(option => {
// case 1: nothing is selected yet
if (sampleKeys.length === 0) return true;
// case2: this selection has a
// selected option and current option
// is the selected -> keep this option
if (sampleKeys.indexOf(prop) > -1 && option.value === samples[prop])
return true;
// case 3: this selection has no value
// but others may have selected this option
return sampleValues.indexOf(option.value) === -1;
});
return [
{
optgroup: "Group",
options: filteredOptiond,
}
]
};
Some Notes on this Concept
Good:
-it works
-you can basically extend and scale it to your desired complexity (optgroups, more fields on samples, checking against other fields with other fields etc.)
Bad:
- performance
- bound to a given (or the nearest) form context (see here)
- much more code to write, than for an array.

Categories