How to edit data from collection inside Template Helper


#1

Hey guys,

I am facing a little challenge here that I assume is very common… I am loading a collection inside a Template Helper and would like to edit a field’s formatting before showing it on the client.

However, I can’t edit this field after calling Collection.findOne because it is an asynchronous operation that doesn’t accept callbacks!

Here is my code:

Template.UpdateForm.helpers({

    'company'() {

        let companyId = FlowRouter.getParam("companyId");
        let company = Companies.findOne({
             _id: companyId
        });
        company.status = 'OK';
        return company;

    }

});

The result of this is an error telling me that it is impossible to read the property ‘status’ of undefined.
When I console.log( ) company, it is indeed undefined - but not because it doesn’t exist/match, but rather because it hasn’t loaded on time.

Any idea what I could do? (this is a situation that appears quite a few times here in my project)

Thanks!


#2

I’m doing something similar with no issue, can you please post your template code? Do you have an oncreated function as well?

Also I have found for me it has better to load the data on the onCreated function and access data via ReactiveVars,


#3
import './updateform.html';
import { Companies } from '../../lib/collections.js';

if (Meteor.isClient) {

    Template.UpdateForm.onCreated(function () {

        this.autorun(() => {

            this.subscribe('companies');

        });

    });

    Template.UpdateForm.onRendered(function () {

        /* just some initialization code for materialize forms */

    });

    Template.UpdateForm.helpers({

         'company'() {

             let companyId = FlowRouter.getParam("companyId");
             let company = Companies.findOne({
                  _id: companyId
             });
             company.status = 'OK';
             return company;
         }

    });

    Template.UpdateForm.events({

        'click #link-updateForm': function () {

            //

        }

    });

}

#4

Thanks for the quick reply!

In terms of performance, is the solution you suggested (loading data on the onCreated and then using reactiveVar) equivalent? From my (amateur) perspective, it sounds even better since the collection will be called only once (or triggered by autorun), not like inside the template helper where it is called every time something changes.


#5

What is the code in your html template file though. Are you using Blaze or React? Are you calling the helper within the correct scope?

updateform.html


#6
<template name="UpdateForm">
    <div class="section">
        <div class="row">
            <div class="col s12">
                {{#each company}}
                <table class="highlight">
                    <thead>
                        <tr>
                            <th>Company</th>
                            <th>Status</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr class="link-table">
                            <td>{{name}}</td>
                            <td>{{status}}</td>
                        </tr>
                    </tbody>
                </table>
                {{/each}}
            </div>
        </div>
    </div>
</template>

#7

I am using {{#each}} instead of {{#let}} because later there will be more companies, not just one - and I will also change the helper to .find().fetch().


#8

Ok Try This.

mport './updateform.html';
import { Companies } from '../../lib/collections.js';

if (Meteor.isClient) {

    Template.UpdateForm.onCreated(function () {
         this.subscribe('companies');          
         this.companyId = FlowRouter.getParam("companyId");
    });

    Template.UpdateForm.onRendered(function () {

        /* just some initialization code for materialize forms */

    });

    Template.UpdateForm.helpers({

         'company': ()=>{             
             let company = Companies.findOne({
                  _id: Template.instance().companyId
             });
             company.status = 'OK';
             return company;
         }

    });

    Template.UpdateForm.events({

        'click #link-updateForm': function () {

            //

        }

    });

}

#9

The problem here is because the helper runs before the data has arrived through the subscription. When the helper runs, findOne returns undefined because it doesn’t have the data yet (note, findOne is sync on the client because of minimongo).

Because the helper is reactive, it will re-run if the data returned by findOne changes. So all you need to do is add a guard clause before modifying the status, like so:

Template.UpdateForm.helpers({

    'company'() {

        let companyId = FlowRouter.getParam("companyId");
        let company = Companies.findOne({
             _id: companyId
        });
        if (!company) return;
        company.status = 'OK';
        return company;

    }

});

When the subscription finishes loading the data needed by the helper, it will re-run and pass the result to Blaze to render

Everything else can stay the same, no need for reactiveVars or storing extra variables on the template instance


#10

Thanks @coagmano

Is this approach better than using reactiveVars in terms of performance?


#11

Yes, much better. Another way to avoid your helper being called before the subscription has returned is to wrap the <div class="section"> block in a {{#if Template.subscriptionsReady}} block. Though this will not protect against the document missing even after the subscription has returned.


#12

It’s better for performance because there’s a whole layer of reactive functions that you get to skip, it’s all in the one place

Though practically, you won’t really notice a difference in performance


#13

If you will use a company collections filtered in your template, is better make this:

Template.UpdateForm.onCreated(function () {
  this.subscribe('specificCompany', FlowRouter.getParam("companyId"));
});
Meteor.publish("specificCompany", function (companyId) {
  if (this.userId) {
    return Companies.find({ _id: companyId });
  }
  return this.ready();
});

Thus, your client load only one company and not all documents. This also block user view/change values in browser console.