3

I'd like to return a list of docs to an array that will be used in some client side JS. In my apsotrophe-pages module I have this apos.docs.find call that is working and returning documents I need to fetch. Now that I have this array of docs, I need to expose the data to the client-side JS.

Reading through the documentation, it looks like pieces are probably recommended for doing this, but it looks like the pieces would need to be put into a widget, and then the widget in the page.

I understand the best practice concern for separation of the modules, but I am just looking to get this data for use with the client as simply and easily as possible (bit of a time crunch).

Is there a simple and easy way for me to expose this autoCompleteData array to the front end, so I can do something similar to below in my client code?

(Thanks for your help! I've had several Apostrophe questions in the last couple days, but I think this one of the final pieces to help me fill in the gaps for connecting backend objects to the front end.)

\lib\modules\apostrophe-pages\views\pages\home.html

 <script>
   var page = apos.page;
   $('#displayDocs').text(page.autoCompleteData);
 </script>

\lib\modules\apostrophe-pages\index.js

 module.exports = {
  types: [
     { name: 'default', label: 'Default Page' },
     { name: 'apostrophe-blog-page', label: 'Blog Index' },
  ],
  autoCompleteData: [],
  construct: function(self, options) {

    self.pageBeforeSend = function(req, callback) {

       let criteria = [
         {"type": "apostrophe-blog"}, 
         {"published": true}
       ];
       criteria = { $and: criteria };

       self.apos.docs.find(req, criteria).sort({ title: 1 }).toObject(function(err, collection){     
       self.autoCompleteData = collection;
    });

    return callback(null);
  }
 }
}
lance-p
  • 1,050
  • 1
  • 14
  • 28

1 Answers1

4

As you know I lead the Apostrophe team at P'unk Avenue.

You're very close to solving your problem. You just need to attach the data you got back to req.data so that it becomes visible as part of the data object in your Nunjucks template.

But you also need to be more careful with your async programming. Right now you are firing off an asynchronous request to go get some docs; meanwhile you are invoking your callback right away. That's not right — the point of the callback is that you don't invoke it until you actually have the data and are ready for it to be rendered in the template.

Here's a correction of the relevant piece of code:

self.apos.docs.find(req, criteria).sort(
  { title: 1 }
).toArray(function(err, autocomplete) {
  if (err) {
    return callback(err);
  }
  req.data.autocomplete = autocomplete;   
  return callback(null);
});  

Changes I made:

  • I call toArray, not toObject. You are interested in more than one object here.
  • I check for an error before proceeding. If there is one I pass it to the callback and return.
  • I assign the array to req.data.autocomplete so my nunjucks page templates and the layouts they extend can see it.
  • I invoke the callback with null to indicate success after that, and return.

Notice that I always use the return keyword when invoking a callback. Always Be Returning (ABR). If not your code will continue to execute in a way you don't expect.

This code is going to have some performance problems because it fetches a lot of information if the pieces have areas and joins and so on. You should consider adding a projection:

self.apos.docs.find(req, criteria, { title: 1, slug: 1 })...

Add any other properties you care about for this purpose to the properties in the projection. (This is a standard MongoDB projection, so read about those if you would like to know more.)

Using the "super pattern" to keep the original pageBeforeSend method

The apostrophe-pages module already has a pageBeforeSend method, and it does important work. If you just override it, you'll lose the benefit of that work.

One solution is to create a new module in your project, one that doesn't extend another at all, and introduce a pageBeforeSend method there. Since pageBeforeSend is invoked by Apostrophe's callAll mechanism, it is invoked for every module that has one.

Another solution is to follow the "super pattern" to make sure the original version of the method also gets called:

var superPageBeforeSend = self.pageBeforeSend;
self.pageBeforeSend = function(req, callback) {
  // Do our things here, then...
  return superPageBeforeSend(req, callback);
};

Passing the data to javascript in the browser

To answer your next question (:

In your page template, you can now write:

<script>
  var autocomplete = {{ data.autocomplete | json }};
</script>

The json nunjucks filter turns the array into JSON that is guaranteed to be safe for output into a script tag like this. So you wind up with a nice array in browser-side JavaScript.

Using the find method for the appropriate content type

You can avoid hassling with creating your own criteria object just to get the right type of content and check for published, etc. Instead consider writing:

self.apos.modules['apostrophe-blog'].find(req, {}).sort()...

Every pieces module has its own find method which returns a cursor customized to that type, so it will do a better job of sticking to things that have reached their publication date and other stuff you might not be thinking about.

Hope this is helpful!

Tom Boutell
  • 7,281
  • 1
  • 26
  • 23
  • Thanks Tom. Another great answer! I got several tips for free including ABR - and this is helpful - so I appreciate the extra comments. I also appreciate the tip for the shorthand method for not having to build extra where criteria. Since you mention it in your comment, I'll go ahead and ask :). I now nave the db data returned to the page, but what I'm after is data._url. You're right, I don't want all the fields returned, only title and url. Since url is supplied by apos and not the db, is there a built in apos method to get the urls of pages/blog posts? – lance-p Oct 14 '16 at 06:11
  • According to documentation, should I have access to jQuery on the view (apostrophe-pages\views\pageshome.html)? In apostrophe-pages\index.js I have: construct: function(self, options) { self.pushAsset('script', 'jquery', {scene:'always'});} , but in home.html I don't have a reference to $ - console.log($) gives an undefined reference error. – lance-p Oct 14 '16 at 19:26
  • You do have access to jQuery, even when logged out, for sure. Our apostrophe-images widgets wouldn't work otherwise. – Tom Boutell Oct 15 '16 at 18:48
  • I wonder if in this particular page template you left out: {% extends data.outerLayout %} – Tom Boutell Oct 15 '16 at 18:49
  • You must use the outer layout template (outerLayout.html) for your full page renders, otherwise Apostrophe cannot insert pushed assets. See the getting started tutorials for more information. – Tom Boutell Oct 15 '16 at 18:50