Search code examples
apostrophe-cms

Injecting backend data into view for use in client-side JS


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);
  }
 }
}

Solution

  • 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!