React Native Firebase search listview - javascript

I have a listview that is hydrated from my Firebase in my React Native app. I have came across a few tutorials on how people search listview's with the fetch function but nothing for Firebase.
I have a search bar at the header of my listview, how can I search by keyword and filter the results using Firebase?
I followed the guide on Firebase's website to setup my listview and datasource. I want to be able to search by keyword in each of those fields
listenForItems(itemsRef) {
itemsRef.on('value', (snap) => {
var items = [];
snap.forEach((child) => {
items.push({
title: child.val().title,
category: child.val().category,
description: child.val().description,
});
});
this.setState({
dataSource: this.state.dataSource.cloneWithRows(items)
});
});
}

I finally figured this out. This code searches based on a specific item name.
Database
{
"directory" : {
"macintosh" : {
"address" : "117 East 11th Street, Hays, KS 67601",
"firstLetter" : "m",
"title" : "Macintosh",
},
"apple" : {
"address" : "117 East 11th Street, Hays, KS 67601",
"firstLetter" : "a",
"title" : "apple",
},
},
ListView w/Search Bar
render() {
return (
<View style={styles.container}>
<ScrollView>
<SearchBar
returnKeyType='search'
lightTheme
placeholder='Search...'
onChangeText={(text) => this.setState({searchText:text})}
onSubmitEditing={() => this.firstSearch()}
/>
{
this.state.loading &&
<ActivityIndicator
size="large"
color="#3498db"
style={styles.activityStyle}
/>
}
<ListView
dataSource={this.state.dataSource}
renderRow={this._renderItem.bind(this)}
enableEmptySections={true}
/>
</ScrollView>
</View>
);
}
Search Function
firstSearch() {
this.searchDirectory(this.itemsRef);
}
searchDirectory(itemsRef) {
var searchText = this.state.searchText.toString();
if (searchText == ""){
this.listenForItems(itemsRef);
}else{
itemsRef.orderByChild("searchable").startAt(searchText).endAt(searchText).on('value', (snap) => {
items = [];
snap.forEach((child) => {
items.push({
address: child.val().address,
firstLetter: child.val().firstLetter,
title: child.val().title,
});
});
this.setState({
dataSource: this.state.dataSource.cloneWithRows(items)
});
});
}
}
This searches the firebase by replacing the listview datasource by using the orderbychild.startat().endat() firebase function. It's the closest thing to a "SELECT * BY %LIKE%" sql search functions.
I modified my code to search by the first letter as well. So incase someone searches for "Macbuk", it will still show the "Macbook" result. It only searches the M. I added another field in my database to the first letter of the title to search and made it lowercase.

Related

How to make unique groups into MUI's Autocomplete?

I am using MUI's Autocomplete to group my suggestions inside of search Bar.
I think it seems to be simple but not .
Firstly please look at the shape of my data
[{
"id" : 1,
"bookName" : "it starts with us",
"author" : "Colin Hoover"
},
{ "id" : 2,
"bookName" : "it ends with us",
"author" : "Colin Hoover"
},
{ "id" : 3,
"bookName" : "The great Gatsby",
"author" : "Scott Fitzgerald"
},
{ "id" : 4,
"bookName" : "To Kill a mocking bird",
"author" : "Harper Lee"
},
{ "id" : 5,
"bookName" : "Remainders of Him",
"author" : "Colin Hoover"
}, ... ]
now you can see, there are 3 books written by 'Colin Hoover', placed at different Ids. What I want to achieve is, my MUI's autocomplete should render
Colin Hoover //author name as heading and books underneath it
Remainders of Him
It Ends with us
It starts with us
... other books from this specific author
into my groupBy attribute in MUI's Autocomplete.
Issue I am having now - When I arrange the books which are meant for a certain author in consecutive Id's (say, id: 1, 2, 3, 4 ..) , the group-by of autocomplete works fine (i.e, it groups the different books under the same author if the author occurs at consecutive ids). But, when I arrange them in random fashion ( ie, id 1,3, 5, 8, 9 ..occupies books under same name "Colin Hoover"), then it doesn't group any of it rather it creates duplicate headers as "Colin Hoover" into autosuggestion list, which I don't want.
ALl I want is same header and all of his/her written books under his name into MUI's autocomplete.
Here's my Code
imports ...
export default function JobSearch () {
const [ books, setBooks ] = useState<Book[]>([]);
const [ searchTerm, setSearchTerm ] = useState<string>('');
// using redux
const dispatch = useAppDispatch();
const { status } = useAppSelector ( state => state.menu );
useEffect(() => {
agent.Books.getBooks() // my endpoint for getting the books
.then((books) => { setBooks(books);
console.log(books)})
.catch((error) => console.log(error))
}, []);
const suggestions = books.map((book) => {
const bookDetail = `${book.name} by ${book.author}`
return {
bookDetail,
...book
}});
const handleChangeInput = ( event : any ) => {
setSearchTerm( event.target.value);
console.log(event.target.value);
}
return (
<Fragment>
<Autocomplete
open = { status.name === "autoCompleteMenu" && status.open }
autoHighlight = {true}
autoComplete = {true}
noOptionsText = {`Search for ${searchTerm}`}
onOpen = { () => {
dispatch(setOpenMenu("autoCompleteMenu"));
}}
onClose = { () => dispatch(setCloseMenu("autoCompleteMenu"))}
options = {suggestions.sort((a, b) => -b.bookDetail.localeCompare(a.bookDetail))}
isOptionEqualToValue = {(option, value) => option.bookDetail === value.bookDetail}
groupBy = {(option) => option.author} // I want all books sorted under same author
getOptionLabel={(option) => option.bookDetail}
renderInput = {(params) => (
<TextField { ...params}
label = {'search ...'}
onChange = {handleChangeInput}
variant = {'outlined'} /> )} />
</Fragment>
)
}
I know I have to implement 'sort' into groupBy, but how to do the same, so as to sort all the books according to the author name and pass it as a parameter of the groupBy attribute.
All Suggestions are welcome and highly appreciated.

javascript filter returning empty array even if the values exist in array object being searched

I am having difficulty using the javascript filter in a react native project. I need help filtering an array of objects in a typescript react native project
The main issue I am having is in the function called onSearchTextChanged specifically the search method which uses a javascript filter
Here's the main code for the screen below
const renderItem = ({ item }) => (
<Pressable style={cardUtilities} android_ripple={{ color: "#dddddd" }} onPress={() => onTapItemHandler(item)}>
<View style={main}>
<Text style={utilityname}>{item.title}</Text>
<Text style={utilityDesc}>{item.description}</Text>
</View>
</Pressable>
)
const onSearchTextChanged = (e) => {
setSearchQuery(e)
search()
}
const search = () => {
console.log("inside handleSearchButtonPressed")
if (!itemListStore) {
return
}
// const text = searchQuery.toLowerCase()
console.log("inside 100")
console.log(itemListStore)
// array of output objects
const filteredObjs = itemListStore.filter(
// eslint-disable-next-line array-callback-return
({ item }) => {
(JSON.stringify(item?.description[0]))?.toLowerCase().includes(searchQuery.toLowerCase()) ||
(JSON.stringify(item?.title[0]))?.toLowerCase().includes(searchQuery.toLowerCase()) ||
( JSON.stringify(item?.link[0]))?.toLowerCase().includes(searchQuery.toLowerCase())
})
console.log("filteredObjs", JSON.stringify(filteredObjs))
console.log(typeof filteredObjs)
console.log(filteredObjs.length)
console.log("inside 400")
console.log("searchQuery ", searchQuery)
if (!searchQuery || searchQuery === "") {
console.log("1")
} else if (!Array.isArray(filteredObjs) && !filteredObjs.length) {
console.log("2")
// set no data flag to true so as to render flatlist conditionally
setNoData(true)
} else if (Array.isArray(filteredObjs)) {
console.log("3")
setNoData(false)
setItemListStore(filteredObjs)
}
}
return (
<SafeAreaView style={container}>
{/* <Loader loading={loading} /> */}
<View style={fullContainer}>
<View style={MAIN_IMG}>
<Image style={$welcomeLogo} source={Images.appLogo} resizeMode="contain" />
</View>
<View style={TOP_143}>
<TextInput
style={textInputStyle}
onChangeText={onSearchTextChanged}
underlineColorAndroid="transparent"
placeholder="Search Here"
value={searchQuery}
/>
</View>
<FlatList
data={itemListStore}
renderItem={renderItem}
keyExtractor={() => {
return uuid.v4() + ""
}} />
</View>
</SafeAreaView>
)
Here is sample data made up of an array of objects to be filtered
[
{
"description":[
"At company; Professionals, we believe that individuals and organisations need to apply a whole new level of thinking to navigate and thrive in the emerging world. We no longer have the luxury of conducting business as usual. The organisation or individual that will survive in today’s world is one who can imagine and create their future.
​
For anyone who feels uneasy and boxed up in their careers, or entrepreneurs and organisations who want to stay ahead of the new era, or youths who want to be equipped for the future - we can help you achieve this."
],
"guid":[
[
"Object"
]
],
"industry":[
"Consulting"
],
"link":[
"https://www.myjobmag.com/jobs/business-development-executive-at-protege-management-2"
],
"pubDate":[
"Fri, 18 Nov 2022 17:31:56 GMT"
],
"title":[
"Business Development Executive at Protege Management"
]
},
{
"description":[
"Montaigne Place Is the leading luxury cosmetics, wellbeing and fragrance company in country. We are the hallmark of sophistication, luxury makeup, skincare innovation and sublime fragrances."
],
"guid":[
[
"Object"
]
],
"industry":[
"Sales / Retail"
],
"link":[
"https://www.myjobmag.com/jobs/business-development-manager-female-at-montaigne-ah-limited"
],
"pubDate":[
"Fri, 18 Nov 2022 17:28:02 GMT"
],
"title":[
"Job Openings at Montaigne AH Limited"
]
}]
Here are the logs output below using a searchQuery value of business
LOG filteredObjs []
LOG object
LOG 0
LOG inside 400
LOG searchQuery Business
LOG 3
I saw two errors
Your code:
const filteredObjs = itemListStore.filter(
// here you destructure the row and take item but item doesn't exist in your object
({ item }) => {
// here there is no return
(JSON.stringify(item?.description[0]))?.toLowerCase().includes(searchQuery.toLowerCase()) ||
(JSON.stringify(item?.title[0]))?.toLowerCase().includes(searchQuery.toLowerCase()) ||
( JSON.stringify(item?.link[0]))?.toLowerCase().includes(searchQuery.toLowerCase())
})
You need to do
const filteredObjs = itemListStore.filter(
(item) => {
return (JSON.stringify(item?.description[0]))?.toLowerCase().includes(searchQuery.toLowerCase()) ||
(JSON.stringify(item?.title[0]))?.toLowerCase().includes(searchQuery.toLowerCase()) ||
(JSON.stringify(item?.link[0]))?.toLowerCase().includes(searchQuery.toLowerCase())
})
Seems like you are missing the return statement in your filter callback function. You have to return the boolean result for it to work
const filteredObjs = itemListStore.filter(
// eslint-disable-next-line array-callback-return
({ item }) => {
return (JSON.stringify(item?.description[0]))?.toLowerCase().includes(searchQuery.toLowerCase()) ||
(JSON.stringify(item?.title[0]))?.toLowerCase().includes(searchQuery.toLowerCase()) ||
( JSON.stringify(item?.link[0]))?.toLowerCase().includes(searchQuery.toLowerCase())
})

Filtering function running one step too late

I'm building a component to filter an array of data.
There are 2 filters, one for the Team and one for the location. When each of the dropdowns are changed I'm changing a state variable for team and location and then running the filter function.
For some reason, the filter is running one step too late.
As an example - if I choose Team A, nothing will update. If I then choose Team B the careersDataFiltered variable will show Team A. If I choose Team C it'll then show Team B's data.
Its almost like its running one step too late.
You can see from the code that I'm running the filter after the state variables have been set which is why this is a bit of head-scratcher for me.
import React, { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
const careersData = [
{
name: "X Job Title",
url: "/url/x",
team: "Team a",
location: "London",
},
{
name: "M Job Title",
url: "/url/m",
team: "Team a",
location: "London",
},
{
name: "B Job Title",
url: "/url/b",
team: "Team c",
location: "Sheffield",
},
{
name: "A Job Title",
url: "/url/a",
team: "Team b",
location: "London",
},
{
name: "F Job Title",
url: "/url/f",
team: "Team b",
location: "Sheffield",
},
{
name: "C Job Title",
url: "/url/c",
team: "Team c",
location: "London",
},
{
name: "Q Job Title",
url: "/url/q",
team: "Team a",
location: "Sheffield",
},
]
const uniqueTeams = []
const uniqueLocations = []
// Build the unique values
if (careersData !== null) {
careersData.map(career => {
if (uniqueTeams.indexOf(career.team) === -1) {
return uniqueTeams.push(career.team)
}
})
}
if (careersData !== null) {
careersData.map(career => {
if (uniqueLocations.indexOf(career.location) === -1) {
return uniqueLocations.push(career.location)
}
})
}
// reorder ready for output
uniqueTeams.sort()
uniqueLocations.sort()
const CurrentVacancies = () => {
const [careersDataFiltered, setCareersDataFiltered] = useState(careersData)
const [filterTeam, setFilterTeam] = useState("")
const [filterLocation, setFilterLocation] = useState("")
// filter array based on values
const runFilter = () => {
// reset the filter data
setCareersDataFiltered(careersData)
if (filterTeam !== "") {
setCareersDataFiltered(
careersDataFiltered.filter(career => career.team === filterTeam)
)
}
if (filterLocation !== "") {
careersDataFiltered(
careersDataFiltered.filter(career => career.location === filterLocation)
)
}
console.log(careersDataFiltered)
}
return (
<>
<div className="flex">
<h2 className="mr-auto">Current Vacancies</h2>
<div className="">
<select
onChange={e => {
setFilterTeam(e.target.value)
runFilter()
}}
>
<option value="">Team</option>
{uniqueTeams.map(team => (
<option key={team} value={team}>
{team}
</option>
))}
</select>
<select
onChange={e => {
setFilterLocation(e.target.value)
runFilter()
}}
>
<option value="">Location</option>
{uniqueLocations.map(location => (
<option key={location} value={location}>
{location}
</option>
))}
</select>
</div>
</div>
<div>
<AnimatePresence>
{careersDataFiltered.map((career, index) => (
<motion.div
key={index}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
positionTransition
>
<div>
<div className="text-sm">
{career.name} - {career.team} | {career.location}
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</>
)
}
export default CurrentVacancies
First of all Your code has some errors, you should use setCareersDataFiltered() in second if statement of runFilter(). Also setCareersDataFiltered() is async so you can not use careersDataFiltered right after you call setCareersDataFiltered(). It's same for call to setFilterTeam() because you are using filterTeam in runFilter() so you can not call runFilter() after your call to setFilterTeam() because your data is not guaranteed to be updated. It's also true for setFilterLocation().
Put runFilter() inside useEffect() so it will call it when any dependencies of runFilter() changes.
useEffect(() => {
runFilter();
}, [runFilter]);
Also change runFilter() as follow and use useCallback() to prevent re-render loop.
// filter array based on values
const runFilter = useCallback(() => {
let filter = careersData;
if (filterTeam !== "") {
filter = filter.filter(career => career.team === filterTeam);
}
if (filterLocation !== "") {
filter = filter.filter(career => career.location === filterLocation);
}
setCareersDataFiltered(filter);
}, [filterLocation, filterTeam]);
At the end remove any manual call to runFilter();
As a hint I think your filter logic has a bug also because I can set team to A and then it only show me A but if I set location it apply location filter and ignore A filter and it's because of async issue. So if you want to apply both filters at the same time you should mix two if statements in runFilter(). I've mixed filters and wrote a new filter routine.
Here is my working demo version
Ciao, this problem could be related on how you set careersDataFiltered in function runFilter. For my personal experience, read and set state directly is never a good idea. Try to change runFilter like this:
const runFilter = () => {
let temp_careersDataFiltered = _.cloneDeep(careersData); //here you have to clone deep careersData because javascript keep reference (I use cloneDeep from lodash)
if (filterTeam !== "") {
temp_careersDataFiltered = careersData.filter(career => career.team === filterTeam)
}
if (filterLocation !== "") {
temp_careersDataFiltered = careersData.filter(career => career.location === filterLocation)
}
setCareersDataFiltered(
temp_careersDataFiltered
)
console.log(temp_careersDataFiltered)
}
Explanation: since you don't have certainty that after call setCareersDataFiltered value of careersDataFiltered is immediately available, apply filters to temp_careersDataFiltered temporary array and in the end set careersDataFiltered to re-render the component.
I'm cloning careersData because you know javascript works with reference and if you write let temp_careersDataFiltered = careersData if you modify temp_careersDataFiltered infact you are modifying careersData also.

How to apply different styles dynamically for matching text from a dynamic paragraph in React Native

I am doing React Native project. This is my first project in React Native.
And I am getting questions and answers in that (multiple questions and answers).
Each answer has multiple different styles with matching text of answer paragraph.
If any matching text there, I have to apply those styles for that selected texts, There may be different font styles, underline, url link, emails.
I have tried with following code, but, It is displaying empty data.
text: the text to search for in the answer's text
instance: the instance to match in case of multiple instances found within the text (if zero is provided, match all instances)
link: this can be a url or a mailto to use for the matched text
styles: a collection of styles to be applied to the matching text
Json Response is following
{
"question": "How do I change my pwd?",
"answer": "To change your pwd, go to the Settings section from the main menu and choose the Change Password option. The new password will be your new password, as well. Still having difficulty logging in? Please contact the Services Team would be great.",
"format": [
{
"text": "Settings",
"instance": 1,
"link": "",
"styles": {
"fontWeight": "bold"
}
},
{
"text": "Change Password",
"instance": 1,
"link": "",
"styles": {
"fontWeight": "bold"
}
},
{
"text": "Services Team",
"instance": 1,
"link": "mailto:client#gmail.com",
"styles": {
"fontStyle": "underline",
"color": "blue"
}
}
]
}
There may be format key or may not be there. But, If that key there, I have to apply different styles for matching data for answer/question data. Even if there mail id, there, I have to show underline once tap on that, It should open email. If there any any url like website, It should open website on tap of it.
I am showing this data in Flatlist
export default class Cell extends PureComponent {
state = {
isSelected: false,
}
formatedContent = (format, label) => {
let managebleLabel = label; // initialize the working text
format.map((item) => {
const { styles, link, text } = item;
console.log('item', item);
const indexOfText = managebleLabel.indexOf(text); // Get the index of our text
const workingLabel = managebleLabel.substring(0, indexOfText + text.length); // Get complete working label with our text to format
managebleLabel = managebleLabel.split(text)[1]; // This is the left label we are going to work with next
const splittedLabel = workingLabel.split(text); // on position 0 we get the label with no format and on position 1 our text to format
const simpleLabel = <Text>{splittedLabel[0]}</Text>; // create the element
const formatedLabel = link && link.length > 0 ? (
this.isLink(text, link, styles)
) : (
<Text style={typeof styles === Object ? styles : {}}>{text}</Text>
); // Assign the format to label
return (
<Text>
{simpleLabel}
{formatedLabel}
</Text>
); // Join the labels
});
};
isLink = (label, link, style) => {
return (
<TouchableOpacity
onPress={() => Linking.openURL(link)}
style={typeof style === Object ? style : {}}
>
<Text>{label}</Text>
</TouchableOpacity>
);
}
onPress = () => {
const { index, onHeaderSelected, item } = this.props;
this.setState(prevState => ({
isSelected: !prevState.isSelected,
}));
}
render() {
const { isSelected } = this.state;
const { item } = this.props;
const answer = get(faqjson, 'answer');
const formatText = get(faqjson, 'format');
return (
<View style={styles.container}>
<TouchableWithoutFeedback onPress={this.onPress}>
<View style={styles.questionContainer}>
<Text style={styles.question}>{item.question}</Text>
<Image source={isSelected ? res.images.arrow_up : res.images.arrow_down} style={styles.image} />
</View>
</TouchableWithoutFeedback>
{isSelected && (
<View style={styles.answerContainer}>
<Text style={[styles.answer]} ellipsizeMode="tail">
{this.formatedContent(formatText, get(faqjson, 'answer'))}
</Text>
</View>
)
}
</View>
);
}
}
But, It is showing empty data after mapping.
Any suggestions?

Switching between dynamic Pickers crashes app

I'm new to React Native and need to dynamically populate a Picker depending on the value of a selected Radio Button. For example, in the following code if you select "Animals" the Picker would contain "Dog", "Cat", and "Fish" and if you selected "People" the Picker would contain "Will Smith".
However, if I select "Cat" or "Fish" from Animals and then click on the People radio button, the app crashes. When debugging I saw I was getting an Array Index Out of Bounds Exception, and I'm guessing it's probably because I'm selecting index 1 or 2 in the Animals array, but there is only 1 item in the People array. It must be trying to get index 1 or 2 from the People array, but that is obviously out of bounds.
The behavior I need is for the Picker to go back to the default option "Select one" whenever I change to a different radio button.
import React, { Component } from 'react';
import { View, Picker } from 'react-native';
import RadioGroup from 'react-native-radio-buttons-group';
// Constants for currently selected radio button
const ANIMALS = 0
const PEOPLE = 1
// Constant for when nothing is selected in the Picker
const NOTHING = 'nothing'
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
selectedVal: NOTHING, // Currently selected item
radioType: ANIMALS, // Currently selected radio button
radioValues: [
{
type: ANIMALS,
label: "Animals",
},
{
type: PEOPLE,
label: "People",
},
]
}
}
getCategories = (type) => {
if (type === ANIMALS) {
return [
"Dog", "Cat", "Fish"
]
} else if (type === PEOPLE) {
return [
"Will Smith"
]
}
}
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<RadioGroup
flexDirection="row"
radioButtons={this.state.radioValues}
onPress={(data) => {
this.setState({
radioValues: data
})
let selected = this.state.radioValues.find(e => e.selected === true)
if (selected.type === ANIMALS) {
this.setState({ radioType: ANIMALS })
} else if (selected.type === PEOPLE) {
this.setState({ radioType: PEOPLE })
}
}} />
<Picker
selectedValue={this.state.selectedVal}
style={{ height: 50, width: 250 }}
onValueChange={(itemValue) =>
this.setState({ selectedVal: itemValue })
}>
<Picker.Item label="<Select one>" value={NOTHING} />
{
this.getCategories(this.state.radioType).map((categoryName, index) => {
return <Picker.Item label={categoryName} value={categoryName} key={index} />
})
}
</Picker>
</View>
)
}
}
This is what the app looks like:
I tried manually setting this.state.selectedVal back to NOTHING inside of componentDidUpdate() but the app still crashes if I select "Cat" or "Fish" from the Animals view and then switch to People. Strangely it works when I run it from a new Expo project, but not with this project where I used the react-native CLI.
Does anyone know how to fix the array index out of bounds exception?

Categories

Resources