Can't find why the if block not getting executed - javascript

I'm a newbie in javascript, i've been following a youtube video of creating a simple project like booklist app using javascript the tutorial is very well explained but when i tried to do it myself i got stuck at one point i can't figure out what's happening
The project is basically about when i submit the details of the book it will be added to the table in the webpage, also it will stored in the local storage too. same like that i need to remove the details of the book from local storage when it is removed from the table.
Here is the code for setting up the class Store with methods getBooks for getting the books from the local storage, addBook for adding new book to local storage, removeBook for removing the book from local storage
class Store{
static getBooks(){
let books;
if (localStorage.getItem('books') == null) {
books = [];
} else{
books = JSON.parse(localStorage.getItem('books'));
}
return books;
}
static addBook(Book) {
const books = Store.getBooks();
books.push(Book);
localStorage.setItem('books', JSON.stringify(books));
}
static removeBook(isbn){
const books = Store.getBooks();
books.forEach((book, index) => {
if (book.isbn === isbn) {
books.splice(index, 1);
}
});
localStorage.setItem('books', JSON.stringify(books));
}
}
The methods getBooks and addBooks are working perfectly fine, but the removeBook method is not working in a way that i wanted.
Here is how i invoked the method,
document.querySelector('#book-list').addEventListener('click', (e) => {
// Delete book from the table in interface
UI.deleteBook(e.target);
Store.removeBook(e.target.parentElement.previousElementSibling.textContent);
});
e.target.parentElement.previousElementSibling.textContent is getting the correct value i needed, so i did made the call to removeBook successfully but i can't pass through the if block inside the method
Here is my complete HTML script
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Book List</title>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.3/css/all.css" integrity="sha384-SZXxX4whJ79/gErwcOYf+zWLeJdY/qpuqC4cAa9rOGUstPomtqpuNWT9wdPEn2fk" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch#4.5.2/dist/yeti/bootstrap.min.css" integrity="undefined" crossorigin="anonymous">
</head>
<body>
<div class="container mt-4">
<h1 class="display-4 text-center">
<i class="fas fa-book-open text-primary"></i> My<span class="text-primary">Book
</Myspan>List</h1>
<form class="book-form">
<div class="form-group">
<label for="title">Title</label>
<input type="text" id="title" class="form-control" autocomplete="off">
</div>
<div class="form-group">
<label for="author">Author</label>
<input type="text" id="author" class="form-control" autocomplete="off">
</div>
<div class="form-group">
<label for="isbn">ISBN#</label>
<input type="text" id="isbn" class="form-control" autocomplete="off">
</div>
<input type="submit" value="Add Book" class="btn btn-primary btn-sm">
</form>
<table class="table table-striped mt-5">
<thead>
<th>Title</th>
<th>Author</th>
<th>ISBN</th>
<th></th>
</thead>
<tbody id="book-list"></tbody>
</table>
</div>
<script type="text/javascript" src="app.js"></script>
</body>
</html>
Here is my complete javascript code,
class Book{
constructor(title, author, isbn){
this.title = title;
this.author = author;
this.isbn = isbn;
}
}
class UI{
static displayBooks(){
const books = Store.getBooks();
books.forEach((book) => UI.addBookToList(book))
}
static addBookToList(book){
const list = document.querySelector("#book-list");
const row = document.createElement('tr');
row.innerHTML = `
<td> ${book.title} </td>
<td> ${book.author} </td>
<td> ${book.isbn} </td>
<td>X</td>
`;
list.appendChild(row);
}
static deleteBook(el){
if(el.classList.contains('delete')){
el.parentElement.parentElement.remove();
}
}
static showAlert(message, className) {
const div = document.createElement('div');
div.className = `alert alert-${className}`;
div.appendChild(document.createTextNode(message));
const container = document.querySelector('.container');
const form = document.querySelector('.book-form');
container.insertBefore(div, form);
setTimeout(()=>
document.querySelector('.alert').remove(),
3000
);
}
static clearFields() {
document.querySelector('#title').value = '';
document.querySelector('#author').value = '';
document.querySelector('#isbn').value = '';
}
}
class Store{
static getBooks(){
let books;
if (localStorage.getItem('books') == null) {
books = [];
} else{
books = JSON.parse(localStorage.getItem('books'));
}
return books;
}
static addBook(Book) {
const books = Store.getBooks();
books.push(Book);
localStorage.setItem('books', JSON.stringify(books));
}
static removeBook(isbn){
const books = Store.getBooks();
books.forEach((book, index) => {
if (book.isbn.toString() === isbn.toString()) {
books.splice(index, 1);
}
});
localStorage.setItem('books', JSON.stringify(books));
}
}
document.addEventListener("DOMContentLoaded", UI.displayBooks());
document.querySelector('.book-form').addEventListener('submit', (e) => {
e.preventDefault();
const title = document.querySelector('#title').value;
const author = document.querySelector('#author').value;
const isbn = document.querySelector('#isbn').value;
if (title === '' || author === '' || isbn === '') {
UI.showAlert("Please fill in all fileds", "danger");
} else {
const book = new Book(title, author, isbn);
UI.addBookToList(book);
Store.addBook(book);
UI.clearFields();
UI.showAlert("Succefully added", 'success');
}
});
document.querySelector('#book-list').addEventListener('click', (e) => {
Store.removeBook(e.target.parentElement.previousElementSibling.textContent);
UI.deleteBook(e.target);
UI.showAlert("Succefully removed", 'success');
});
I spent one and half hour to figure out what's wrong in the code but i still can't, I'm completely new to javascript.

The problem is that in your HTML you pad the book ISBN (and other fields) with spaces:
row.innerHTML = `
<td> ${book.title} </td>
<td> ${book.author} </td>
<td> ${book.isbn} </td>
<td>X</td>
`;
This means that the textContent of those td elements will not match with the properties of your book object. Either trim what you get from textContent, or just remove those spaces from your HTML:
row.innerHTML = `
<td>${book.title}</td>
<td>${book.author}</td>
<td>${book.isbn}</td>
<td>X</td>
`;
If your goal was to give these texts a bit of margin, then do that with CSS styling instead.
There are also 2 other issues I bumped into:
Your HTML has </Myspan>, which should be </span>.
You don't correctly set the handler for the DOMContentLoaded event. The argument should be a function, but you actually execute a function instead (immediately). So remove the parentheses at the end:
document.addEventListener("DOMContentLoaded", UI.displayBooks);

Related

why does vue recycle elements?

I'm using vue, and I found a bizarre behaviour while working on one of my projects.
When I update an array in javascript the items are put inside the old html elements (I suppose) so if these old elements have some particular attributes the new items are going to get them as well.
I'll put this example code (visually it sucks but that's not the point).
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue#3" defer></script>
<script src="script.js" defer></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div class="day container">
<div class="selection" v-for="day in daysList">
<input type="radio" class="radio-day" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" #click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div class="hour-container">
<div v-for="hour in hoursList" class="hour" :id="returnTheInput(hour)" #click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>
Here's the script:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
selectedDay = day;
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i++){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
To see my point:
select a day
select an hour (click on it)
select the other day
By doing this the hour will still be selected, even though it will be from the new ones.
That's not what I had expected nor what I'd want. I thought the new items would be assigned to completely new html elements.
How do I avoid this? I could change the internal logic of my script, but I was wondering if there was another way. Ideally I'd want Vue to create new html elements for the new items (since I guess it's recycling the old ones).
There are at least 2 solutions for this.
The first is to assign an unique key to each child with the :key attribute:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
selectedDay = day;
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i++){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue#3.2.37/dist/vue.global.js"></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div class="day container">
<div class="selection" v-for="day in daysList">
<input type="radio" class="radio-day" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" #click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div class="hour-container">
<div v-for="hour in hoursList" :key="hour" class="hour" :id="returnTheInput(hour)" #click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>
The second is to reset child elements then re-render them asynchronously with the nextTick utility:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
vm.hoursList = [];
selectedDay = day;
Vue.nextTick(() => {
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i++){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
});
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue#3.2.37/dist/vue.global.js"></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div class="day container">
<div class="selection" v-for="day in daysList">
<input type="radio" class="radio-day" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" #click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div class="hour-container">
<div v-for="hour in hoursList" class="hour" :id="returnTheInput(hour)" #click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>

Array isn't getting overridden in local storage

I'm creating a to-do list project. where I'm trying to delete a todo when the user clicks on the delete button. I'm using the array's filter method to remove that clicked todo. But when I refresh, that deleted todo comes back. the reason is that It's not getting removed from the local storage. There's something wrong with the event listener at the very bottom of the javascript file. I'm trying to overrides the array with whatever filter method returns and saving it to the local storage but still it doesn't work.
Javascript file
import Todo from './todo.js';
import './style.css';
const TODO_LIST_KEY = 'TODO_LIST_KEY';
const template = document.querySelector('#list-item-template');
const todoListContainer = document.querySelector('#list');
const form = document.querySelector('.form');
const inputField = document.querySelector('#todo-input');
const loadList = () => {
const dataInStringFormat = localStorage.getItem(TODO_LIST_KEY);
return JSON.parse(dataInStringFormat) || [];
};
const renderTodo = (todo) => {
console.log("I'm inside of renderTodo Method");
const templateClone = template.content.cloneNode(true);
const taskContent = templateClone.querySelector('[data-list-item-text]');
taskContent.innerText = todo.description;
const checkBox = templateClone.querySelector('[data-list-item-checkbox]');
checkBox.checked = todo.completed;
checkBox.addEventListener('change', () => {
todo.completed = checkBox.checked;
saveList();
});
const listItem = templateClone.querySelector('.list-item');
listItem.dataset.todoIndex = todo.index;
todoListContainer.appendChild(templateClone);
};
let todoList = loadList();
todoList.forEach((todo) => renderTodo(todo));
const saveList = () => {
localStorage.setItem(TODO_LIST_KEY, JSON.stringify(todoList));
};
const clearField = () => {
inputField.value = '';
};
form.addEventListener('submit', () => {
if (inputField.value === '') return;
// Create a new Todo
const todoTemplate = new Todo(todoList.length, inputField.value, false);
todoList.push(todoTemplate); // new todo gets added to the list
renderTodo(todoTemplate); //Here it adds that new todo to the list
saveList();
clearField();
});
todoListContainer.addEventListener('click', (e) => {
if (!e.target.matches('[data-button-delete]')) return;
// Get the todo that is clicked on
const parent = e.target.closest('.list-item');
const todoIndex = parent.dataset.todoIndex;
// const todoItem = todoList.find((t) => t.index === todoIndex);
parent.remove(); // removes from the screen
todoList = todoList.filter((todo) => todo.index !== todoIndex);
saveList();
});
HTML File
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To Do List</title>
</head>
<body>
<section class="todo-container">
<ul class="todo-list" id="list">
<li class="heading">
<h3>Today's To Do</h3>
<img
src="https://img.icons8.com/ios-glyphs/30/000000/refresh--v1.png"
alt="refresh-icon"
class="refresh-icon"
/>
</li>
<li>
<form class="form">
<label for="todo-input">
<input
type="text"
id="todo-input"
placeholder="Add to your list..."
/>
</label>
</form>
</li>
</ul>
<article class="footer">
Clear all completed
</article>
</section>
<template id="list-item-template">
<li class="list-item">
<label class="list-item-label">
<input type="checkbox" data-list-item-checkbox />
<span data-list-item-text></span>
</label>
<img
data-button-delete
src="https://img.icons8.com/ios-glyphs/30/000000/trash--v1.png"
alt="delete-icon"
class="delete-icon"
/>
</li>
</template>
</body>
</html>
I'm guessing that todo.index is a number. dataset values are always strings, so todo.index !== todoIndex will always be true when todo.index and todoIndex are different types.
Set todoIndex to an integer:
const todoIndex = parseInt(parent.dataset.todoIndex);

Cannot read property 'name' of undefined. Shopping cart

I'm trying to implement a simple shopping cart with JS.
$('.addToCart').click(function(event) {
event.preventDefault();
var name = $(this).data('name');
var price = Number($(this).data('price'));
cart.addProduct(name, price, 1);
});
and here is my cart object
var cart = (function () {
cartStorage = [];
function Product(name, price, count) {
this.name = name;
this.price = price;
this.count = count;
}
var object = {};
object.addProduct = function (name, price, count) {
for (var i in cartStorage) {
if (cartStorage[i].name == object.name) {
cartStorage[i].count++;
return;
}
}
var newItem = Product(name, price, count);
console.log(newItem);
cartStorage.push(newItem);
}
It prints undefined when I'am trying to console log it. And if I click one more time on a button it says 'Cannot read property 'name'.
here is my typical item card block
<div class="col-8 col-md-4 col-lg-3 card">
<img src="https://picsum.photos/200?random=9">
<div class="priceWrapper">
<input type="button" value="BUY" class = "addToCart" data-name = "Product #8" data-price = "0.001">
<p class="newPrice">0.001$ </p>
<p class="oldPrice"><strike>300$</strike></p>
</div>
<p class="item-name">Product #8 </p>
</div>
Try this
var newItem = new Product(name, price, count);
Use keyword new for new object from object constructor function.
you're doing this: var object = {}, and a few lines later, this: object.name.
Of course you'll get an error... I do not know what you were trying to do, but you have to initialize that object first to access any properties on it...
And as a side not, naming a variable "object" is not a really good idea, try to give your variables more meaningful names. It would make your code more readable both to you and to other developers coming after you.
I hope below code can give you some hints...
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.4.1.min.js">
</script>
</head>
<body>
<div class="col-8 col-md-4 col-lg-3 card">
<img src="https://picsum.photos/200?random=9">
<div class="priceWrapper">
<input type="button" value="BUY" class="addToCart" data-name="Product #8" data-price="0.001">
<p class="newPrice">0.001$ </p>
<p class="oldPrice">
<strike>3$</strike>
</p>
</div>
<p class="item-name">Product #8 </p>
</div>
<div class="col-8 col-md-4 col-lg-3 card">
<img src="https://picsum.photos/200?random=9">
<div class="priceWrapper">
<input type="button" value="BUY" class="addToCart" data-name="Product #9" data-price="0.002">
<p class="newPrice">0.002$ </p>
<p class="oldPrice">
<strike>2$</strike>
</p>
</div>
<p class="item-name">Product #8 </p>
</div>
<script type="text/javascript">
var cart = (function () {
const cartStorage = [];
function Product(name, price, count) {
this.name = name;
this.price = price;
this.count = count;
}
var object = {};
object.addProduct = function (name, price, count) {
var isProductAlredyInCart = false
for (var i in cartStorage) {
if (cartStorage[i].name == name) {
cartStorage[i].count++;
isProductAlredyInCart = true;
}
}
if (!isProductAlredyInCart) {
var newItem = new Product(name, price, count);
cartStorage.push(newItem);
}
console.log(cartStorage);
}
return object;
})();
$('.addToCart').click(function (event) {
event.preventDefault();
var name = $(this).data('name');
var price = Number($(this).data('price'));
cart.addProduct(name, price, 1);
});
</script>
</body>
</html>
You need to return object from the cart function, suggestion from #Huỳnh Tuân is perfect.
I would personally suggest first you sharp your JS basics, like how new works ?, closures etc.

Uncaught ReferenceError: weight is not defined at HTMLButtonElement.letsCalculateBMI

I have this task where I am to build a BMI calculator on specific instructions. I seem to have followed all the instructions except one in letsCalculateBMI. The instruction says:
letsCalculateBMI and get it to obtain the selected value from the SELECT element, pass that value to a getSelectedUser function call, which should return the user object for the selected value. This user object should be assigned to a user variable.
My confusion stems from how to getSelectedUser function call inside letsCalculateBMI to return the user object and the user object assigned to a user.
For a quicker view in computeBMI arrow function, the user parameter is an immediately destruct to weight, height, and country properties.
Currently the error I have is Uncaught ReferenceError: weight is not defined at HTMLButtonElement.letsCalculateBMI
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Mini App</title>
</head>
<body>
<div class="select">
<select class="select-text">
<option disabled selected>Select User</option>
</select>
</div>
<div class="details mdc-elevation--z3">
<p>
<span class="prop" data-age>Age :</span>
<span class="value" data-age-value>23 years</span>
</p>
<p>
<span class="prop" data-height>Height :</span>
<span class="value" data-height-value>169cm</span>
</p>
<p>
<span class="prop" data-weight>Weight :</span>
<span class="value" data-weight-value>68kg</span>
</p>
<p>
<span class="prop" data-gender>Gender :</span>
<span class="value" data-gender-value>Female</span>
</p>
<p>
<span class="prop" data-country>Country :</span>
<span class="value" data-country-value>Nigerian</span>
</p>
</div>
<button id="oracle" class="mdc-button" onclick="letsCalculateBMI()">
Calculate BMI
</button>
<div id="outcome">
<h5 class="mdc-typography--headline5">
BMI
</h5>
<p class ="bmi-text"></p>
</div>
<script>
const users = [];
const countriesWithLowerBmi = ["Chad", "Sierra Leone", "Mali", "Gambia", "Uganda", "Ghana", "Senegal", "Somalia", "Ivory Coast", "Isreal"];
const featToMeter = 0.3048;
const bmiCountryRatio = 0.82;
const computeBMI = ({weight, height, country}) => {
const heightInMeters = height * featToMeter;
let BMI = weight / (heightInMeters^2);
if (countriesWithLowerBmi.includes(country))
BMI *= bmiCountryRatio;
return Math.round(BMI, 2);
};
const getSelectedUser = (userId) => {
return users.find(({id}) => id === userId);
};
const displaySelectedUser = ({target}) => {
const user = getSelectedUser(target.value);
const properties = Object.keys(user);
properties.forEach(prop => {
const span = document.querySelector(`span[data-${prop}-value]`);
if(span) {
span.textContent= user[prop];
}
})
}
const letsCalculateBMI = () => {
const value = document.querySelector('.select-text').value;
getSelectedUser(value);
const user = {weight, height, country}
const bmi = computeBMI(user);
document.querySelector('.bmi-text').innerHTML = bmi
};
const powerupTheUI = () => {
const button = document.querySelector('#oracle');
const select = document.querySelector('.select-text');
select.addEventListener('change', displaySelectedUser);
button.addEventListener('click',letsCalculateBMI);
};
const displayUsers = (users) => {
users.forEach(user => {
const select = document.querySelector('.select-text');
const option = document.createElement('option');
option.text = user.name;
option.value = user.id;
select.appendChild(option);
});
};
const fetchAndDisplayUsers = () => {
users.push(
{
age: 40,
weight: 75,
height: 6,
country: 'Nigeria',
name: 'Charles Odili',
id: 'dfhb454768DghtF'
},
{
age: 23,
weight: 68,
height: 6,
country: 'Nigeria',
name: 'Simpcy',
id: 'gibb12erish'
}
);
displayUsers(users);
};
const startApp = () => {
powerupTheUI();
fetchAndDisplayUsers();
};
startApp();
</script>
</body>
</html>
The error tells you all you need to know: weight isn't defined because, well, you haven't defined it (same goes for height and country). These are properties of the user, so you need to get them from the user object returned by getSelectedUser.
For example:
user = getSelectedUser(value);
computeBMI(user.weight, user.height, user.country);
This should fix your problem, but....
For a quicker view in computeBMI arrow function, the user parameter is an immediately destruct to weight, height, and country properties.
In my opinion, this isn't good OOP design - you've already got an object with all that info; why the need to write more code to split it up?
What I'd rather do would be something like this:
...
computeBMI(getSelectedUser(value));
...
const computeBMI = (user) => {
const heightInMeters = user.height * featToMeter;
let BMI = user.weight / (heightInMeters^2);
if (countriesWithLowerBmi.includes(user.country))
BMI *= bmiCountryRatio;
return Math.round(BMI, 2);
};
letsCalculateBMI should be:
const letsCalculateBMI = () => {
const value = document.querySelector('.select-text').value;
const user = getSelectedUser(value);
const bmi = computeBMI(user);
document.getElementById("bmi-text").innerHTML = bmi;
};

how to filter/search list items and markers

The objective is to filter the display of list elements and corresponding markers.I'm unable to understand what is wrong with the logic. The search input should filter and when you undo/cancel the search input then the list should reappear with the markers.
HTML:
enter <html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Neighborhood Map</title>
<link href="style.css" rel="stylesheet">
<script src="js/jquery-3.2.1.js"></script>
<script src="js/knockout-3.4.0.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div id="sidebar" class="col-xs-12 col-sm-5 col-md-3">
<h1 id="header">Chennai City Cultural Hubs</h1>
<div class="search-box">
<input class="text-search" type="text" placeholder="Enter here" data-
bind="textInput: query">
</div>
<div class= "list-box">
<div class="menu" data-bind="foreach: filteredItems">
<a class="menu-item"data-bind="text: title, click: $parent.setLoc" >
</a>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-8">
<div id="map"></div>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>
JS:
function appViewModel() {
var self = this;
this.query = ko.observable('');
var filter = self.query().toLowerCase();
this.locationArray = ko.observableArray([]);
locations.forEach(function (item) {
self.locationArray().push(item);
});
self.setLoc = function (clickedLoc) {
var clickedData = clickedLoc.marker;
google.maps.event.trigger(clickedData, 'click')
};
this.filteredItems = ko.computed(function () {
if (!this.filteredItems) {
return self.locationArray();
} else {
return ko.utils.arrayFilter(self.locationArray(), function (item) {
var result = (item.title.toLowerCase().indexOf(filter) > -1)
item.marker.setVisible(result);
return result;
});
}
}, self);
};
ko.applyBindings(new appViewModel());
An explanation and solution would be very helpful.
Issues:
Your filter variable is not observable, so it won't update after instantiation. It's always an empty string, since by the time you assign it, query() returns "".
Checking for !this.filteredItems in the computed does not do anything, because it will never be false. (filteredItems is a ko.computed instance, which will evaluate to true)
Solution
You can rewrite your filteredItems to:
this.filteredItems = ko.computed(function () {
var q = this.query().toLowerCase();
if (!q) {
// TODO (?): reset all `setVisible` props.
return this.locationArray();
}
return this.locationArray()
.filter(function(item) {
var passedFilter = item.title.toLowerCase().indexOf(q) > -1;
item.marker.setVisible(passedFilter);
return passedFilter;
});
}, self);
By calling query you create a dependency to any changes in the search input. By calling locationArray you ensure updates when the data source changes. Note that you'll need to make sure your setVisible logic executes, even when you clear the query...
Remarks & tips
If you want, you can swap out Array.prototype.filter with ko.utils.arrayFilter, but the .filter method is widely supported by now.
You can create more (pure) computeds to separate logic. (E.g.: const lowerQuery = ko.pureComputed(() => this.query().toLowerCase()))
I wouldn't call setVisible in the filter since it's an unexpected side effect. Why not make it computed as well?

Categories