Search with the URL Query Parameters on Flow Router


#1

Hi there!

I use Search Source, and Flow Router from Arunoda. They both work great, but I’m just struggling to get them work together for a particular thing…

I have a helper that returns some documents rendered from the server-run SearchSource method like this:

Template.search.helpers({
  things: function() {
    var langParam = FlowRouter.current().queryParams.lang;
    console.log(langParam);
    return BookSearch.getData();
  }
});

As you see, I’m just trying to search for things that are registered in the language entered at the url (e.g. ‘en’) as a query parameter. Let’s say in “english” as in this example:

http://localhost:3000/search?lang=en

I can perfectly read the “en” and log on the console by the below code, but it does not work to search with.

var langParam = FlowRouter.current().queryParams.lang;
console.log(langParam);

So; although I get “en” printed on the browser console, I do not get the things that are registered in “en” language. What I need to know is how to render only the data that fetches to the condition I want (in this case english language, i.e. documents that return true this condition in Mongo: {lang: langParam}, registered in the helper function above. For that, one uses the Package.getData() API, I think, but I could not locate exactly how to write the code inside (tried but was unsuccessful)…

So how can I achieve a proper search using the query params?

Thanks!

Note: This question has also been populated on Stack Overflow. Nonetheless it has so far not been replied :disappointed: .


#2

use FlowRouter.getQueryParam('lang') for a reactive data source

Alternatively, why not subscribe just to the books in that language?

FlowRouter.route('/books', {
  subscriptions: function (params, queryParams) {
    this.register('books', queryParams.lang);
  }
});

#3

Thanks. But how do I render the relevant data accordingly? My problem is more about rendering the right data than reading the queryParams. Though, it’s nice to learn about the reactive pattern.


#4

Is there any reason not to use a client and server collection and publications? If the BookSearch is async you can do something like this.

Template.books.helpers({
  books: () => Template.instance().books.find();
});
Template.books.onCreated(function () {
  this.books = new Mongo.Collection(null);
  this.autorun(() => {
    BookSearch.getData((err, results) => {
      if (!err) {
        results.forEach(doc => {
          this.books.upsert(doc, { $set: doc });
        });
      } else {
        // handle error
      }
    });
  });
});

#5

Hmm, sorry I feel very far away from understanding your solution. I mean I think I’m too junior for successfully replacing my code with the solution you provided… :frowning:

Is there any reason not to use a client and server collection and publications?

By default, I publish the entire Books collection to all clients. Do you mean that I register in the router definition which books to publish so I don’t send everything?? Perhaps that’s a good idea…

Is there no way to register in the helper to send the relevant data?


#6

By default, I publish the entire Books collection to all clients. Do you mean that I register in the router definition which books to publish so I don’t send everything?? Perhaps that’s a good idea…

Yeah. That’s usually how meteor prefers to do things. A publish function defines some subset of the collection to add to the client. So you define the collection on both client and server. That is to say,

Books = new Mongo.Collection('books');

On the server, you define your publication.

if (Meteor.isServer) {
  Meteor.publish('books', function (language) {
    check(language, Match.Optional(String));
    if (language) {
      return Books.find({ language: language });
    } else {
      return Books.find();
    }
  });
}

Now you can also define your route. It would look as follows

FlowRouter.route('/books', {
  subscriptions: function (params, queryParams) {
    this.register('books', queryParams.lang);
  },
  action: function () {
    BlazeLayout.render('layout', { content: 'Books.list' });
  }
});

Then finally, on your client, you simply have to make your helper and use an {{#each}}{{/each}}.

Template['Books.list'].helpers({
  books: () => Books.find(),
  ready: () => FlowRouter.subsReady('books')
});

// then in your template
{{#if ready}}
  {{#each books}}
    <h1>{{title}}</h1>
    <h4>{{author}}</h4>
  {{/each}}
{{else}}
  <p>Loading books...</p>
{{/if}}

#7

I will try this! Though, it’s midnight here at the moment… :sleeping:

Thanks!


#8

Hi there again!

I only had a chance to try this out now (fulltime job :/).

So, first of all; Thanks a lot! I think I have learnt quite some with your help, although my problem still need resolving. :smile:

  1. How do we do the coupling in between the router’s
    queryParams.lang subscription and the server’s publication
    with language variable? I mean there’s no common variable/data that is fetched, therefore; how does the logic actually work, and how can I use another query param, say queryParams.category and publish it on the server?

  2. Also, I need to utilise the queryParams with the SearchSource package’s getData() api, instead of simply rendering with Books.find() in the helper as you showed. Does that refer that I need to do further alteration on the server search code (can copy here if required), especially on the server rendering of the outputs? I ask this because it does not work when I try to use the queryParam with searchable list… :confused:

  3. If I want to use a template level subscription (actually I need to otherwise it messes up), how can I subscribe for the books I want in the template with the queryparams?? Wouldn’t it be like this below? Though, it doesn’t work (again I have question marks regarding the client-server precise coupling:

    Template.searchcity.onCreated(function () {
      this.autorun(function() {
        var langParam = FlowRouter.getQueryParam("lang");
        this.subscribe('allTheBooks', langParam);
      });
    }); 
    

I’ll be so happy if you help me on these, and hope these will also serve others. Thanks!
Emin


#9

First of all, searchsource sets up necessary data delivery for you so you don’t have to, indeed should not set up publications or subscriptions for your search flow. There’s tons of literature around for how pub/sub works in Meteor so I’ll skip ahead to your searchsource problem.

I see that you want to scope your search to a certain language. Here’s a basic set up that would get you going. You should also fine tune things like throttling, metadata handling, limiting, paging, input and query param sanitization, result transformations etc.

Template

<template name="booksearch">
  <form name="booksearch"><input type="search"/></form>
  <ul>
    {{#each hits}}
      <li>{{title}}</li>
    {{#each}}
  </ul>
</template>

Client: set up your helper

var options = {
  // cache the search results for 5 minutes
  keepHistory: 1000 * 60 * 5,
  // allow fast local searches on the cache
  localSearch: true
};
// feed the search to the title field only
var fields = ['title'];
// Set up your search
BookSearch = new SearchSource('books', fields, options);

/*
  get the search results reactively. mind you, this is not an invocation.
  you'll invoke the search within your event handler down below
*/
Template.booksearch.helpers({
  hits : function() {
    return BookSearch.getData();
  }
})

Template.booksearch.events({
  'submit form': function(e,t) {
    // listen for the submit event
    e.preventDefault();
    var options = {
      // this is your lang query param from the url
      lang: FlowRouter.getQueryParam('lang')
    };
    // value of the search input from your template
    var searchText = t.$('input').val();
    // invoke the search using the input and the language
    BookSearch.search(searchText,options);
  }
})

Server: set up your search

SearchSource.defineSource('books', function(searchText, options) {
  // make sure you do have a lang option or use a default one
  var lang = options.lang || 'english'
  if(searchText) {
    var regExp = buildRegExp(searchText);
    // use the input and lang to build your mongodb selector
    var selector = {title: regExp, language: lang};
    return Books.find(selector).fetch();
  } else {
    // don't return anything if nothing is searched for
    return [];
  }
});

function buildRegExp(searchText) {
  // copied over from the naive github example
  var parts = searchText.trim().split(/[ \-\:]+/);
  return new RegExp("(" + parts.join('|') + ")", "ig");
}

I think this should cover it. Let me know if this works for you. I’ll also copy this over to SO if you want this answer set in stone :wink:


#10

You’re awesome @serkandurusoy! Thank you so much!!!

What I’ve learn (that I didn’t know before) is that I need to pass variables with same name from client to server when I use such API, in this case:

BookSearch.search(x,y) ––> SearchSource.defineSource('books', function(x,y) {//code here}.

Therefore I can grasp them in the server (x & y) and use them. Great! Another day of enlightenment with Meteor. Wuhu! :heart_eyes:


#11

I’m glad it works for you.

A small correction, though, passing variables cares about the order, not the name.

So you can do x=2; y=3; myMethod(x,y) on function myMethod(a,b) {return a + b} and receive back 5!


#12

Aha, I see. Wow. Smart kinda. That’s also great to know. Thanks!