Meteor, creating update form and filling fields - meteor

I have the following structure:
{
id: 23423-dsfsdf-32423,
name: Proj1,
services: [
{
id:sdfs-24423-sdf,
name:P1_Service1,
products:[{},{},{}]
},
{
id:sdfs-24jhh-sdf,
name:P1_Service2,
products:[{},{},{}]
},
{
id:sdfs-2jnbn3-sdf,
name:P1_Service3,
products:[{},{},{}]
}
]
},
{
id: 23423-cxcvx-32423,
name: Proj2,
services: [
{
id:sdfs-xvxcv-sdf,
name:P2_Service1,
characteristics:[{},{},{}]
},
{
id:sdfs-xvwqw-sdf,
name:P2_Service2,
characteristics:[{},{},{}]
},
{
id:sdfs-erdfd-sdf,
name:P2_Service3,
characteristics:[{},{},{}]
}
]
}
I have no problem creating a form this schema an insert form with quickForm.
But I cant figure out (and tried to read every tutorial and instruction and nothing worked) how to create an update form with all fields filled and (need to expand and fill the services and the characteristics arrays also:
of course, as i said, in update i need the services and characteristics to expend to the right size with all the fields.
But if i could understand how to fill the form fields i could understand myself how to expend the arrays...
i've tried:
{{> quickForm collection="Projects" id="updateProjectForm" collection="Projects" type="method" class="update-project-form" doc=project }}
with:
import SimpleSchema from 'simpl-schema';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
// Attaching the subscription to the template so we can reuse it
Template.ProjectSingle.onCreated(function(){
var self = this;
self.autorun(function(){
var id = FlowRouter.getParam('id');
self.subscribe('projectSingle', id);
});
});
Template.ProjectSingle.helpers({
project: ()=> {
var id = FlowRouter.getParam('id');
console.log(Projects.findOne({_id: id}));
return Projects.findOne({_id: id});
}
});
I can't even see the console.log() printing.
This solution at list didn't crash the meteor server... everything else i've tried crashed the server on many errors
Maybe i need to mention that i'm using partials so maybe there is a problem with the JS files but i don't think so as the onCreated method is being read.
10x.
EDIT:
I've removed the partial for the update template and it is now in the root Template with its own JS with the method:
projectDoc: ()=> {
var id = FlowRouter.getParam('id');
console.log("Update: " + Projects.findOne({_id: id}));
return Projects.findOne({_id: id});
}
Now i can see this method is being called but for some reason it is being called twice. First with the correct data and then getting undefined so i've still not getting the fields showing anything but if i could find why it is being called twice i will solve the first level form (no services and so on)

Solved it (Not sure this is the best way as i'm still having two calls to the method but this is working for now:
projectDoc: ()=> {
var id = FlowRouter.getParam('id');
if(Projects.findOne({_id: id}) != null){
console.log(Projects.findOne({_id: id}));
thisProject = Projects.findOne({_id: id});
return Projects.findOne({_id: id});
} else {
return thisProject;
}
}

Related

Meteor publications/subscriptions not working as expected

I have two publications.
The first pub implements a search. This search in particular.
/* publications.js */
Meteor.publish('patients.appointments.search', function (search) {
check(search, Match.OneOf(String, null, undefined));
var query = {},
projection = {
limit: 10,
sort: { 'profile.surname': 1 } };
if (search) {
var regex = new RegExp( search, 'i' );
query = {
$or: [
{'profile.first_name': regex},
{'profile.middle_name': regex},
{'profile.surname': regex}
]
};
projection.limit = 20;
}
return Patients.find(query, projection);
});
The second one basically returns some fields
/* publications.js */
Meteor.publish('patients.appointments', function () {
return Patients.find({}, {fields: {'profile.first_name': 1,
'profile.middle_name': 1,
'profile.surname': 1});
});
I've subscribed to each publication like so:
/* appointments.js */
Template.appointmentNewPatientSearch.onCreated(function () {
var template = Template.instance();
template.searchQuery = new ReactiveVar();
template.searching = new ReactiveVar(false);
template.autorun(function () {
template.subscribe('patients.appointments.search', template.searchQuery.get(), function () {
setTimeout(function () {
template.searching.set(false);
}, 300);
});
});
});
Template.appointmentNewPatientName.onCreated(function () {
this.subscribe('patients.appointments');
});
So here's my problem: When I use the second subscription (to appointments.patients), the first one doesn't work. When I comment the second subscription, the first one works again. I'm not sure what I'm doing wrong here.
The issue here is you have two sets of publications for the same Collection. So when you refer to the collection in client there is now way to specify which one of the publication it has to refer too.
What you can do is, publish all data collectively i.e. all fields you are going to need and then use code on client to perform queries on them.
Alternatively, the better approach will be to have two templates. A descriptive code:
<template name="template1">
//Code here
{{> template2}} //include template 2 here
</template>
<template name="template2">
//Code for template 2
</template>
Now, subscribe for one publication to template one and do the stuff there. Subscribe to second publication to template 2.
In the main template (template1) include template2 in it using the handlebar syntax {{> template2}}

Meteor - Tracker.nonreactive() is not removing reactivity from helper

I'm trying to make a Meteor helper non-reactive with this code:
let titleNonReactive;
Template.articleSubmission.onCreated(function () {
this.autorun(function() {
titleNonReactive = Template.currentData().title;
});
});
Template.articleSubmission.helpers({
titleNonreactive: function() {
return titleNonReactive;
}
});
However the resulting output is still reactive. If I save a new value in the background, it's automatically updated on the frontend where I'm displaying the result of this helper with {{ titleNonreactive }}.
How can I fix this?
This is likely caused by your Blaze data context (will need to see your Template code to confirm), but here's a possible solution that doesn't involve using Tracker.nonreactive. Since you want the value of titleNonreactive to not be reactive, you can just use a standard local / non-reactive variable to store a copy of the original reactive title. For example:
import { Template } from 'meteor/templating';
import { articles } from '/imports/api/articles/collection';
import './main.html';
let originalTitle;
Template.body.onCreated(function onCreated() {
this.autorun(() => {
const article = articles.findOne();
if (article && !originalTitle) {
originalTitle = article.title;
}
});
});
Template.body.helpers({
article() {
return articles.findOne();
},
titleNonreactive() {
return originalTitle;
}
});
Then in your Template:
<ul>
{{#with article}}
<li>Reactive Title: {{title}}</li>
<li>Non-Reactive Title: {{titleNonreactive}}</li>
{{/with}}
</ul>

Meteor SimpleSchema with user dropdown

I have an app in Meteor and the idea is that an admin user can add a document and assign it to one of its customers (customers are stored in the user collection). So I would like to present a dropdownbox with customers on the document insert view. The relevant code of the schema is below:
customer: {
type: [String],
label: "Customers",
allowedValues: function () {
return Meteor.users.find().map(function (user) {
return user._id
});
},
autoform: {
options: function () {
return Meteor.users.find({}).map(function(user) {
return {
value: user._id,
label: user.profile.name
}
})
}
},
optional: true
}
When I put a type: String (instead of [String]) it shows the current user only in a dropdownbox. If I use [String] (as it should be), the dropdownbox actually turns in a text box (it does not have the typical dropdown behaviour) with 3 fields (for all the users it found), yet it only shows the first one again but leaves placeholders for the other 2.
The view uses:
{{> afQuickField name='customer'}}
IMPORTANT: The account package, by default doest not publish the users collection. You will have to write a new publication method in your server and corresponding subscription in your client for this to work.
No read on..
Well.. This is a bit tricky. Look at the code below and edit your 'autoform' section accordingly.
autoform: {
options: function () {
var options = [];
Meteor.users.find().forEach(function (element) {
options.push({
label: element.profile.name, value: element._id
})
});
return options;
}
}
the required syntax for selection box 'options' is:
options:{[label,value],...}
The above code reads all the rows from the user collection, and pushes each row to an array called 'options' as an array.
Hope this helps or gives you some insights.
Please note that the above only works if your collection subscription/publication are proper.
Look at the following code to get a simple idea.
if (Meteor.isClient) {
Meteor.subscribe('allUsers')
}
if (Meteor.isServer) {
Meteor.publish('allUsers', function() {
return Meteor.users.find({}, {fields:{username:1,emails:1}})
})
}

Why is data set with Meteor Iron Router not available in the template rendered callback?

This is a bit puzzling to me. I set data in the router (which I'm using very simply intentionally at this stage of my project), as follows :
Router.route('/groups/:_id',function() {
this.render('groupPage', {
data : function() {
return Groups.findOne({_id : this.params._id});
}
}, { sort : {time: -1} } );
});
The data you would expect, is now available in the template helpers, but if I have a look at 'this' in the rendered function its null
Template.groupPage.rendered = function() {
console.log(this);
};
I'd love to understand why (presuming its an expected result), or If its something I'm doing / not doing that causes this?
From my experience, this isn't uncommon. Below is how I handle it in my routes.
From what I understand, the template gets rendered client-side while the client is subscribing, so the null is actually what data is available.
Once the client recieves data from the subscription (server), it is added to the collection which causes the template to re-render.
Below is the pattern I use for routes. Notice the if(!this.ready()) return;
which handles the no data situation.
Router.route('landing', {
path: '/b/:b/:brandId/:template',
onAfterAction: function() {
if (this.title) document.title = this.title;
},
data: function() {
if(!this.ready()) return;
var brand = Brands.findOne(this.params.brandId);
if (!brand) return false;
this.title = brand.title;
return brand;
},
waitOn: function() {
return [
Meteor.subscribe('landingPageByBrandId', this.params.brandId),
Meteor.subscribe('myProfile'), // For verification
];
},
});
Issue
I was experiencing this myself today. I believe that there is a race condition between the Template.rendered callback and the iron router data function. I have since raised a question as an IronRouter issue on github to deal with the core issue.
In the meantime, workarounds:
Option 1: Wrap your code in a window.setTimeout()
Template.groupPage.rendered = function() {
var data_context = this.data;
window.setTimeout(function() {
console.log(data_context);
}, 100);
};
Option 2: Wrap your code in a this.autorun()
Template.groupPage.rendered = function() {
var data_context = this.data;
this.autorun(function() {
console.log(data_context);
});
};
Note: in this option, the function will run every time that the template's data context changes! The autorun will be destroyed along with the template though, unlike Tracker.autorun calls.

Backbone Collection.fetch() returns first item null

I'm using the following code in my view to fetch my collection from the server:
initialize: function () {
_this = this;
this.collection.fetch({
success : function(collection, response) {
_.each(response, function(i){
var todo = new TodosModel({
id: i.id,
content: i.content,
completed: i.completed
});
// Add to collection
_this.collection.add(todo);
// Render
_this.render(todo);
});
},
error : function(collection, response) {
console.log('ERROR GETTING COLLECTION!');
}
});
},
Which seems to work - here's the output from my server:
{
"0": {
"id": 1,
"content": "one",
"completed": false
},
"3": {
"id": 4,
"content": "two",
"completed": true
},
"4": {
"id": 5,
"content": "tester",
"completed": false
}
}
Except for the fact that if I log out my collection there is a null entry in the first position:
Which then causes issues as if I add an item it takes the ID of the last element. I'm new to backbone and am hoping I'm just missing something simple.
Here's my crack at a quick run through of your code. I haven't tested anything so there might be typos. I'm still not sure where the stray empty model is coming from but if you restructure your application as outlined below, I suspect the problem will go away.
The model and collection look okay so let us have a look at your view.
el: $('#todos'),
listBlock: $('#todos-list'),
newTodoField: $('#add input'),
//...
template: $('#todo-template').html(),
//...
events: { /* ... */ },
These should be okay but you need to ensure that all those elements are in the DOM when your view "class" is loaded. Usually you'd compile the template once:
template: _.template($('#todo-template').html()),
and then just use this.template as a function to get your HTML. I'll assume that template is a compiled template function below.
initialize: function () {
_this = this;
You have an accidental global variable here, this can cause interesting bugs. You want to say var _this = this;.
this.el = $(this.el);
Backbone already gives you a jQuery'd version of el in $el so you don't need to do this, just use this.$el.
this.collection.fetch({
success : function(collection, response) {
_.each(response, function(i) {
var todo = new TodosModel({ /* ... */ });
// Add to collection
_this.collection.add(todo);
// Render
_this.render(todo);
});
},
//...
The collection's fetch will add the models to the collection before the success handler is called so you don't have to create new models or add anything to the collection. Generally the render method renders the whole thing rather than rendering just one piece and you bind the view's render to the collection's "reset" event; the fetch call will trigger a "reset" event when it has fetched so the usual pattern looks like this:
initialize: function() {
// So we don't have to worry about the context. Do this before you
// use `render` or you'll have reference problems.
_.bindAll(this, 'render');
// Trigger a call to render when the collection has some stuff.
this.collection.on('reset', this.render);
// And go get the stuff we want. You can put your `error` callback in
// here if you want it, wanting it is a good idea.
this.collection.fetch();
}
Now for render:
render: function (todo) {
var templ = _.template(this.template);
this.listBlock.append(templ({
id: todo.get('id'),
content: todo.get('content'),
completed: todo.get('completed')
}));
// Mark completed
if(todo.get('completed')) {
this.listBlock.children('li[data-id="'+todo.get('id')+'"]')
.addClass('todo-completed');
}
}
Normally this would be split into two pieces:
render to render the whole collection.
Another method, say renderOne, to render a single model. This also allows you to bind renderOne to the collection's "add" event.
So something like this would be typical:
render: function() {
// Clear it out so that we can start with a clean slate. This may or
// may not be what you want depending on the structure of your HTML.
// You might want `this.listBlock.empty()` instead.
this.$el.empty();
// Punt to `renderOne` for each item. You can use the second argument
// to get the right `this` or add `renderOne` to the `_.bindAll` list
// up in `initialize`.
this.collection.each(this.renderOne, this);
},
renderOne: function(todo) {
this.listBlock.append(
this.template({
todo: todo.toJSON()
})
)
// Mark completed
if(todo.get('completed')) {
this.listBlock.find('li[data-id="' + todo.id + '"]')
.addClass('todo-completed');
}
}
Notice the use of toJSON to supply data to the template. Backbone models and collections have a toJSON method to give you a simplified version of the data so you might as well use it. The model's id is available as an attribute so you don't have to use get to get it. You could (and probably should) push the todo-completed logic into the template, just a little
<% if(completed) { %>class="completed"<% } %>
in the right place should do the trick.
addTodo: function (e) {
//...
var todo = new TodosModel({
id: todoID,
content: todoContent,
completed: todoCompleted
});
this.render(todo);
todo.save();
_this.collection.add(todo);
You could bind renderOne to the collection's "add" event to take care of rendering the new model. Then use the save callbacks to finish it off:
var _this = this;
var todo = new TodosModel({ /* ... */ });
todo.save({}, {
wait: true,
success: function(model, response) {
// Let the events deal with rendering...
_this.collection.add(model);
}
});
Again, an error callback on the save might be nice.
completeTodo: function (e) {
//...
todo.save({
completed: todoCompleted
});
}
The save call here will trigger a 'change:completed' event so you could bind to that to adjust the HTML.
removeTodo: function (e) {
//...
}
The destroy call will trigger a "destroy" event on the model and on the collection:
Any event that is triggered on a model in a collection will also
be triggered on the collection directly, for convenience. This
allows you to listen for changes to specific attributes in any model
in a collection, [...]
So you could listen for "destroy" events on the collection and use those to remove the TODO from the display. And destroying the model should remove it from the collection without your intervention.
printColl: function () {
this.collection.each(function (todo) {
console.log('ID: '+todo.get('id')+' | CONTENT: '+todo.get('content')+' | COMPLETED: '+todo.get('completed'));
});
}
You could just console.log(this.collection.toJSON()) instead,
you'd have to click around a little to open up the stuff in the
console but you wouldn't miss anything that way.
All the event binding for the collection would take place in your
view's initialize method. If you're going to remove the view then
you'd want to override the remove to unbind from the collection
to prevent memory leaks:
remove: function() {
// Call this.collection.off(...) to undo all the bindings from
// `initialize`.
//...
// Then do what the default `remove` does.
this.$el.remove()
}
You could also use a separate view for each TODO item but that might be overkill for something simple.

Resources