I'm struggling to implement reactive tables based on a FS.Collection object. I've tried both aldeed/meteor-tabular and aslagle/reactive-table but both fail because the collection doesn't appear to exist. However, if I subscribe and retrieve fields from the Collection without using a reactive table package then the data displays just fine. What am I missing? It can't be a coincidence that both packages fail to work...
Here's my implementation with the aslagle/reactive-table package...
//template
<template name="documentTable">
{{#if Template.subscriptionsReady}}
{{> reactiveTable settings=settings}}
{{else}}
{{> spinner}}
{{/if}}
{{#if currentUser}}
{{> fileUpload}}
{{/if}}
</template>
//documents js
Template.documents.onCreated( () => {
p_id = FlowRouter.current().params.id;
Template.instance().subscribe('documents', p_id);
});
Template.documents.helpers({
documents: function () {
return Documents.find();
},
settings: function () {
return {
collection: documents,
showFilter: false,
rowsPerPage: 5,
showNavigation: auto,
showRowCount: true,
fields: ['_id','userId','propertyId','uploadedAt']
};
}
});
//collection definition
if (Meteor.isServer) {
var docStore = new FS.Store.S3("documents", {
region: "eu-west-1",
accessKeyId: (Meteor.isServer && !process.env.AWS_ACCESS_KEY_ID ? Meteor.settings.AWSAccessKeyId : null),
secretAccessKey: (Meteor.isServer && !process.env.AWS_SECRET_ACCESS_KEY ? Meteor.settings.AWSSecretAccessKey : null),
bucket: Meteor.isServer && process.env.AWS_S3_BUCKET || Meteor.settings.AWSBucket,
folder: "documents"
});
Documents = new FS.Collection("Documents", {
stores: [docStore],
filter: {
allow: {
contentTypes: ['application/pdf']
}
}
});
}
// end server
if (Meteor.isClient) {
var docStore = new FS.Store.S3("documents");
Documents = new FS.Collection("Documents", {
stores: [docStore],
filter: {
allow: {
contentTypes: ['application/pdf']
}
}
});
}
// end client
// allow rules
Documents.allow({
insert: function(userId) {
// only allow insert from signed in user
return userId != null;
},
update: function(userId) {
// only allow update from signed in uesr
return userId != null;
},
download: function() {
return true;
},
});
In the reactive-table case I'm getting the error that the argument is not an instance of Mongo.Collection, a cursor or an array while with meteor-tabular it fails to start because it encounters a ReferenceError and states that Documents isn't defined.
Anyone any thoughts on this?
I'm using aslagle/reactive-table with mongo quite well, with a pub/sub model; I don't know what your new FS is? is that a mongo collection?
I have something like this when I use the reactive-table...
//on the server
Documents = new Mongo.Collection('documents');
//on the client js
Documents = new Mongo.Collection('documents');
Template.documents.helpers({
settings: function () {
return {
collection: Documents.find(),
rowsPerPage: 5,
showFilter: false,
showNavigation: auto,
showRowCount: true,
fields: [
{key: '_id',
label: 'ID' },
{key: 'userId',
label: 'User#' },
{key: 'propertyId',
label: 'Property#' },
{key: 'uploadedAt',
label: 'Uploaded' },
]
};
}
});
//on the client html file
{{> reactiveTable class="table table-bordered table-hover" settings=settings}}
Related
How to publish single objects seems not clear enough to me. Please what's the best way to handle this. This code snippet does not display anything on the view.
Helper file
singleSchool: function () {
if (Meteor.userId()) {
let myslug = FlowRouter.getParam('myslug');
var subValues = Meteor.subscribe('SingleSchool', myslug );
if (myslug ) {
let Schools = SchoolDb.findOne({slug: myslug});
if (Schools && subValues.ready()) {
return Schools;
}
}
}
},
Publish file
Meteor.publish('SingleSchool', function (schoolSlug) {
check( schoolSlug, Match.OneOf( String, null, undefined ) );
user = Meteor.users.findOne({_id:this.userId})
if(user) {
if(user.emails[0].verified) {
return SchoolDb.findOne({ slug: schoolSlug, userId: {$lt: this.userId}});
} else {
throw new Meteor.Error('Not authorized');
return false;
}
}
});
template file
<template name="view">
{{#if currentUser}}
{{#if Template.subscriptionsReady }}
{{#with singleSchool}}
{{singleSchool._id}}
{{singleSchool.addschoolname}}
{{/with}}
{{/if}}
{{/if}}
</template>
As you said "This code snippet does not display anything on the view." well, inside Meteor.publish you need to return cursor, not array or any other object.
So use this code:
Meteor.publish('SingleSchool', function (schoolSlug) {
check( schoolSlug, Match.OneOf( String, null, undefined ) );
var user = Meteor.users.findOne({_id:this.userId});
if(!user || !user.emails[0].verified) {
throw new Meteor.Error('Not authorized');
}
return SchoolDb.find({ slug: schoolSlug, userId: {$lt: this.userId}},{limit:1});
});
I would definitely recommend you to go through How to avoid Common Mistakes
When I am concerned only for a single object, I implement this using a meteor method:
Meteor.methods({
"getSingleSchool":function(schoolSlug) {
//... check args and user permissions
return SchoolDb.findOne({ slug: schoolSlug, userId: {$lt: this.userId}});
},
});
Then in the template I run this method in the onCreated autorun part:
Template.view.onCreated(function(){
const instance = this;
instance.state = new ReactiveDict();
instance.autorun(function(){
let my slug = FlowRouter.getParam('myslug');
// load if no loaded yet
if (my slug && !instance.state.get("singleSchool")) {
Meteor.call("getSingleSchool", mySlug, function(err, res){
//handle err if occurred...
this.state.set("singleSchool", res);
}.bind(instance)); //make instance available
}
});
});
The helper then just returns a value, if the school is loaded:
singleSchool: function () {
return Template.instance().state.get("singleSchool");
},
I'm running into the same problem as issue #53 of aldeed:tabular. When defining the table as suggested in the documentation, it is too soon to invoke a translation function (TAPi18n.__ or other), since the I18N variables are not yet set.
What is the nice, reactive way of feeding the translated column titles into DataTables, either directly as suggested by aldeed himself upon closing the issue, or through aldeed:tabular?
With .tabular.options
There is a way with the template's .tabular.options reactive
variable, but it is quirky. Here is a variation of the library
example using
tap-i18n to translate the
column headers:
function __(key) {
if (Meteor.isServer) {
return key;
} else {
return TAPi18n.__(key);
}
}
Books = new Meteor.Collection("Books");
TabularTables = {};
TabularTables.Books = new Tabular.Table({
name: "Books",
collection: Books,
columns: [] // Initially empty, reactively updated below
});
var getTranslatedColumns = function() {
return [
{data: "title", title: __("Title")},
{data: "author", title: __("Author")},
{data: "copies", title: __("Copies Available")},
{
data: "lastCheckedOut",
title: __("Last Checkout"),
render: function (val, type, doc) {
if (val instanceof Date) {
return moment(val).calendar();
} else {
return "Never";
}
}
},
{data: "summary", title: __("Summary")},
{
tmpl: Meteor.isClient && Template.bookCheckOutCell
}
];
}
if (Meteor.isClient) {
Template.tabular.onRendered(function() {
var self = this;
self.autorun(function() {
var options = _.clone(self.tabular.options.get());
options.columns = getTranslatedColumns();
self.tabular.options.set(_.clone(options));
});
});
}
With a forked version
I created a pull request against branch devel of meteor-tabular to enable the straightforward, reactive-based approach like so:
<template name="MyTemplateWithATable">
{{> tabular table=makeTable class="table table-editable table-striped table-bordered table-condensed"}}
</template>
var MyColumns = ["title", "author"];
// Assume translations are set up for "MyTable.column.title", "MyTable.column.author"
// in other source files; see TAPi18n documentation for how to do that
function makeTable() {
return new Tabular.Table({
name: "MyTable",
collection: MyCollection,
columns: _.map(MyColumns,
function(colSymbol) {
return {
data: colSymbol,
title: TAPi18n.__("MyTable.column." + colSymbol)
};
})
});
}
if (Meteor.isServer) {
// Called only once
makeTable();
} else if (Meteor.isClient) {
// Reactively called multiple times e.g. when switching languages
Template.MyTemplateWithATable.helpers({makeTable: makeTable});
}
Recent versions of aldeed:tabular allow to specify a function for setting the column titles.
import {TAPi18n} from 'meteor/tap:i18n';
TabularTables = {};
TabularTables.Departments= new Tabular.Table({
name: 'Departments',
collection: Departments,
responsive: true,
autoWidth: true,
stateSave: false,
columns: [
{data: "name", titleFn: function() {
return TAPi18n.__("name");
}},
{data: "description", titleFn: function() {
return TAPi18n.__("description");
}}
]
});
The language change is reactive. If you have translations you can switch and columns will be translated.
TAPi18n.setLanguage("en");
TAPi18n.setLanguage("de");
Word of warning:
This currently does not work when you include invisible columns in your table data. The offset is wrong and you get wrong column titles.
With dburles:collection-helpers package you can add collection helpers on any Mongo.collection. But I can't do that on FS.Collection. I get TypeError: Object [object Object] has no method 'helpers'. Transform function doesn't work either.
var createUploader = function(fileObj, readStream, writeStream) {
fileObj.uploadedBy = Meteor.users.find({_id: fileObj.uploader});
readStream.pipe(writeStream);
};
Photos = new FS.Collection("photos", {
stores: [
new FS.Store.GridFS("photos", {transformWrite: createUploader})
],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
Can't do this? Notice when a photo is inserted from the client FS.File gets userId, hence fileObj.uploadedBy = Meteor.users.find({_id: fileObj.uploader});
Ok I know this is not the not-so-easy solution I was looking for. Since I am using publish-composite package. I can just publish users' data(only profile field) with photos. And on the client I can do template helper like this:
Template.photo.helpers({
photoUploader: function() {
var currentPhoto = Photos.findOne();
var user = Meteor.users.findOne({_id: currentPhoto.uploader});
return user.profile.name;
},
});
and
<template name="photos">
{{#each photos}}
{{> photo}}
{{/each}}
...
then
<template name="photo">
{{photoUploader}}
...
matb33-collection-helpers package works by applying a transform function to a collection. CollectionFS already has its own transform function applied, therefore you cannot overwrite that with ones from the collection helpers package.
As suggested at the issue tracker
Since CFS already applies a transform, it would not be a good idea to use collection helpers. You should be able to do pretty much the same thing by extending the FS.File prototype with your own functions, though.
You could define custom functions on the prototype. The prototype will have access to other properties of the doc through this so you would basically have the same functionality with collection helpers.
Another option would be to store the file related information on the individual file object during the insert as metadata such as:
Template.photoUploadForm.events({
'change .photoInput': function(event, template) {
FS.Utility.eachFile(event, function(file) {
var newPhoto = new FS.File(file);
newPhoto.metadata = {uploadedBy: Meteor.user().profile.name};
Photos.insert(newPhoto, function (err, fileObj) {
if (!err) console.log(fileObj._id + " inserted!")
});
});
}
});
Your code can also be rewritten to implement a beforeWrite filter instead of a transformWrite as in
Photos = new FS.Collection("photos", {
stores: [
new FS.Store.GridFS("photos", {
beforeWrite: function (fileObj) {
fileObj.metadata = {uploadedBy: Meteor.user().profile.name};
}
})
],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
Finally, you can opt to storing the ID of the user and publishing a reactive join
Photos = new FS.Collection("photos", {
stores: [
new FS.Store.GridFS("photos", {
beforeWrite: function (fileObj) {
fileObj.metadata = {
uploadedBy: Meteor.userId()
};
}
})
],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
And for the publication, you can use reywood:publish-composite
Meteor.publishComposite('photosWithUsers', function() {
return {
find: function() {
return Photos.find();
},
children: [
{
find: function(photo) {
return Meteor.users.find(photo.uploadedBy, {
fields: {username: 1, 'profile.name': 1}
});
}
}
]
};
});
Of course on the client, you need to subscribe to the photosWithUsers publication.
Now to access that information in the client, since you cannot apply a transform or a helper on the collectionFS documents, you can create a global template helper:
Template.registerHelper('getUsername', function(userId) {
check(userId, String);
var user = Meteor.users.findOne(userId);
return user && user.profile.name + ' (' + user.username + ')';
});
Now you can use that helper in your templates:
<template name="somePhoto">
{{#with FS.GetFile "Photos" photo}}
<img src="{{url}}" alt="This photo has been uploaded by {{getUsername uploadedBy}}">
{{/with}}
</template>
Template.somePhoto.helpers({
photo: function() {
return Photos.findOne();
}
})
I have a simpleSchema:
imageUrl: {
type: Object,
optional: true,
autoValue: function() {
if (Meteor.isClient) return;
var imageField = this.field('imageId');
if (!imageField.isSet){
this.unset();
} else {
var imageObj = MealsImages.findOne(imageField.value);
if (imageObj){
return {thumb: imageObj.S3Url('thumb'), big: imageObj.S3Url('big')};
}
}
},
autoform: {
label: false,
type: 'hidden',
afFieldInput: {
type: "hidden"
}
}
},
For some reason, when I update the record this field always appears in $unset array:
Meteor.methods({
mealUpsert: function(doc, mealId) {
check(doc, Meals.simpleSchema());
console.log('test7');
console.log(doc);
if (mealId){
Meals.update({_id: mealId}, doc);
} else {
mealId = Meals.insert(doc);
}
return false;
}
});
Will print:
I20150830-21:49:39.560(-4)? { '$set':
...
I20150830-21:49:39.562(-4)? '$unset':
I20150830-21:49:39.562(-4)? { imageUrl: '',
...
I'm using autoform:
<template name="mealUpdateForm">
<div class="meal-content">
{{> quickForm collection="Meals" doc=this id="mealUpdateForm" meteormethod="mealUpsert" type="method-update"}}
</div>
</template>
And will never be updated or set. Any clue why field could appear in $unset list?
I think I've figured this out - you have a this.unset() in your autoValue for imageUrl. This is called whenever you omit imageId from the modifier, even if it is already present in a document you are modifying!
I am having a problem getting my Insert Autoform to work properply. I am trying to have it similar to the example http://autoform.meteor.com/insertaf and my code is below. I have already removed insecure and autopublish
client/templates/venues/venue_submit.html
<template name="venueSubmit">
<!-- {{> quickForm collection="Venues" id="venueSubmit" type="insert"}} -->
{{#if isSuccessfulvenueSubmit }}
<h2>Thanks for the Venue </h2>
{{else}}
{{#autoForm id="insertVenueForm" type="insert" collection=Collections.Venues omitFields="createdAt" resetOnSuccess=true}}
{{> afQuickField name="Venue"}}
<div class="form-group">
<button type="submit" class="btn btn-primary">Add Venue</button>
<button type="reset" class="btn btn-default">Reset Form</button>
</div>
{{/autoForm}}
{{/if}}
</template>
client/templates/venues/venue_submit.js
Schemas = {};
Template.registerHelper("Schemas", Schemas);
Schemas.Venue = new SimpleSchema({
Venue: {
type: String,
label: "Venue Name",
max: 200,
autoform: {
placeholder: "Name of the Venue"
}
},
....
});
AutoForm.debug()
var Collections = {};
Template.registerHelper("Collections", Collections);
Venues = Collections.Venues = new Mongo.Collection("Venues");
Venues.attachSchema(Schemas.Venue);
Venues.allow({
insert: function (userId, doc) {
return true;
},
remove: function (userID, doc, fields, modifier) {
return true;
},
remove: function (userId, doc) {
return true;
}
});
if (Meteor.isClient) {
Meteor.subscribe("Venues")
};
server/Publications.js
Meteor.publish('venue', function () {
return Venues.find(id);
});
The AutoForm insert type generates a document and inserts in on the client. Without the autopublish and insecure packages installed you need to make sure to subscribe to the appropriate collection on the client. It does not exist if you do not subscribe to it.
if (Meteor.isClient) {
Meteor.subscribe("Venues")
}
Your problem is that you have venue_submit.js inside the client folder, but its contents are not intended solely for the client.
You can leave venue_submit.html exactly as it is.
Change venue_submit.js to:
Template.registerHelper("Schemas", Schemas);
AutoForm.debug();
Template.registerHelper("Collections", Collections);
Meteor.subscribe("Venues");
You only need the lines here that relate to the client: the two template helpers, the AutoForm debug (which you don't need except for debugging), and the subscribe to the Venues collection.
Change server/Publications.js to include everything related to the server side:
Meteor.publish('Venues', function () {
return Venues.find({});
});
Venues.allow({
insert: function (userId, doc) {
return true;
},
remove: function (userID, doc, fields, modifier) {
return true;
},
remove: function (userId, doc) {
return true;
}
});
It includes the publish function and the permissions on the collection.
Now create lib/schema.js:
Schemas = {};
Schemas.Venue = new SimpleSchema({
Venue: {
type: String,
label: "Venue Name",
max: 200,
autoform: {
placeholder: "Name of the Venue"
}
}
});
Collections = {};
Venues = Collections.Venues = new Mongo.Collection("Venues");
Venues.attachSchema(Schemas.Venue);
Everything in lib will be available to both the client and server and the contents of this folder will be loaded first so the schema and collection definitions will be available to all the rest of the code. Note the lack of a var keyword for the Collections. Using var sets the scope to only within the file. Omitting it makes the Collections variable available throughout your code.