aiden87
aiden87

Reputation: 969

search and filter with knockout.js

What i want to do is create a search that would find items that match search criteria. For example if i want to search for a book called "a song of ice and fire"... a "a song" needs to be enough to find a filter results. Search critieria is search by book title

I'm getting my data from sql server database, via mvc controller and on client-site with this method, which actually works fine and displays data in table (as i'm only testing if i get this data).

PS: The important thing here is that final result is, that i am not displaying all the books in any grid/table and doing search/filtering on that table, it all should be inbeded in search and only found by search method.

update1

self.search = function (value) {
            self.AllBooks.removeAll();
            for (var x in self.AllBooks) {
                if (self.AllBooks[x].toLowerCase().indexOf(value.toLowerCase()) >= 0) {
                    self.AllBooks.push(self.AllBooks[x]);
                }
            }
        }

Upvotes: 0

Views: 1370

Answers (3)

user3297291
user3297291

Reputation: 23372

Since you're using knockout, I'll show you a more "reactive", typical knockout way of tackling this feature.

Your viewmodel should have one, private data source property. In your case, a list of all books that never changes. You should not clear this list.

Then, in another property, you store a computed list of data (books). This list takes a filter function and applies it to the full data source.

The filter method relies on your search query (value, in your search method). Let's make it observable so we know when to trigger updates!

Here's an example with minimal changes to your original code putting it all together:

function Book(data) {
  var self = this;
  for (var key in data) {
    this[key] = ko.observable(data[key] == null ? "" : data[key]);
  }
}

function BookViewModel(data) {
  var self = this;
  self.title = ko.observable();
  
  self.searchQuery = ko.observable("");
  
  // The data source
  self.ListOfBooks = ko.observableArray([]);
  for (var i = 0; i < data.length; i++) {
    self.ListOfBooks.push(new BookList(data[i]));
  };
  
  // The filtered list
  self.searchResults = ko.pureComputed(function() {
    var query = self.searchQuery().trim().toLowerCase();
    
    if (!query) return [];
    
    return self.ListOfBooks().filter(function(list) {
      // The actual search method, easily moved or replaced
      // by some other logic
      var book = list.Book();
      
      return Object.keys(book).some(function(k) {
        return ko.unwrap(book[k]).toLowerCase().includes(query);
      })
    });
  });
}

function BookList(data) {
  var self = this;
  for (var key in data) {
    if (key != "Book")
      this[key] = ko.observable(data[key] == null ? "" : data[key]);
  }
  self.Book = ko.observable(data.Book)

}

var _allBooks = [ 
  { Book: { title: "To Kill a Mockingbird", author: "Harper Lee" }},
  { Book: { title: "Animal Farm", author: "George Orwell" }},
  { Book: { title: "The Lord of the Rings", author: "J.R.R. Tolkien" }}
];
ko.applyBindings(new BookViewModel(_allBooks == null ? new Array() : _allBooks));
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>


<input type="search" data-bind="textInput: searchQuery">
<ul data-bind="foreach: searchResults">
  <li data-bind="with: Book">
    <span data-bind="text: title"></span>, by <span data-bind="text: author"></span>
  </li>
</ul>




<div data-bind="visible: !searchQuery()" style="opacity: .5">
  <p>(Try searching for these books:)</p>
  <ul data-bind="foreach: ListOfBooks">
  <li data-bind="with: Book">
    <span data-bind="text: title"></span>, by <span data-bind="text: author"></span>
  </li>
</ul>

A second example, in which I refactored some other parts as well, move some stuff around and use some more modern js features:

function Book(data) {
  for (var key in data) {
    this[key] = ko.observable(data[key] == null ? "" : data[key]);
  }
  
  this.searchString = ko.pureComputed(() =>
    Object
      .keys(data)
      .map(k => this[k])
      .map(ko.unwrap)
      .join("||")
      .toLowerCase()
  );
};

Book.fromData = data => new Book(data);
Book.matchesQuery = query => book => book.matchesQuery(query);

Book.prototype.matchesQuery = function(query) {
  return this.searchString().includes(query.trim().toLowerCase());
};

function BookViewModel(data) {
  this.searchQuery = ko.observable("");
  
  // The data source
  this.allBooks = ko.observableArray(data.map(Book.fromData));
  
  // The filtered list
  this.searchResults = ko.pureComputed(() => {
    return this.searchQuery()
      ? this.allBooks().filter(Book.matchesQuery(this.searchQuery()))
      : [];
  });
}

var _allBooks = [ 
  { title: "To Kill a Mockingbird", author: "Harper Lee" },
  { title: "Animal Farm", author: "George Orwell" },
  { title: "The Lord of the Rings", author: "J.R.R. Tolkien" }
];

ko.applyBindings(new BookViewModel(_allBooks));
* { box-sizing: border-box; position: relative;}

.searchwidget > input {
  width: 50%;
  padding: .5rem;
  border-radius: 0;
  outline: none;
  border: 1px solid #ccc;
  -webkit-appearance: none;
}

.searchwidget ul {
  margin: 0;
  padding: 0;
  position: absolute;
  width: 50%;
  
}

.searchwidget li {
  
  margin: 0;
  padding: .5em;
  list-style: none;
  
  border: 1px solid #ccc;
  border-top-width: 0;
  background: #efefef;
  opacity: .95;
  cursor: pointer;
}

.searchwidget li:nth-child(even) {
  background: #dfdfdf;
}

.searchwidget > li:hover {
  background: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<div class="searchwidget">
  <input type="search" data-bind="textInput: searchQuery">
  <ul data-bind="foreach: searchResults">
    <li>
      <span data-bind="text: title"></span>, by <span data-bind="text: author"></span>
    </li>
  </ul>
</div>



<div data-bind="visible: !searchQuery()" style="opacity: .5">
  <p>(Try searching for these books:)</p>
  <ul data-bind="foreach: allBooks">
  <li>
    <span data-bind="text: title"></span>, by <span data-bind="text: author"></span>
  </li>
</ul>

Upvotes: 1

Sohan
Sohan

Reputation: 6809

Using Underscore JS it is more clean and efficient,

var _=require("underscore");
var ListOfBooks = ["JavaBook", "C#Book", "JavaScriptBook", "HTMLBook"];
var searchBook="Java";
var filteredBooks = _.filter(ListOfBooks, function(books) {
    return books.toLowerCase().indexOf(searchBook.toLowerCase() )>= 0;
});
console.log("filteredBooks " +JSON.stringify(filteredBooks));

Upvotes: -1

Raghunath Chary
Raghunath Chary

Reputation: 41

In the search function you are iterating over "ListOfBooks"array and adding the element matching to search criteria to same array ListOfBooks. This duplicates the elements which matches the criteria.

var ListOfBooks = ["JavaBook", "C#Book", "JavaScriptBook", "HTMLBook"];
var value = "Java";
for (var x in ListOfBooks) {
  if (ListOfBooks[x].toLowerCase().indexOf(value.toLowerCase()) >= 0) {
    ListOfBooks.push(ListOfBooks[x]);
  }
}
console.log(ListOfBooks);

If you want to get results in separate array you can do:

var ListOfBooks = ["JavaBook", "C#Book", "JavaScriptBook", "HTMLBook"];
var value = "Java";
var SearchResults = [];
for (var x in ListOfBooks) {
  if (ListOfBooks[x].toLowerCase().indexOf(value.toLowerCase()) >= 0) {
    SearchResults.push(ListOfBooks[x]);
  }
}
console.log(SearchResults);

Or if you want to retain all the elements matching the criteria you use splice method:

var ListOfBooks = ["JavaBook", "C#Book", "JavaScriptBook", "HTMLBook"];
var value = "Java";
for (var x in ListOfBooks) {
  if (ListOfBooks[x].toLowerCase().indexOf(value.toLowerCase()) < 0) {
    ListOfBooks.splice(x, 1);
  }
}
console.log(ListOfBooks);

Upvotes: 1

Related Questions