0

After doing more research from the documentation, it looks like a custom widget is the best way to try to achieve what I need: an autocomplete drop down that loads article/blog posts by title as the user types (I selected npm jquery-autocomplete, https://www.npmjs.com/package/jquery-autocomplete, as I thought it would be easy to implement for this feature, but perhaps there is an easier way within apos itself).

I followed the instructions in the tutorial on widgets, http://apostrophecms.org/docs/tutorials/getting-started/custom-widgets.html, and think that I almost have this hooked up, but I don't see the search widget (view) rendered, instead the dom shows an empty element div class="apos-area" data-apos-area="" element, which indicates there is a piece missing between linking all the part together, but I can't identify the missing piece based on the documentation sample code.

This is question #1, what is missing in the code below to keep the search.html view from loading?

app.js

 modules: {
    'search-widget': {}
 }

lib\modules\search-widget\index.js

module.exports = {
  extend: 'apostrophe-widgets',
  label: 'SiteSearch',
  addFields: [{
        type: 'string',
        name: 'title',
        label: 'Title'
    },
    {
        type: 'string',
        name: 'url',
        label: 'Url'
    }
  ],
  construct: function(self, options) {

    //load third party styles and scripts?
    var superPushAssets = self.pushAssets;
       self.pushAssets = function() {
       superPushAssets();
       self.pushAsset('stylesheet', 'autocomplete', { when: 'always' });
       self.pushAsset('script', 'autocomplete', { when: 'always' })
     };

    //get the data
    self.pageBeforeSend = function(req, callback) {

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

        self.apos.docs.find(req, criteria).sort({ title: 1 }).toArray(function(err, autocomplete) {
            if (err) {
                return callback(err);
            }
            //Can I do something like this to load the widget data?
            req.data.autocomplete = autocomplete;

            return callback(null);

        });
    }
}
};

lib\modules\search-widget\views\search.html

<div id="custom-search-input">
    <div class="input-group col-md-12">
        <input type="text" class="form-control input-lg" id="site-search" placeholder="Search" />
        <span class="input-group-btn">
            <button class="btn btn-info btn-lg" type="button">
                <i class="glyphicon glyphicon-search"></i>
            </button>
       </span>
    </div>
</div>

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

  {% extends 'apostrophe-templates:layout.html' %} {% block bodyClass %}{{ super() }} home-page{% endblock %} {% block title %}Help Site: {{ data.page.title | e }}{% endblock %} {% block main %}
    <section id="promo" class="promo section offset-header">
        <div class="container text-center">
            <div class="row">
                <div class="col-md-offset-2 col-md-8 col-sm-6 col-xs-12">
                    <!--search widget-->
                    {{ apos.singleton(data.page, 'search', 'search-widget') }}
                </div>
            </div>
        </div>
    </section>

Page

Question #2 is, in the search-widget\index.js construct function, is this the correct way to load the third party assets that will be needed to render it to the dom and use it?

Question #3 is, in search.html can I simply use data.page to send the data to the jquery-autocomplete object?

<div id="custom-search-input">
    <div class="input-group col-md-12">
        <input type="text" class="form-control input-lg" id="site-search" placeholder="Search" />
        <span class="input-group-btn">
            <button class="btn btn-info btn-lg" type="button">
                <i class="glyphicon glyphicon-search"></i>
        </button>
    </span>
    </div>
</div>
<script>  
 $(function() {
    $("#site-search").autocomplete({
            source: {{ data.autocomplete | json }}
        }); 
  });
</script>
lance-p
  • 1,050
  • 1
  • 14
  • 28

1 Answers1

1

As you know I'm the head of the Apostrophe team at P'unk Avenue.

First, a module that provides widgets should have a name ending in "-widgets", not "-widget." It must be plural (MAP - Modules Are Plural). This is because there will be a single instance of the module that manages all the server-side needs of the widgets (this is known as the "singleton pattern"). It's not "one widget," it's a module that manages all the widgets of this kind.

Second, adding code to pageBeforeSend as you are now will run it at all times — no matter if this page contains the widget or not. That's not optimal for performance by a long shot. Especially since you are loading all the blog posts on the site. Things will slow down quick.

I strongly recommend that you look at the apostrophe-twitter-widgets module and its source code. Among other things, this module implements a server-side Express route specifically to supply data to the widget, when and if there is actually an instance of the widget on the page and it wants to know. And it implements a "widget player," a method that is invoked on the browser side when and if there is a widget of that kind on the page.

That module also contains some custom browser-side assets and pushes them correctly, which would also answer some of the questions I can see just below the surface of this question.

However, please note that as it happens, Apostrophe already always pushes jquery autocomplete to the browser. That happens to be the autocomplete implementation we use for joins and tags. So you don't need to worry about pushing that asset.

Your server-side route ought to call the autocomplete cursor filter rather than fetching Everything In The World All The Time. (: Apostrophe uses autocomplete a lot, so it already provides a convenience for fetching things that match a prefix in a smart and well-implemented way.

If you're unclear on cursor filters be sure to read the HOWTOs on working with the model layer.

Also, always use the find method of the appropriate module, don't reinvent it and try to figure out for yourself what makes a blog post appropriate to return.

Your route might look like:

// Inside the construct function of your module...

self.route('get', 'autocomplete', function(req, res) {
  return self.apos.docs.getManager('apostrophe-blog')
    .find()
    .autocomplete(req.query.term)
    .limit(10)
    .projection({
      areas: false,
      joins: false
    })
    .toArray(function(err, posts) {
      if (err) {
        res.statusCode = 500;
        return res.send('error');
      }
      return res.send(_.map(posts, function(post) {
        return {
          value: post._url,
          label: post.title
        }
      }));
    }
  );
});

That would go inside the construct function of your module.

Now there is a GET-method Express route waiting to hear from you at /modules/search-widgets/autocomplete. Finish the job by writing a player method on the browser side. You can see what those look like and where to put them in the apostrophe-twitter-widgets code. Yours, of course, will call .autocomplete() on the text entry field in your widget's template. And it will set the jquery autocomplete source option to /modules/search-widgets/autocomplete.

Hope this is helpful!

Tom Boutell
  • 7,281
  • 1
  • 26
  • 23