Best practice for Meteor AutoForm validation of items not in the form? - meteor

AutoForm works great when you want to validate a form using the schema, but oftentimes the form doesn't contain all the data that's in the schema, so you get a validation error.
For example, I have a submit button that is disabled until it determines the form is completely valid, but because the form doesn't contain all the data that's in the schema, it can never show that it's completely valid.
Say that you've got a schema
ShoppingCartSchema = new SimpleSchema({
itemsOrdered: {
type: [Object],
optional: false,
},
totalPrice: {
type: Number,
optional: false,
},
customerAddress: {
type: Object,
optional: false
},
systemGeneratedInfo: {
type: Object,
optional: true,
blackbox: true
},
});
ShoppingCarts.attachSchema(ShoppingCartSchema);
My form code would be something like:
{{> quickForm collection="ShoppingCarts" id="shoppingCartForm" type="insert"}}
Obviously you don't want totalPrice to be an item on the form for the user to fill on their own. systemGeneratedInfo could be an object that your code generates based on the form values, but which doesn't appear on the form itself.
You could put totalPrice into a hidden form element but that seems sketchy. You really wouldn't want to do that with systemGeneratedInfo.
What other strategies would there be for tackling items on the object that don't show up on the form, but which still allow the form shown on the front end to be completely validated?

You have to make use of the schema field of the quickForm or autoForm. Then you create Template helpers to pass the schema to the form (or if your schemas are available globally you can just pass Schemas.someSchema to the form).
You can either define completely new schemas just for minor form functionality tweaks, which in my mind is overkill if you have a giant nested schema and you just want to omit a couple of fields or...
You can just import the original schema into your template.js and then do an EJSON.clone on it and modify the fields and attributes locally in the helper (or elsewhere) before you pass it to the template.
// Profile.js
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { AutoForm } from 'meteor/aldeed:autoform';
import { EJSON } from 'meteor/ejson';
import { Schema } from '../../../api/profiles/profiles.js';
Template.profile.helpers({
profileSchema: function() {
let userSchemaForAutoForm = EJSON.clone(Schema.User);
userSchemaForAutoForm._schema.emails.optional = true;
return userSchemaForAutoForm;
}
});
// Profile.html
{{#autoForm id="profileEditForm" collection="Meteor.users" schema=profileSchema doc=currentUser type="update"}}

Related

Altering a returned collection in onBefore hook or similar hook

Is it possible to alter (add more fields for each record) a returned collection in onBeforeAction or similar hook?
I have InvoiceHistory collection which I am paginating through. I also want to display as part of each invoice the company name, business address, email address and VAT registration number - and these four fields are stored in another collection. So I would like to add these four fields to each record returned from InvoiceHistory. If there is another way to do it I am open to suggestions. I am using Alethes meteor pagination which loses the helper fields returned in its itemTemplate when you navigate/browse/page to the second page. Alethes pagination also relies on iron-router, is it maybe possible to achieve what I want with the help of iron-router
========================================================================
#Sean. Thanks. Below is the code that uses meteor publish composite:
if (Meteor.isServer) {
import { publishComposite } from 'meteor/reywood:publish-composite';
publishComposite('invoicesWithCompanyDetails', function(userId, startingDate,endingDate) {
return {
find() {
// Find purchase history for userId and the two dates that were entered. Note arguments for callback function
// being used in query.
return PurchaseHistory.find({Id:userId,transactionDate:{$gte:new Date(decodeURIComponent(startingDate)),
$lt:new Date(decodeURIComponent(endingDate))}});
},
children: [
{
find() {
return CompanySharedNumbers.find(
{ _id:"firstOccurrence" },
{ fields: { companyName: 1, companyAddress: 1 } }
);
}
}
]
}
});
}
Router.route('/store/invoices/:_username/:startingDate/:endingDate', { //:_startingDate/:_endingDate
name: 'invoices',
template: 'invoices',
onBeforeAction: function()
{
...
},
waitOn: function() {
var startingDate = this.params.startingDate;
var endingDate = this.params.endingDate;
return [Meteor.subscribe('systemInfo'),Meteor.subscribe('testingOn'),Meteor.subscribe('invoicesWithCompanyDetails',startingDate,endingDate)];
}
});
Pages = new Meteor.Pagination(PurchaseHistory, {
itemTemplate: "invoice",
availableSettings: {filters: true},
filters: {},
route: "/store/invoices/:_username/:startingDate/:endingDate/",
router: "iron-router",
routerTemplate: "invoices",
routerLayout: "main",
sort: {
transactionDate: 1
},
perPage: 1,
templateName: "invoices",
homeRoute:"home"
});
Instead of adding new fields to all your document. What you could do is to add a helper to load a company document :
Template.OneInvoice.helpers({
loadCompany(companyId){
return Company.findOne({_id: companyId});
}
});
And use it in your template code:
<template name="OneInvoice">
<p>Invoice id: {{invoice._id}}</p>
<p>Invoice total: {{invoice.total}}</p>
{{#let company=(loadCompany invoice.companyId)}}
<p>Company name: {{company.name}}</p>
<p>Company business address: {{company.businessAddress}}</p>
{{/let}}
</template>
This way, the code is very simple and easily maintainable.
If I'm understanding you correctly, this sounds like a job for a composite publication.
Using a composite publication you can return related data from multiple collections at the same time.
So, you can find all your invoices, then using the company ID stored in the invoice objects you can return the company name and other info from the Companies collection at the same time.
I've used composite publications before for complex data structures and they worked perfectly.
Hope that helps

SimpleSchema and AutoForm: Render password field as single input

I'm working in a project where a "super admin" user can create another users, setting they usernames and passwords. I have an AutoForm quickForm rendering a form based upon the SimpleSchema attached to the Meteor.users collection (using Collection2).
Following the Collection2 docs recommendation on attaching a schema to the users collection, my schema looks like this:
Usuarios.schema = new SimpleSchema({
...
username: {
type: String,
},
services: {
type: Object,
optional: true,
blackbox: true
},
"services.password": {
type: String,
optional: true,
autoform: {
type: 'password'
}
},
...
});
The rendered form looks like this:
But I would like to have the Password field rendered like the Username one (without the Services panel).
I haven't found a workaround to this. I need to have the Services Object type atribute in the schema or the validation upon user insert fails (with Accounts.createUser()), and so, the panel is rendered (because of the Object type of the atribute).
Any ideas on how I could achieve the desired template rendering?
Your password is part of the 'service' object, which is why it is automatically rendered inside a afObjectField when using quickForm or quickFields:
afObjectField
When you use the afQuickField component for a field that is an Object,
it is rendered using the afObjectField component unless you override
the type or specify options. This happens by default when you use a
quickForm for a schema that has a field of type Object.
The afObjectField component renders all of an object field's subfields
together as one group. The group is labeled with the name of the
object field. The actual visual representation of the group will vary
based on which theme template you use. For the "bootstrap3" default
template, the group appears in a panel with a heading.
Solution A: Manual Form Rendering
To render your password as a single field, you need to set up your autForm manually and reference the fields by using afFieldInput. The form mthen may look like (not tested but code should look like) this:
{{> autoForm id="login" schema=Usuarios.schema}}
{{> afFieldInput type="text" name="username"}}
{{> afFieldInput type="password" name="services.password"}}
{{/autoForm}}
Which has the advantage that your form looks exactly as you wish but has the disadvantage, that you have to add all extras (validation messages and stuff) manually.
Solution B: Change your Schema
When you pull password out of the service group, then you will have the same effect of auto-render as with username: a single input field.
Usuarios.schema = new SimpleSchema({
...
username: {
type: String,
},
services: {
type: Object,
optional: true,
blackbox: true
},
...
password: {
type: String,
optional: true,
autoform: {
type: 'password'
}
},
...
});

Use Flow Router Param in Autoform

Friends,
I'm working on my first app in Meteor and hitting my head against the wall on something...
I have a scenario similar to a blog + comments situation where I have one collection (call it 'posts') and want to associate documents from another collection (call it 'comments').
The best way I know to pass the post._id to the comments as a "postId" field is to use the Flow Router params, since the form is on the 'post/:id' view.
But for the life of me, I cannot figure out how to get "var postId = FlowRouter.getParam('postId');" to pass to Autoform so it populates. I've tried adding it as a function in the schema, as a hook, and as a hidden field in the form on the page (obviously don't want to go that route).
Autoform is amazing and I want to use it, but may have to wire it up the hard way if I can't get this darn value to populate.
Any ideas? I've been hitting my head against the wall on this for a couple of days now.
Thanks!
First, just so we're on the same page, if you have your route is set up like this:
FlowRouter.route('/blog/:postId', {
action: function (params, queryParams) {
FlowLayout.render('layout', { body: 'postTemplate' });
},
});
You are able to call FlowRouter.getParam('postId') from inside the AutoForm hook
You'll need to use an AutoForm hook and have a complete schema. I'm using the package aldeed:collection2 for the schema set up. The postId field must be explicity declared. This code is running on both server and client.
Comments = new Mongo.Collection("comments");
Comments.attachSchema(new SimpleSchema({
comment: {
type: String,
label: "Comment"
},
postId: {
type: String
}
}));
Setting your form up like this is not what you want:
{{> quickForm collection="Comments" id="commentForm" type="insert"}}
That's no good because it will show the postId field in the HTML output. We don't want that, so you have to fully define the form like this:
{{#autoForm collection="Comments" id="commentForm" type="insert"}}
<fieldset>
{{> afQuickField name='comment' rows=6}}
</fieldset>
<button type="submit" class="btn btn-primary">Insert</button>
{{/autoForm}}
Then add the AutoForm hook. This code is running on the client.
var commentHooks = {
before: {
insert: function(doc){
var postId = FlowRouter.getParam('postId');
doc.postId = postId;
return doc;
}
}
};
AutoForm.addHooks(['commentForm'],commentHooks);
Make sure you have your allow/deny rules set up, and it should be working fine.
I was struggling with this same use case as well, and I found this on the Meteor forums: https://forums.meteor.com/t/use-flow-router-param-in-autoform/14433/2
If you're using a schema to build your form (either with the autoform or quickform tags) then you can put it right in there.
For example:
campaignId: {
type: String,
autoform: {
value: function() {
return FlowRouter.getParam('campaignId');
},
type: "hidden"
}
},

Meteor autoform Form hidden fields not rendering defaultValue and not saving

I have two hidden fields in my autoform Schema defined as shown below. I wish to save those two fields with other fields while not showing them to the app user. But I noticed from the autoform rendered html that the two hidden fields have no value, also they don't save with other fields to DB. Not sure what I might be missing / wrong here? Thanks for your help
Invoice = new SimpleSchema({
clientid: {
type: String,
optional: true
},
total: {
type: String,
label: 'Total Amount',
optional: true
},
tax: {
type: String,
label: 'Taxes',
optional: true
},
category: {
type: String,
optional: true,
autoform: {
type: "hidden",
label: false
},
defaultValue: 'Test Category'
}
});
{{> quickForm id="invoiceForm" buttonContent="Insert" buttonClasses="btn btn-primary btn-sm" schema=Invoice type="method" meteormethod="saveInvoice"}}
I don't think that you can have a field in your aldeed Schema that will render/ get saved in the form as a hidden field. So I suggest you pass the data (which you originally wanted to pass a a hidden field) though Sessions.
For example, if you are using autoform >> meteormethod to save the form, then you can save the session content within the server method. If you are not using a method, then you might want to pass the hidden data through Autoform.hooks >> onSubmit
In my opinion it's best to keep the logic related to the form to the quickform template, for the case you would reuse your schema in another form, for instance.
I'd recommend you to do the following:
...
},
category: {
type: String,
optional: true,
defaultValue: 'Test Category'
}
...
And use the omitFields clause (note you can specify multiple fields to be omitted separating them by comma):
{{> quickForm id="invoiceForm" buttonContent="Insert" buttonClasses="btn btn-primary btn-sm" schema=Invoice type="method" meteormethod="saveInvoice" omitFields="category, foo, bar, ..."}}
I've noted you are using method as the form type. If you are manually setting a method for saving your data, you might consider specify default and auto values inside the method itself. It will give you more freedom and control over your data.
Have you tried not making the category field optional? There seems to be a conceptual problem between having a defaultValue and having the field as optional.

Meteor Scope error when using Autoform

Using autoform and dependencies plus iron router. Autopublish is on and I'm seeing the collection on the client console. New project on .8, everything newly installed.
in schema.js, which I've tried in a few locations (/lib, /)
Tvseries = new Meteor.Collection("tvseries", {
schema: {
title: {
type: String,
label: "Title",
max: 250
},
airStartDate: {
type: Date,
label: "First episode air date"
}
}
});
Then a very basic autoform taken from the example:
<template name="addseries">
{{> quickForm collection="tvseries" id="inserttvseriesForm" type="insert"}}
</template>
Plus a route that is just loading this form:
Router.map(function () {
this.route('addseries', {
path: '/addseries',
template: "addseries"
});
});
I get this message in the JS console:
Exception from Deps recompute function: Error: tvseries is not in the window scope.
You have a typo:
<template name="addseries">
{{> quickForm collection="Tvseries" id="inserttvseriesForm" type="insert"}}
</template>
Your collection is named as Tvseries, not tvseries.
In case Serkan's typo suggestion didn't work, and for anyone looking for relief on this since this shows up on google and is how I got here:
From the docs (this section is buried too far down IMO)
Should the value of schema and collection have quotation marks around it?
If you use quotation marks, then you are telling the autoform to "look for an object in the window scope with this name". So if you define your collections at the top level of your client files and without the var keyword, then you can use this trick to avoid writing helpers.
If you don't use quotation marks, then you must define a helper function with that name and have it return the SimpleSchema or Mongo.Collection instance.
So you'd need a helper function that would look like this:
Template.addseries.helpers({
Tvseries: function () {
Return Tvseries;
}
});
And if you have a schema that is not attached to the collection, you'd also create another helper to return the schema so that you can call it from the template. The docs recommend registering this helper globally:
Template.registerHelper('Schemas', Schemas);

Resources