Boolean in nested schema causing required state - meteor

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;

Related

knockout write binding without observable

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>

How can I read a value from a mongodb cursor inside the same publication where the cursor was generated

I have a meteor publication in which I was trying to use .findOne() to access the fields from the mongodb. But as I cannot return .findOne() from a meteor publish, I am now using just .find(). The problem is that it returns a cursor instead of a document so I am not able to read the values inside that cursor in my publish function.
Below is my mongodb query
var question = Question.find({ "_id": quesId },
{ fields: {"pages": 1, "settings.name": 1, "status": 1 }},
{limit: 1});
And I want to use the value of pages that I get from the above query inside the same publish function
you can set an observer on your cursor and get a hook into the results. i do that frequently to transform the result.
e.g. (i am making the assumption that the publish name is "questions")
let cursor = Question.find({ "_id": quesId }, { "pages": 1, "settings.name": 1, "status": 1 }, {limit: 1});
let handle = cursor.observeChanges({
added: (id, fields) => {
// look at fields
this.added('questions', id, fields);
},
changed: (id, fields) => {
// look at fields
this.changed('questions', id, fields);
},
removed: (id) => {
this.removed('questions', id);
}
});
this.ready();
this.onStop(() => {
handle.stop();
});
this will give you a hook into the records that are added at the initial publish, and changed later. call ready() when your publication is ready to be published (you can see the this.ready() in the sample).
As far as I understand your question, you want to access the single document in the cursor without doing a redundant findOne() in your code. You can transform the result into an array with .fetch() on the cursor, get the first entry with [0] and then directly get the pages attribute.
const question = Question.find(quesId,
{ fields: { "pages": 1, "settings.name": 1, "status": 1 }});
const pages = question.fetch()[0].pages;
Note also that when searching on _id you don't have to specify {_id: quesId} in your filter, you can directly use the value you want to search on as the filter parameter, mongo assumes that you're searching on _id. Furthermore the {limit: 1} is redundant since you're searching on a scalar _id value and so you're guaranteed that the cursor length will be one.

autoform won't render select option field

I have an issue regarding collection2 with relationships and autoform.
I try to implement an 1:n relationship, where each object has exactly 1 objectType, while to each objectType multiple objects can be referred to.
My schema looks as follows:
// register collections
Objects = new Mongo.Collection('objects');
ObjectTypes = new Mongo.Collection('objectTypes');
// define schema
var Schemas = {};
Schemas.ObjectType = new SimpleSchema({ // object type schema
name: {
type: String
}
});
Schemas.Object = new SimpleSchema({ // object schema
type: {
type: ObjectTypes.Schema,
optional: true
},
title: {
type: String
}
});
// attach schemas
ObjectTypes.attachSchema(Schemas.ObjectType);
Objects.attachSchema(Schemas.Object);
My autoform looks like this:
{{> quickForm collection="Objects" id="insertTestForm" type="insert"}}
I actually would expect a select option field for my type attribute, however, a text input appears. Anyone knows why?
According to the documentation [1], it should be a select option field:
If you use a field that has a type that is a Mongo.Collection instance, autoform will automatically provide select options based on _id and name fields from the related Mongo.Collection. You may override with your own options to use a field other than name or to show a limited subset of all documents. You can also use allowedValues to limit which _ids should be shown in the options list.
[1] https://github.com/aldeed/meteor-collection2/blob/master/RELATIONSHIPS.md#user-content-autoform
EDIT
If I use
type: ObjectTypes,
instead of
type: ObjectTypes.Schema,
my app crashes, throwing the following error:
Your app is crashing. Here's the latest log.
/Users/XXX/.meteor/packages/meteor-tool/.1.1.3.ik16id++os.osx.x86_64+web.browser+web.cordova/mt-os.osx.x86_64/dev_bundle/server-lib/node_modules/fibers/future.js:245
throw(ex);
^
RangeError: Maximum call stack size exceeded
Exited with code: 8
Your application is crashing. Waiting for file change.
Your type isn't "a Mongo.Collection instance" like the documentation says; it's a Schema. Try this:
Schemas.Object = new SimpleSchema({
type: {
type: ObjectTypes,
optional: true
},
...
Since nobody could help me solve this incident, I came up with an alternate solution:
// register collections
Objects = new Mongo.Collection('objects');
ObjectTypes = new Mongo.Collection('objectTypes');
// define schema
var Schemas = {};
Schemas.Object = new SimpleSchema({ // object schema
type: {
type: String,
optional: true,
autoform: {
return ObjectTypes.find().map(function(c) {
return{label: c.name, value: c._id}
});
}
},
// ...
});
// attach schema
Objects.attachSchema(Schemas.Object);
As u can see, I manually map the attributes I need from the objectTypes collection into the autoform attribute. Since it returns an array of objects, containing the label and value attributes, autoform will automatically render a select option.

Datatype validation error - MeteorJS/Autoform/SimpleSchema/Embedded Simple Schema

I am using Simple Schema,collection hooks and Autoform packages in Meteor and I am trying to update a Embedded object in my schema in the after update collection hook. I feel I might doing something silly, but was just unable to solve this problem. I am getting the exeption while saving: Exception while invoking method '/providers/update' Error: 0 must be an integer
My schema:
Schemas.Providers = new SimpleSchema({
email: {
type: String,
label: 'Email Address',
autoValue: function() {
if (this.isInsert || this.isUpdate) {
return Meteor.user().emails[0].address;
}
},
autoform: {
afFieldInput: {
type: 'email'
}
}
},
officelocation: {
type: String,
label: 'Location of Office',
optional:true
},
location: {
type: [LocationSchema],
optional:true
}});
The collection hook that works:
Providers.after.update(function (userId, doc) {
var oldDoc = this.previous;
Providers.direct.update({_id: doc._id},{$set: {location:{
type:'Point',
coordinates:[0,0]
}}});
});
The collection hook that does not work.. Ideally I should not be updating after the collection update, but wanted to make sure this works:
Providers.after.update(function (userId, doc) {
var oldDoc = this.previous;
var array=[];
var iArray=doc.officelocation.split(",");
array.push(Number(iArray[1]));
array.push(Number(iArray[0]))
Providers.direct.update({_id: doc._id},{$set: {location:[{
type:'Point',
coordinates:array
}]}});
});
Looking at what you are trying to store, use parseInt instead of Number that you are using. It will return an integer that mongo can store.
The issue is not with the method that you are using. It is with the way you are storing data in mongo. Show us how your LocationSchema looks like.
Detail:
Mongo uses a different number format that what javascript uses. In javascript, a Number can be an integer, a float, a decimal or anything that you want. Mongo has very strict demands when it comes to integer or floats.
Overall, what it means is that if you want to store an accurate decimal number in mongo (which I suspect what you are trying to do), you have to either store it as a string (you loose the ability to do direct mongo operation such as $inc $gt etc) or divide it into two parts and store separably. The third option is to store it as a float which isn't accurate an is only useful if you want some kind of approximate value.

Meteor AutoForm - Invalid Field Name with Array Index

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";
}
},
},

Resources