I've got this in my Simple Schema:
"servicesSelected.0.sku" : {
type: String,
optional: true
},
Basically, I want the sku key in the first array item of servicesSelected to be a String and optional.
Here's my form code, which is for a checkbox.
{{> afFieldInput class="track-order-change" type="checkbox" checkbox="true" template="" name="servicesSelected.0.sku" value="hdrPhotos"}}
The error I get is Invalid Field Name "servicesSelected.0.sku"
As soon as I remove the array index in both the schema and the afFieldInput the error goes away, but the point is to validate the data that is in array index 0...
I am going to assume that it's invalid because in JS you can't have a number as the first character in a key name if you are using dot notation.
But Simple Schema and Autoform do not support square bracket notation...
I'm not sure if SimpleSchema allows you to validate an array like this. A custom validation might be necessary.
I understand that the idea here is that the first element of the array can have the sku property, but others cannot. In this case, try the following method:
servicesSelected: {
type: [selectedServiceSchema],
custom: function() {
for(var i=1; i<this.value.length; ++i) {
if(this.value[i].sku) return "SKU set in the wrong service";
}
},
},
Related
In knockout, say we have something like this:
var person = {
name: "The H. Dude",
telecom: [
"mailto:dude#host.com",
"tel:+1-987-654-3210"
]
}
and I have a data binding elements like this:
<label>Name: <input type="text" data-bind="value: name"/></label>
<label>Phone:<input type="text" data-bind="value: telecom.find(url => url.startsWith('tel:'))"/></label>
<label>Email:<input type="text" data-bind="value: telecom.find(url => url.startsWith('mailto:'))"/></label>
This works alright. However, this would hit the user over the head with the URL scheme prefix.
So, what we would like to do is something like this:
data-bind="value: telecom.find(url => url.startsWith('tel:')).substring('tel:'.length)"
and
data-bind="value: telecom.find(url => url.startsWith('mailto:')).substing('mailto:'.length)"
respectively.
And that works just fine for a read-only property, which I might just display. But when I type a new phone number in, the expression ends in a function call, which can't be written to, and of course substring function doesn't know how to work backwards to prepend the "tel:" or "mailto:" prefix before the user-entered value gets written to the object.
I have worked deep down in an XForms engine and before that I had made my own UI framework with a path language similar to XPath, used precisely for two-way data-binding. There I had invented the notion of a "conversion" which was a pair of functions, in this case you might say:
telPrefixConversion = {
prefix: "tel:",
forward: function(value) { return value.substring(this.prefix.length); }
backward: function(value) { return prefix + value; }
}
And I'm thinking this would be super-useful in knockout too. Then on the binding I could just say
data-bind="{value: telecom.find(url => url.startsWith('mailto:')), conversion: telPrefixConversion}"
and now knockout-3.5.0.debug.js line 2842 could do this:
if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
// For two-way bindings, provide a write method in case the value
// isn't a writable observable.
var writeKey = typeof twoWayBindings[key] == 'string' ? twoWayBindings[key] : key;
propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + writableVal + "=_z}");
}
that last line could change to
propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + conversion.backward(writableVal) + "=_z}");
Now I can already think of a way to do that by using a computed observable instead, but it seems heavy weight for something like that. This conversion principle is very powerful as it can also convert complex objects into UI string representations and on the way back it goes into the complex object again.
I am so tempted to implement that in knockout too, since I have done it twice already on other data binding UI tools. Should I go ahead? Or am I missing a simple feature like this?
ATTEMPT 1 - Computed Observable: I have since used computed observables to solve my immediate need, but I found out that this would work when you have more than one telephone number in some kind of repeated groups. For example, let's say you have a list of friends with name, email, and phone number adding computed scratchpad properties just to convert a value to and from string representation is not good.
The fist answer here also suggests computed observable, but both my initial attempt and what is suggested in that answer is too special. We want to have the ability to do it anywhere, regardless if it is a property of one or the other object no matter whether they are also repeated in an array.
So I am thinking of something like this:
class Conversion {
constructor(object, property) {
let self = this;
object[property] = ko.computed({
read: function() {
return self.m2v(ko.util.unwrapObservable(this[property]));
},
write: function(value) {
this[property](m2v(value)); // <<<< infinite recursion????
},
owner: object
});
}
model2view(modelValue) { throw new Error("undefined abstract function called"); }
view2model(viewValue) { throw new Error("undefined abstract function called"); }
}
These model2view and view2model functions can then be overridden to deal with my prefixes, or with date formats as in this other question, etc.
The problem I am stuck on is this:
we replaced the actual value with the observable
when assigning the view value to the observable we would be entering this cycle
We would still need some new property where we store the value for the view separate from the property that holds the actual model value. And that's what I'm trying to avoid by supplying this function pair instead to some other binding option somehow.
ATTEMPT 2 - Extender
I found the observable extenders might almost do the trick:
ko.extenders.prefixConversion = function(target, prefix) {
const read = function() { return target().substring(prefix.length); };
const result = ko.pureComputed({
read: read,
write: function(value) { target(prefix + value); }
});
result(read());
return result;
}
this is used together with the following initializer, and also with these telecom things being objects with url elements, not just the urls as plain strings, example:
o = { ...
telecom: [
{ url: "tel:+1-987-654-3210" },
{ url: "mailto:dude#host.com" }] ... }
then turning this into observables:
telecom
.filter(t => t.url.match('^(tel|mailto):'))
.forEach(t => {
const prefix = t.url.substring(0, value.indexOf(':')+1);
t.url = ko.observable(t.url).extend({prefixConversion: prefix});
});
nice and general works for different prefixes. And here is how we ultimately bind them:
<label>Email:<input data-bind="value: telecom.find(t => t.url.startsWith('mailto:')).url"/></label>
<label>Phone:<input data-bind="value: telecom.find(t => t.url().startsWith('tel:')).url"/></label>
Now this causes the telecom values to be wiped out, because it stores the values back with removed prefixes. It almost works, I can see how the read and write functions produce the correct value, write adds the prefix, and read removes it, but then somewhere, somehow, that value without the prefix gets written into the observable._state._latestValue and then we lose a hold of this altogether.
I don't know if this is a bug somewhere. I don't know even how that prefix-less value ever got written into the observable state.
But in the end, I find even this is too costly an approach anyway. I think the issue applies strictly to binding modes view and text and perhaps textInput, i.e., wherever the ultimate representation is a string. It only applies to the UI surface presentations, it is not about the represented object and its properties. No code should ever get the tel: and mailto: prefix removed, this is only for the user, therefore, this conversion could be bound to the binding handler.
MY OWN SOLUTION
So I am resolving this the same way that I did it with the XForms framework once. I added v2t and t2v functions: v2t means value to text, and t2v means text to value. For example:
<input ...
data-bind="..."
data-t2v="return t.indexOf('tel:') == 0 ? t : 'tel:' + t;"
data-v2t="var p = v.substring(0,4); return p == 'tel:' ? v.substring(4) : v;"
.../>
These attributes get converted to functions during first initialization (or lazily when needed):
if(!(element.t2v === null || element.t2v)) {
const att = element.getAttribute("t2v");
element.t2v = att ? new Function("t", att) : null;
}
if(!(element.v2t === null || element.v2t)) {
const att = element.getAttribute("v2t");
element.v2t = att ? new Function("v", att) : null;
}
then in a few select places, I check and use the element.t2v and .v2t properties respectively before reading and writing respectively. Example:
element.value = element.v2t ? element.v2t(element, value) : value;
This is a solution I have been using for a decade and I think it is right. It is not used when the UI text interactions are more involved. Anything that needs keystroke-event granularity or is in other ways complex, such as requiring inputs from other objects would be handled differently.
I have this implemented and working now. But it is a change to knockout and the question is not moot, because perhaps the knockout people might have a different solution for that.
For example, I note that there is also ko.utils.domData, which I don't understand yet, but which could potentially provide an avenue for this. But my solution is tested in a different data binding framework for a long time and now implemented in knockout, and I think it is the right solution as, again, the test to value and value to text conversions are about the widget and how it renders model data values, not about the data.
You could achieve this without having to modify Knockout, using the built-in writable computed functionality.
Suppose you had a list of people, like so:
vm.person = [
{
name: ko.observable("The H. Dude"),
telecom: [
"mailto:dude#host.com",
"tel:+1-987-654-3210"
]
},
{
name: ko.observable("The I. Gal"),
telecom: [
"mailto:gal#host.com",
"tel:+1-987-654-3211"
]
}
];
Then you could define on your viewmodel a constructor function that returns a writable computed:
vm.getTelecom = function(personIndex, telIndex, prefix) {
return ko.pureComputed({
read() {
return vm.person[personIndex].telecom[telIndex].substring(prefix.length);
},
write(newVal) {
vm.person[personIndex].telecom[telIndex] = prefix + newVal;
}
});
}
And bind that to your inputs:
<!-- ko foreach: person -->
<div>
<label>Name: <input type="text" data-bind="textInput: name"/></label>
<label>Phone:<input type="tel" data-bind="textInput: $parent.getTelecom($index(), 1, 'tel:')"/></label>
<label>Email:<input type="email" data-bind="textInput:$parent.getTelecom($index(), 0, 'mailto:')"/></label>
</div>
<!-- /ko -->
Working demo: https://jsfiddle.net/thebluenile/ujb50vwn/
It feels to me like you're trying to do too much in the view, where knockout would prefer you to add more logic to your viewmodel.
Here's an example of a simple Person View Model that does two things:
Handle the model's (not ideal) format for email and phone explicitly both on the way in and out
Separate the binding values from the model values. The model values have the prefixes, the binding values hide that from the user.
ko.extenders.prefixConversion = function(target, prefix) {
const newTarget = ko.pureComputed({
read: () => target().substring(prefix.length),
write: value => target(prefix + value)
});
return newTarget;
}
const Prefix = {
mail: "mailto:",
phone: "tel:"
};
const PersonVM = person => {
const name = ko.observable(person.name);
const email = ko.observable(
person.telecom.find(str => str.startsWith(Prefix.mail))
);
const phone = ko.observable(
person.telecom.find(str => str.startsWith(Prefix.phone))
);
const toJS = ko.pureComputed(() => ({
name: name(),
telecom: [
email(),
phone()
]
}));
return {
name,
email: email.extend({ prefixConversion: Prefix.mail }),
phone: phone.extend({ prefixConversion: Prefix.phone }),
toJS
};
}
const personVM = PersonVM({
name: "The H. Dude",
telecom: [
"mailto:dude#host.com",
"tel:+1-987-654-3210"
]
});
ko.applyBindings(personVM);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<input data-bind="textInput: name" type="text">
<input data-bind="textInput: email" type="text">
<input data-bind="textInput: phone" type="text">
<pre data-bind="text: JSON.stringify(toJS(), null, 2)"></pre>
I'd like to know the value of another field even when that field hasn't been set in this update, but is part of the document and was set in the past. Is this possible using aldeed:simple-schema?
No. Unless the other field is included in the modifier you have to lookup the document using Collection.findOne and get the field value from that.
Simple you can use for instance:
AutoForm.getFieldValue('profileInformation.dateOfBirth');
age: {
type: String,
optional: true,
defaultValue : function(){
return AutoForm.getFieldValue('profileInformation.dateOfBirth');
}
}
I thought it'd be easy but, yeah... it wasn't. I already posted a question that went in the same direction, but formulated another question.
What I want to do
I have the collection songs, that has a time attribute (the playing-time of the song). This attribute should be handled different in the form-validation and the backend-validation!
! I'd like to do it with what autoform (and simple-schema / collection2) offers me. If that's possible...
in the form the time should be entered and validated as a string that fits the regex /^\d{1,2}:?[0-5][0-9]$/ (so either format "mm:ss" or mmss).
in the database it should be stored as a Number
What I tried to do
1. The "formToDoc-way"
This is my javascript
// schema for collection
var schema = {
time: {
label: "Time (MM:SS)",
type: Number // !!!
},
// ...
};
SongsSchema = new SimpleSchema(schema);
Songs.attachSchema(SongsSchema);
// schema for form validation
schema.time.type = String // changing from Number to String!
schema.time.regEx = /^\d{1,2}:?[0-5][0-9]$/;
SongsSchemaForm = new SimpleSchema(schema);
And this is my template:
{{>quickForm
id="..."
type="insert"
collection="Songs"
schema="SongsSchemaForm"
}}
My desired workflow would be:
time is validated as a String using the schema
time is being converted to seconds (Number)
time is validated as a Number in the backend
song is stored
And the way back.
I first tried to use the hook formToDoc and converted the string into seconds (Number).
The Problem:
I found out, that the form validation via the given schema (for the form) takes place AFTER the conversion in `formToDoc, so it is a Number already and validation as a String fails.
That is why I looked for another hook that fires after the form is validated. That's why I tried...
2. The "before.insert-way"
I used the hook before.insert and the way to the database worked!
AutoForm.hooks({
formCreateSong: {
before: {
insert: function (doc) {
// converting the doc.time to Number (seconds)
// ...
return doc;
}
},
docToForm: function (doc) {
// convert the doc.time (Number) back to a string (MM:SS)
// ...
return doc;
}
}
});
The Problem:
When I implemented an update-form, the docToForm was not called so in the update-form was the numerical value (in seconds).
Questions:
How can I do the way back from the database to the form, so the conversion from seconds to a string MM:SS?
Is there a better way how to cope with this usecase (different data types in the form-validation and backend-validation)?
I am looking for a "meteor autoform" way of solving this.
Thank you alot for reading and hopefully a good answer ;-)
I feel like the time should really be formatted inside the view and not inside the model. So here's the Schema for time I'd use:
...
function convertTimeToSeconds (timeString) {
var timeSplit = timeString.split(':')
return (parseInt(timeSplit[0]) * 60 + parseInt(timeSplit[1]))
}
time: {
type: Number,
autoValue: function () {
if(!/^\d{1,2}:?[0-5][0-9]$/.test(this.value)) return false
return convertTimeToSeconds(this.value)
}
}
...
This has a small disadvantage of course. You can't use the quickForm-helper anymore, but will have to use autoForm.
To then display the value I'd simply find the songs and then write a helper:
Template.registerHelper('formateTime', function (seconds) {
var secondsMod = seconds % 60
return [(seconds - secondsMod) / 60, secondsMod].join(':')
})
In your template:
{{ formatTime time }}
The easy answer is don't validate the string, validate the number that the string is converted into.
With simpleschema, all you do is create a custom validation. That custom validation is going to grab the string, turn it into a number, and then validate that number.
Then, when you pull it from the database, you'll have to take that number & convert it into a string. Now, simpleschema doesn't do this natively, but it's easy enough to do in your form.
Now, if you wanted to get fancy, here's what I'd recommend:
Add new schema fields:
SimpleSchema.extendOptions({
userValue: Match.Optional(Function),
dbValue: Match.Optional(Function),
});
Then, add a function to your time field (stored as Date field):
userValue: function () {
return moment(this.value).format('mm:ss');
},
dbValue: function () {
return timeToNumber(this.value);
}
Then, make a function that converts a timeString to a number (quick and dirty example, you'll have to add error checking):
function timeToNumber(str) {
str.replace(':',''); //remove colon
var mins = +str.substr(0,2);
var secs = +str.substr(2,2);
return mins * 60 + secs;
}
Then, for real-time validation you can use schema.namedContext().validateOne. To update the db, just send timeToNumber(input.value).
in Meteor, I'm having a collection with a Schema, and a number of items are added dynamically.
In this case, I'm dealing with milestones object, and once the user check one off I want to update complete in this Collections item to true (default is false)
Here is my schema
milestones: {
type: Array,
optional: true
},
'milestones.$': {
type: Object
},
'milestones.$.name': {
type: String
},
'milestones.$.hours': {
type: Number
},
'milestones.$.complete': {
type: Boolean
}
How do I write a $set statement for this?
You have an array of objects so, $elemMatch do the trick here.
Projects.update({_id:this._id},{milestones:{$elemMatch:{'milestones.$.name':this.name}},{$set:{'milestone.$.complete':value}}})
So thanks to Aldeed I found a solution - which needs to be called on server side, otherwise it won't let the update happen. Do:
Projects.update({_id:currentPostId, 'milestones.name':name}, {$set:{'milestones.$.complete':true}});
The function is called on the client with Meteor.call with all needed params.
According to your schema you have an object containing an array of objects. So you should write you $set like this:
{$set: {'milestone.$.complete':value}}
This will update the first array element corresponding to the query.
You can find here the official documentation if you want to know more about arrays updates in Mongo.
I have schemas set up so that I can have an array of complex input sets via autoform. Something like:
address = {
street:{
type: String
},
city: {
type: String
},
active_address: {
type: Boolean,
optional: true
},
...
}
people: {
name:{
type: String
},
address:{
type: [address],
optional: true,
defaultValue: []
}
}
This way adding an address is optional, but if you add an address all of the address fields are required.
Trying to submit the form throws a required error for every field under "address" except for the Boolean, even though the checkbox is not checked.
For reference, I'm creating the form as such:
{{#autoForm collection="people" id=formId type="insert" doc=getDocument autosave=true template="autoupdate"}}
{{> afQuickField name='name' template="autoupdate" placeholder="schemaLabel"}}
{{> afQuickField name='address' template="autoupdate"}}
...
{{/autoForm}}
I'm using custom form templates very heavily based on bootstrap3 form templates that come with autoform.
Tried
Tried adding a hook like so:
formToDoc:function(doc, ss, formId){
for (var i = 0, l = doc.address.length; i < l; ++i){
if (!doc.address[i].active_address){
delete doc.address[i].active_address;
};
}
return doc;
}
Which solves the submit problem, but still inserts an array full of empty strings "" for the other values. This causes the update form to go haywire, similar as to what's illustrated in my other question.
The issue is that the array isn't empty, but instead has an object of empty values. I could probably run over every value in the form and remove all the fields but that feels very hacky and expensive.
I was incorrect in my last assessment. I had removed the defaultValue: [] from the address field in the person schema. Using that with the following code in the formToDoc hook fixes the issue:
for (var i = 0, l = doc.address.length; i < l; ++i){
if (!doc.address[i].active_address){
doc.address[i].active_address = null;
}
}
return doc;