Use Flow Router Param in Autoform - meteor

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

Related

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

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

Best way to prevent a template helper to be rerun when it is unnecessary?

I'm trying to prevent a template helper to be rerun when it is unnecessary. I made a simple application to illustrate this behavior:
Let's say I want to display some items that contain only a title and a description.
<template name="Tests">
{{#each items}}
{{> TestsItems}}
{{/each}}
</template>
<template name="TestsItems">
<div class="title">{{title}}</div>
<div class="description">{{description}}</div>
</template>
I have autopublish enabled.
Template.Tests.helpers({
items: function () {
return Items.find();
}
});
Template.TestsItems.helpers({
description: function () {
// I'm using this helper to do some updates
// on a jQuery plugin when the description field change.
// see example 1: https://github.com/avital/meteor-ui-new-rendered-callback/
console.log("The description is run");
return this.description;
}
});
When a new update is made on the title field only, you can see that the description helper is rerun. What I'm trying to achieve is to only rerun this helper when there is a new value for the description field and not every time a field has changed in the document.
As {{#constant}} and {{#isolate}} are deprecated, how can I get this behavior in the latest Meteor versions?
Note 1: Create a new subtemplate including the description does not fix the problem.
I would avoid side effects in template helpers. Instead I would use an autorun:
Template.TestItems.rendered = function () {
var _id = this.data._id;
this.autorun(function () {
// Select only the description field, so that we only
// trigger a re-run if the description field changes
var description = Items.findOne(_id, {fields: {description: 1}}).description;
// update the JQuery plugin
});
}

Meteor: how do I create a post insert hook?

I'm using autoform to create forms. I have the template below which is correctly inserting data into the collection when the form is submitted. What I want to do is insert a record into another collection when the insert into the "ContactDetails" collection has been completed successfully.
<template name="contactDetailsForm">
{{#if submitted}}
{{> quickForm collection="ContactDetails" omitFields="createdBy" doc=editingDoc id="contactDetailsForm" type="update"}}
{{else}}
{{> quickForm collection="ContactDetails" omitFields="createdBy" id="contactDetailsForm" type="insert"}}
{{/if}}
</template>
As far as I know I would need to add a hook. I'm really not sure what I'm doing with this. I'd imagine it would look something like this:
AutoForm.addHooks(['contactDetailsForm'], {
after: {
insert: function(error, result) {
if (error) {
console.log("Insert Error:", error);
} else {
console.log("Insert Result:", result);
// NOW DO INSERT INTO OTHER COLLECTION
}
}
}
});
Can anyone show me how to insert a record into another collection after an insert has been successfully completed in a different collection?
Any advice/help/examples on this would be sincerely appreciated.
The matb33:collection-hooks package is a standard way to create such hooks. First add it with
meteor add matb33:collection-hooks
Then create your hook:
ContactDetails.after.insert(function(userId, doc) {
console.log("Inserted:", this._id);
...
});
In general the hooks are not available yet - but they are in autoform:
https://github.com/aldeed/meteor-autoform#callbackshooks
If you were not using autoform I would do the insert with a Meteor method, where you could just do the after insert, after you inserted the first one.
See this https://www.discovermeteor.com/blog/meteor-methods-client-side-operations/

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);

Template is re-rendered even though there is no data change

I'm struggling with an issue that I will explain giving a simple demo.
There's following very simple document in People Collection.
{
"_id" : "vxmLRndHgNocZouJg",
"fname" : "John" ,
"nicks" : [ "Johnny" , "Jo"]
}
Now let's consider following templates. Basically I display username and a list of nicknames with input field for adding more nicknames.
<head>
<title>test</title>
</head>
<body>
{{> name}}<br/>
{{> nicks}}
</body>
<template name="name">
<input type="text" value="{{fname}}"/>
</template>
<template name="nicks">
{{#each nicks}}
<div>{{this}}</div>
{{else}}
no nicks yet
{{/each}}
<input type="text" name="nicks"/>
<input type="submit"/>
</template>
My client javascript code is as follows:
Template.name.fname = function() {
return People.findOne({"fname" : "John"},{
transform : function(doc) {
return doc.fname;
}
});
}
Template.name.rendered = function() {
console.log('Template "name" rendered!');
}
Template.nicks.nicks = function() {
var john = People.findOne({"fname" : "John"});
if(john) return john.nicks;
}
Template.nicks.events({
'click input[type="submit"]' : function () {
var johnId = People.findOne({"fname" : "John"})._id; // demo code
People.update(johnId,{
$addToSet : {
nicks : $('input[name="nicks"]').val()
}
})
}
});
My problem is that after adding nickname (update of nicks field in a document) template name is re-rendered (I know because I console.log it). When I query People collection to provide data for name template I use transform option so changes in nicks field shouldn't have impact on name template.
Meteor docs supports this:
Cursors are a reactive data source. The first time you retrieve a cursor's documents with fetch, map, or forEach inside a reactive computation (eg, a template or autorun), Meteor will register a dependency on the underlying data. Any change to the collection that changes the documents in a cursor will trigger a recomputation.
Why template name is re-rendered then?
The template is re-rendered because you change the People collection.
When you alter the People collection, Meteor automatically assumes that everything that it provides data to needs to be recalculated. (Which your name template does via Template.name.fname.
Even though you transform the output of the cursor, the People collection has changed in some way. The query is done before the transform is used, in other words, its not the transform that is looked at but the whole collection.
Meteor thinks that perhaps your document with {'fname':'John'} may have some other field that might have changed and it needs to requery it to check (which the nicks field has been altered). The transform is then applied after the requery.
Your HTML might not actually change at this point, only if the cursor returns something different will the html be changed.
If it becomes an issue in any scenario (i.e forms being cleared or DOM being changed when it shouldn't be) you can use the {{#isolate}} {{/isolate}} blocks to ensure that only everything inside them is re-rendered and nothing outside.

Resources