Meteor autoform access nested property - meteor

I've got some linked schemas and am trying to access properties of a subschema from the form for the primary schema. It's a mouthful, I know. Code may help:
//js
Collection = new Meteor.Collection('collection');
Schemas = {};
Schemas.secondary = new SimpleSchema({
unitType: {
type: String,
autoform: {
type: 'select',
options: //function with all the options
}
}
});
Schemas.primary= new SimpleSchema({
name: {
type: String,
},
units: {
type: [ Schemas.secondary ]
}
});
Collection.attachSchema(Schemas.primary);
//HTML
{{#autoForm collection="Collection" id="someId" type="insert"}}
{{> afQuickField name='name'}} // this works
{{> afQuickField name='units'}} // this works
{{> afQuickField name='units.unitType'}} // this doesn't work :-(
{{/autoForm}}
The reason I'm doing this is because there are other properties in the secondary schema that I want to show conditionally, based on the value of the select box. I also tried to put a form inside a form and then run {{#each afFieldNames name='"units"}} but that didn't quite work either. Instead of giving me just the fields contained in units (i.e., the secondary schema), it looped through all fields of both primary and secondary.
Thoughts? I'm not married to this pattern but I can't think of another way.
Thanks again, all.
db

I had this issue myself.
Give this a go
{{> afQuickField scope='units.unitType' name='units.unitType'}}
If you dump your modifier in your before submit hook you should be able to see the subdocument successfully filled out
AutoForm.hooks({
someId: {
before: {
'insert': function(modifier) {
console.log(modifier);
}
}
}
});
Let me know if this works for you!
All the best,
Elliott

Related

Meteor Error: Missing required parameters on path The missing params are: ["_id"]. The params object passed in was: {}

I have this in my routes:
Router.map(function() {
...
this.route('studentEdit', {
path: '/student/:_id/edit',
data: function() {
return Students.findOne(this.params._id);
},
});
this.route('studentDetail', {
path: '/student/:_id',
data: function() {
return Students.findOne(this.params._id);
}
});
...
});
And I have this in my template using autoform:
{{#autoForm collection="Students" id="studentEdit" doc=this type="update"}}
{{> afQuickField name='name'}}
{{> afQuickField name='phone'}}
{{> afQuickField name='address' rows=6}}
{{> afQuickField name='remarks' rows=6}}
<button type="submit" class="btn waves-effect waves-light"><i class="material-icons">save</i></button>
{{/autoForm}}
The edit page loads fine, with the prepopulated fields. And when I save, it does save, yet, it doesn't redirect to the detail page, and returns this error in console:
Exception in delivering result of invoking '/students/update': Error: Missing required parameters on path "/student/:_id". The missing params are: ["_id"]. The params object passed in was: {}.
UPDATE
Routing to the detail page now works, yet the error still exist in the console. I must be missing something. This is what I've done to get it working for the time being:
var moveOnRouter = {
onSuccess: function(formType, result) {
Router.go('studentDetail', {_id: this.docId});
}
}
AutoForm.addHooks('studentEdit', moveOnRouter);
You need to explicitly go to the other route on submit from your form. But since your button is a submit you also need to prevent the default submit action.
With template events you'd do something like:
Template.myTemplate.events({
'submit .btn'(ev) {
ev.preventDefault();
router.go('studentDetail',{ _id: this.docId });
}
});
But since you're hooking autoform perhaps it's easier just to remove the type="submit" from your button definition.

Inserting a file into an array in Meteor using aldeed:autoform, cfs:autoform and the update-pushArray type

I'd like to build an array of uploaded files in each document in my collection named Modules. I'm using the following packages:
aldeed:autoform
aldeed:collection2
cfs:standard-packages
cfs:gridfs
cfs:autoform
Collection and Schema (relevant parts):
Modules = new Mongo.Collection('modules');
Modules.attachSchema (new SimpleSchema({
slides: {
type: Array,
optional: true
},
'slides.$': {
type: Object
},
'slides.$.fileId': {
type: String,
label: "Image File"
},
'slides.$.time': {
type: Number,
label: "Time in Seconds"
}
}));
FileStore = new FS.Collection("fileStore", {
stores: [new FS.Store.GridFS("fileStore")]
});
FileStore.allow({
download: function() {
return true;
},
fetch: null
});
In the HTML Template:
{{#autoForm collection="Modules" scope="slides" id="addSlideForm" type="update-pushArray" doc=this}}
<fieldset>
{{> afQuickField name="time" type="number"}}
{{> afQuickField name="fileId" type="cfs-file" collection="fileStore"}}
</fieldset>
<button type="submit" class="btn btn-primary" >Add Slide</button>
{{/autoForm}}
When I hit the submit button, an element is pushed into the array as expected. The time value is correct, but under fileId there is only dummyId instead of the expected _id from fileStore.
In other parts of the application that do not involve nested arrays, uploading files works as expected. In other parts of the application that do not involve uploading files, the update-pushArray form works as expected. The complication is with combining the two.
Am I doing this incorrectly? Or is cfs:autoform just not compatible with the update-pushArray form type?
To use CFS your #autoform type must be either "insert" or "method", check cfs documentation for more info.
Hope it helps!

Meteor autoform "afFieldValueIs" with a boolean checkbox only triggers once

I have a checkbox that needs to show/hide another input box. I'm doing the following:
Schema:
isFlexibleTime:
type: Boolean
label: 'Is the start time flexible?'
flexibleTimeDetails:
type: String
label: 'Flexible time details'
optional: true
Template:
+afQuickField(name='isFlexibleTime')
if afFieldValueIs name='isFlexibleTime' value=true
+afQuickField(name='flexibleTimeDetails')
The helper will trigger one time and show the other field but it won't trigger again. Any help into what is wrong would be much appreciated.
EDIT
Actually on further inspection it seems there is currently a bug with the checkbox event as of AutoForm 5.1.2 https://github.com/aldeed/meteor-autoform/issues/861
The issue has been open a little while, so you can use a quick workaround like:
In your template event:
'click [name=isFlexibleTime]': function() {
Session.set('isFlexibleTime', AutoForm.getFieldValue('isFlexibleTime','ID_OF_YOUR_AUTOFORM'));
}
Template helper:
isChecked: function() {
return Session.get('isFlexibleTime');
}
then:
{{#if isChecked}}
{{> afQuickField name="flexibleTimeDetails"}}
{{/if}}
I'm not sure if that's your actual syntax but following the example from: http://autoform.meteor.com/fieldvalues it should look like this:
{{> afQuickField name="isFlexibleTime"}}
{{#if afFieldValueIs name="isFlexibleTime" value="true"}}
{{> afQuickField name="flexibleTimeDetails"}}
{{/if}}

How do I get templates inserted from custom block helpers to be individually rerendered in Meteor?

When I use the built-in block helper #each, book templates are rerendered individually when changed:
users =
_id: 'foo'
books: [
{name: 'book1'}
{name: 'book2'}
]
<template name="user">
{{#each books}}
{{> book}}
{{/each}}
</template>
<template name="book">
<div>{{name}}</div>
</template>
When the data is changed - the first book name is set to 'bookone' instead of 'book1' - only the book template (the div containing 'book1') is rerendered. This is the desired behavior. When I use a custom block helper, the behavior is different:
<template name="user">
{{#each_with_id}}
{{> book}}
{{/each}}
</template>
<template name="book">
<div data-id="{{_id}}">{{name}}</div>
</template>
Templates.user.each_with_id = (options) ->
html = "
for book, i in this.books
this.name = book.name
html += Spark.labelBranch i.toString(), ->
options.fn this
html
Now when the name of the first book changes, the whole user template is rerendered.
It does not work as you expect, because the implementation of built-in each is based on the cursor.observeChanges feature. You will not be able to achieve the same exact result without using an auxiliary collection of some sort. The idea is quite simple. It seems that you don't have a "books" collection but you can create a client-side-only cache:
Books = new Meteor.Collection(null);
where you will need to put some data dynamically like this:
Users.find({/* probably some filtering here */}).observeCanges({
added: function (id, fields) {
_.each(fields.books, function (book, index) {
Books.insert(_.extend(book, {
owner: id,
index: index,
}));
}
},
changed: function (id, fields) {
Books.remove({
owner:id, name:{
$nin:_.pluck(fields.books, 'name')
},
});
_.each(fields.books, function (book, index) {
Books.update({
owner : id,
name : book.name,
}, {$set:_.extend(book, {
owner : id,
index : index,
})}, {upsert:true});
}
},
removed: function (id) {
Books.remove({owner:id});
},
});
Then instead of each_with_id you will be able to the built-in each with appropriate cursor, e.g.
Books.find({owner:Session.get('currentUserId')}, {sort:{index:1}});
You may also look at this other topic which basically covers the same problem you're asking about.

Accessing parent context in Meteor templates and template helpers

I'm running into a template context situation that I'm having a hard time finding a way around.
Here's the template in question:
{{#each votes}}
<h3>{{question}}</h3>
<ul>
{{#each participants}}
<li>
<p>{{email}}</p>
<select name="option-select">
{{#each ../options}}
<option value="{{option}}" class="{{is_selected_option}}">{{option}}</option>
{{/each}}
</select>
</li>
{{/each}}
</ul>
</div>
{{/each}}
And here's an example of a vote document:
{
_id: '32093ufsdj90j234',
question: 'What is the best food of all time?'
options: [
'Pizza',
'Tacos',
'Salad',
'Thai'
],
participants: [
{
id: '2f537a74-3ce0-47b3-80fc-97a4189b2c15'
vote: 0
},
{
id: '8bffafa7-8736-4c4b-968e-82900b82c266'
vote: 1
}
]
}
And here's the issue...
When the template drops into the #each for participants, it no longer has access to the vote context, and therefore doesn't have access to the available options for each vote.
I can somewhat get around this by using the ../options handlebars path to jump back into the parent context, but this doesn't affect the context of the template helper, so this in Template.vote.is_selected_option refers to the current participant, not to the current vote or option, and has no way of knowing which option we are currently iterating through.
Any suggestions on how to get around this, without resorting to DOM manipulation and jQuery shenanigans?
This is a templating issue that has come up multiple times for me. We need a formal way of reaching up the template context hierarchy, in templates, template helpers, and template events.
It seems since Spacebars (Meteor's new template engine), you have access to the parent context within {{#each}} blocks using ../.
In Meteor 0.9.1, you can also write a helper and use Template.parentData() in its implementation.
It's not particularly pretty, but I've done something like this:
<template name='forLoop'>
{{#each augmentedParticipants}}
{{> participant }}
{{/each}}
</template>
<template name='participant'>
...
Question: {{this.parent.question}}
...
</template>
// and in the js:
Template.forLoop.helpers({
augmentedParticipants: function() {
var self = this;
return _.map(self.participants,function(p) {
p.parent = self;
return p;
});
}
});
It's similar to the approach that AVGP suggested, but augments the data at the helper level instead of the db level, which I think is a little lighter-weight.
If you get fancy, you could try to write a Handlebars block helper eachWithParent that would abstract this functionality. Meteor's extensions to handlebars are documented here: https://github.com/meteor/meteor/wiki/Handlebars
I don't know the formal way (if there is one), but to solve your issue, I would link the participants with the parent ID like this:
{
_id: "1234",
question: "Whats up?",
...
participants: [
{
_id: "abcd",
parent_id: "1234",
vote: 0
}
]
}
and use this parent_id in helpers, events, etc. to jump back to the parent using findOne.
That is obviously a sub optimal thing to do, but it's the easiest way that comes to my mind as long as there is no way of referencing the parent context.
Maybe there is a way but it is very well hidden in the inner workings of Meteor without mention in the docs, if so: Please update this question if you find one.
It's a long shot, but maybe this could work:
{{#with ../}}
{{#each options}}
{{this}}
{{/each}}
{{/with}}
This should make life easier.
// use #eachWithParent instead of #each and the parent._id will be passed into the context as parent.
Handlebars.registerHelper('eachWithParent', function(context, options) {
var self = this;
var contextWithParent = _.map(context,function(p) {
p.parent = self._id;
return p;
});
var ret = "";
for(var i=0, j=contextWithParent.length; i<j; i++) {
ret = ret + options.fn( contextWithParent[i] );
}
return ret;
});
Go ahead and change
p.parent = self._id;
to whatever you want to access in the parent context.
Fixed it:
// https://github.com/meteor/handlebars.js/blob/master/lib/handlebars/base.js
// use #eachWithParent instead of #each and the parent._id will be passed into the context as parent.
Handlebars.registerHelper('eachWithParent', function(context, options) {
var self = this;
var contextWithParent = _.map(context,function(p) {
p.parent = self._id;
return p;
});
return Handlebars._default_helpers.each(contextWithParent, options);
});
This works :) with no error
Simply register a global template helper:
Template.registerHelper('parentData',
function () {
return Template.parentData(1);
}
);
and use it in your HTML templates as:
{{#each someRecords}}
{{parentData.someValue}}
{{/each}}
======= EDIT
For Meteor 1.2+, you shold use:
UI.registerHelper('parentData', function() {
return Template.parentData(1);
});
I was stuck in a similar way and found that the Template.parentData() approach suggested in other answers currently doesn't work within event handlers (see https://github.com/meteor/meteor/issues/5491). User Lirbank posted this simple workaround:
Pass the data from the outer context to an html element in the inner context, in the same template:
{{#each companies}}
{{#each employees}}
Do something
{{/each}}
{{/each}}
Now the company ID can be accessed from the event handler with something like
$(event.currentTarget).attr('companyId')
"click .selected":function(e){
var parent_id = $(e.currentTarget).parent().attr("uid");
return parent_id
},
<td id="" class="staff_docs" uid="{{_id}}">
{{#each all_req_doc}}
<div class="icheckbox selected "></div>
{{/each}}
</td>
{{#each parent}}
{{#each child}}
<input type="hidden" name="child_id" value="{{_id}}" />
<input type="hidden" name="parent_id" value="{{../_id}}" />
{{/each}}
{{/each}}
The _id is NOT the _did of the thing, it's the id of the parent!

Resources