React.js search filtration on the array of objects - javascript

I'm facing a problem with the search. It is a front-end search rather than a remote search, I'm using react.js because it is a requirement in the problem and created a component named App. My task is to display and highlight the matching parts according to the type value.
I will appreciate it. If you provide me a good solution for this.
Let me tell you the whole scenario. I'm dividing this problem into 3 parts.
Part 1: What is the shape of the data?
The shape of the data is this:
src/data.js:
export default [
{
id: 1,
name: 'Wordpress',
list: [
{
id: 1,
name: 'Best Mobile App Builder',
slug: '/'
},
{
id: 2,
name: 'Best Wordpress Themes',
slug: '/'
},
{
id: 3,
name: 'Best Website Creator',
slug: '/'
},
{
id: 4,
name: 'Best Wordpress Builder',
slug: '/'
}
]
},
{
id: 2,
name: 'SaaS',
list: [
{
id: 1,
name: 'Appointment Scheduling Software',
slug: '/'
},
{
id: 2,
name: 'Design Services',
slug: '/'
},
{
id: 3,
name: 'Online Cloud Storage',
slug: '/'
},
{
id: 4,
name: 'Remote PC Access',
slug: '/'
}
]
},
];
Note:
Basically this is my filter function.
src/filter.js:
import _ from 'lodash';
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
/**
* Returns the new filtered array with highlighted parts.
* #param data {Array<Object>} - The collection to iterate over.
* #param inputValue {string} - The input value.
* #return {Array} - Returns the new filtered array.
*/
export const filterByNames = (data, inputValue) => {
// Create a dynamic regex expression object with ignore case sensitivity
const re = new RegExp(_.escapeRegExp(inputValue), 'i');
const results = data.filter((object) => {
if (re.test(object.name)) {
return true;
} else {
return object.list.some((item) => {
if (re.test(item.name)) {
// Calculates the characters to highlight in text based on query
const matches = match(item.name, inputValue);
// Breaks the given text to parts based on matches.
// After that create a new property named `parts` and assign an array to it.
item['parts'] = parse(item.name, matches);
return true;
} else {
return false;
}
});
}
});
return results;
};
The search is working fine but facing 2 major issues.
When the above match of the name property occurs, then it stops and does not go much deeper. The same thing is happening with the nested list name property.
When the filtration happens behind the scenes we're mutating the original data by adding a new property named parts which contains highlighted parts and it is an array of objects. But I don't want to mutate the original data instead wants to return the new filtered array which contains parts property.
See this.
WORKING DEMO :
Part 2: Which third-party libraries I'm using for filter and highlighting?
lodash string function escapeRegExp for escapes the RegExp
special characters.
autosuggest-highlight match function to calculates the
characters to highlight in text based on the query.
After that, from the same library parse function help us to break the given text to parts based on matches. In the end, it will
return an array of objects with the match string and highlight
boolean flag. So it's easy for us to bold the highlighted parts on the UI.
Part 3: App component
import React, { useState } from 'react';
import { filterByNames } from './filter';
import data from './data';
/**
* Return the JSX for the List
* #param data {Array<Object>} - The collection to iterate over.
* #return {null|*} - Returns the JSX or null.
*/
const renderList = (data) => {
if (Array.isArray(data) && data.length > 0) {
return data.map((object) => {
return (
<div key={object.id}>
<h1>{object.name}</h1>
<ul className="list">
{object.list.map((item) => {
return (
<li key={item.id}>
{item.parts ? (
<a href={item.slug}>
{item.parts.map((part, index) => (
<span
key={index}
style={{ fontWeight: part.highlight ? 700 : 400 }}
>
{part.text}
</span>
))}
</a>
) : (
<a href={item.slug}>{item.name}</a>
)}
</li>
)
})}
</ul>
</div>
)
})
} else {
return null
}
};
// Main App Component
const App = () => {
const [value, setValue] = useState('');
const onChangeHandler = (event) => {
const { target } = event;
const val = target.value;
setValue(val);
};
const results = !value ? data : filterByNames(data, value);
return (
<div className="demo">
<input type="text" value={value} onChange={onChangeHandler}/>
<div className="demo-result">
{ renderList(results) }
</div>
</div>
);
};
export default App;

Here is the revised code.
export const filterByNames = (data, inputValue) => {
// Create a dynamic regex expression object with ignore case sensitivity
const re = new RegExp(_.escapeRegExp(inputValue), "i");
const clonedData = _.cloneDeep(data);
const results = clonedData.filter((object) => {
return object.list.filter((item) => {
if (re.test(item.name)) {
// Calculates the characters to highlight in text based on query
const matches = match(item.name, inputValue);
// Breaks the given text to parts based on matches.
// After that create a new property named `parts` and assign an array to it.
item["parts"] = parse(item.name, matches);
return true;
} else {
return false;
}
}).length > 0 || re.test(object.name);
});
return results;
};
Forked link here.
https://codesandbox.io/s/search-frontend-forked-e3z55

Here is the code having solved both
export const filterByNames = (data, inputValue) => {
// Create a dynamic regex expression object with ignore case sensitivity
const re = new RegExp(_.escapeRegExp(inputValue), "i");
// since we cannot directly mutate the data object, why not copy it here ? (or if the data is bigger and copying is also not an option then consider using two arrays of data, one for the mutation and one default maybe)
let data_ = JSON.parse(JSON.stringify(data));
// filter and return the newer copy of the object.
const results = data_.filter((object) => {
// since we need the highlighting in both cases, on top level, or even in nested level so create separate function for that.
let highLightEm = (list) => {
return object.list.some((item) => {
if (re.test(item.name)) {
// Calculates the characters to highlight in text based on query
const matches = match(item.name, inputValue);
// Breaks the given text to parts based on matches.
// After that create a new property named `parts` and assign an array to it.
item["parts"] = parse(item.name, matches);
return true;
} else {
return false;
}
});
};
if (re.test(object.name)) {
// check for the highlighting in the inner name
highLightEm(object);
return true;
} else {
return highLightEm(object);
}
});
return results;
};
https://codesandbox.io/s/search-frontend-forked-kxui9?file=/src/filter.js

Related

How to pass a filtered array while keeping the original unchanged (ReactJS)

I am currently working on a todo list app, and i am trying to implement a function where users can filter tasks based on their completion status (all, completed, or uncompleted). I am trying to use the javascript filter function.'
I have tried using a duplicate array, therefore retaining the original values, and manipulating the duplicate array. However, I could not get the original array to update as the list increases, so i decided to scrap that.
There is a lot of code in my project but i will only copy the ones relevant to the question.
const Body = () => {
let tasks = [
{
task: 'Walk the dog',
completed: false,
id: Math.round(Math.random() * 100000),
},
{
task: 'Learn ReactJS',
completed: false,
id: Math.round(Math.random() * 100000),
},
];
const [task, setTask] = useState(tasks);
const addTask = () => {
let input = document.getElementById('task-field');
let randomID = Math.round(Math.random() * 100000);
if (input != null) {
setTask([...task, { task: input.value, completed: false, id: randomID }]);
}
};
return (
<div className='body'>
<Header></Header>
<Input setMethod={set} inputMethod={addTask}></Input>
<Tasks tasks={task} display={display} removeMethod={complete}></Tasks>
</div>
);
};
export default Body;
const Tasks=({tasks, display, removeMethod,})=> {
const [task,setTask] = useState(tasks)
const [display,setDIsplay] = useState(tasks)
const filterFunction=(event)=> {
let filtered;
const target = event.target;
switch(target.innerHTML) {
case('All'):
filtered = task
break;
case('Active'):
filtered = task.filter(()=> {
return !task.completed
})
break;
case('Completed'):
filtered = task.filter(()=> {
return task.completed
})
setDisplay(filtered)
}
}
return(
<div className="tasks">
{display.map(el=> {
return (
<TaskComponent task={el} removeMethod = {removeMethod} key = {el.id}></TaskComponent>
)
})}
<Footer method = {filterFunction} ></Footer>
</div>
)
}
export default Tasks;
so the question is, how do i implement this feauture in ReactJS?
for reference, i am linking a fellow coder's take on the same project : https://nasratt.github.io/frontendMentorSolutions/projects/todoList/
thanks in advance for the help.

How to filter an array of items if item's property contains text

I have an array of AppItems. Each app item has a property that is an array of ProfileItems. I want to filter my AppItems array based on which AppItem's have a Profile who's name contains my search text. It should return True on the first profile that contains the search text.
The problem is I'm for-looping through the profile items within a foreach within the filter function, which I don't think it likes. I have no idea how to do this.
export interface AppState {
appItems: AppItem[];
}
export interface AppItem {
profiles: ProfileItem[];
...
}
export interface ProfileItem {
name: string;
...
}
appItemsFiltered(state) {
return state.appItems
.filter((item: AppItem) => {
if (!state.filters.searchQuery) return true;
item.profiles.forEach(function (profile, index) {
const name = profile.name.toLowerCase()
const text = state.filters.searchQuery?.toLowerCase();
const result = name.indexOf(text)
if (result !== -1) return true;
})
return false
};
}
If the arrays is:
const array = [
{profiles: [{name: 'name 10'}, {name: 'name 11'}]},
{profiles: [{name: 'name 20'}, {name: 'name 21'}]},
// ...
];
Do filter like:
const filterText = 'name 21';
const result = array.filter(x => x.profiles.some(x => x.name === filterText));
result will be an array of matches.
const hasFound = result.length > 0;
When you return in a forEach it is the same as using continue in a for loop; it just moves on to the next iteration in the loop.
If you are looking for "at least one result is true" then consider using Array.prototype.some, which returns a boolean based on whether a single result in the array resolves as true.
Here's my attempt at re-writing your code with that solution, though with the data provided I can't do any better than this:
appItemsFiltered(state) {
return state.appItems
.filter((item: AppItem) => {
if (!state.filters.searchQuery) return true;
return item.profiles.some(function (profile, index) {
const name = profile.name.toLowerCase()
const text = state.filters.searchQuery?.toLowerCase();
const result = name.indexOf(text)
if (result !== -1) return true;
})
};
}
I wrote a snippet that introduces two approaches:
join() the strings & search in the resulting string
use some() with find()
const AppState = {
appItems: [{
profileItem: ['1.1', '1.2', '1.3'],
},
{
profileItem: ['2.1', '2.2', '2.3'],
},
{
profileItem: ['3.1', '3.2', '3.3'],
}
]
}
// creating an HTML representation of the list
const itemHtml = (item) => {
return `<li>${ item }</li>`
}
const profileItemsHtml = (profileItems) => {
return profileItems.map(item => itemHtml(item)).join('')
}
const createList = (appItems) => {
return appItems.reduce((a, {
profileItem
}) => {
a = [...a, ...profileItem]
return a
}, [])
}
// putting out the HTML
const updateListContainer = (container, list) => {
container.innerHTML = profileItemsHtml(list)
}
// the full list
const fullList = createList(AppState.appItems)
const fullListContainer = document.getElementById('full-list')
updateListContainer(fullListContainer, fullList)
// initiating the filtered list
let filteredList1 = fullList
const filteredListContainer1 = document.getElementById('filtered-list-1')
updateListContainer(filteredListContainer1, filteredList1)
let filteredList2 = fullList
const filteredListContainer2 = document.getElementById('filtered-list-2')
updateListContainer(filteredListContainer2, filteredList2)
// setting up filtering on input field input event
const filterInput = document.getElementById('filter-input')
filterInput.addEventListener('input', function(e) {
// FILTER 1: use join()
// if the list is made up of only strings, then you
// could join them & search the whole string at once
// might yield errors in edge cases, but mostly it
// should be correct, I think
// edge case example: 22 (you can try it)
const filtered1 = AppState.appItems.find(({
profileItem
}) => profileItem.join('').includes(e.target.value))
if (filtered1 && e.target.value) {
updateListContainer(filteredListContainer1, filtered1.profileItem)
} else {
updateListContainer(filteredListContainer1, fullList)
}
// FILTER 2: use SOME
const filtered2 = AppState.appItems.find(({
profileItem
}) => profileItem.some(item => item.includes(e.target.value)))
if (filtered2 && e.target.value) {
updateListContainer(filteredListContainer2, filtered2.profileItem)
} else {
updateListContainer(filteredListContainer2, fullList)
}
})
.list-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
<div>
<label for="filter-input">
Filter:
<input type="text" id="filter-input" />
</label>
<hr>
<div class="list-container">
<div>
FULL LIST:
<ul id="full-list"></ul>
</div>
<div>
FILTERED WITH JOIN:
<ul id="filtered-list-1"></ul>
</div>
<div>
FILTERED WITH SOME:
<ul id="filtered-list-2"></ul>
</div>
</div>
</div>

Convert all MirageJS ids to integers

MirageJS provides all model ids as strings. Our backend uses integers, which are convenient for sorting and so on. After reading around MirageJS does not support integer IDs out of the box. From the conversations I've read the best solution would be to convert Ids in a serializer.
Output:
{
id: "1",
title: "Some title",
otherValue: "Some other value"
}
But what I want is:
Expected Output:
{
id: 1,
title: "Some title",
otherValue: "Some other value"
}
I really want to convert ALL ids. This would included nested objects, and serialized Ids.
I think you should be able to use a custom IdentityManager for this. Here's a REPL example. (Note: REPL is a work in progress + currently only works on Chrome).
Here's the code:
import { Server, Model } from "miragejs";
class IntegerIDManager {
constructor() {
this.ids = new Set();
this.nextId = 1;
}
// Returns a new unused unique identifier.
fetch() {
let id = this.nextId++;
this.ids.add(id);
return id;
}
// Registers an identifier as used. Must throw if identifier is already used.
set(id) {
if (this.ids.has(id)) {
throw new Error('ID ' + id + 'has already been used.');
}
this.ids.add(id);
}
// Resets all used identifiers to unused.
reset() {
this.ids.clear();
}
}
export default new Server({
identityManagers: {
application: IntegerIDManager,
},
models: {
user: Model,
},
seeds(server) {
server.createList("user", 3);
},
routes() {
this.resource("user");
},
});
When I make a GET request to /users with this server I get integer IDs back.
My solution is to traverse the data and recursively convert all Ids. It's working pretty well.
I have a number of other requirements, like removing the data key and embedding or serializing Ids.
const ApplicationSerializer = Serializer.extend({
root: true,
serialize(resource, request) {
// required to serializedIds
// handle removing root key
const json = Serializer.prototype.serialize.apply(this, arguments)
const root = resource.models
? this.keyForCollection(resource.modelName)
: this.keyForModel(resource.modelName)
const keyedItem = json[root]
// convert single string id to integer
const idToInt = id => Number(id)
// convert array of ids to integers
const idsToInt = ids => ids.map(id => idToInt(id))
// check if the data being passed is a collection or model
const isCollection = data => Array.isArray(data)
// check if data should be traversed
const shouldTraverse = entry =>
Array.isArray(entry) || entry instanceof Object
// check if the entry is an id
const isIdKey = key => key === 'id'
// check for serialized Ids
// don't be stupid and create an array of values with a key like `arachnIds`
const isIdArray = (key, value) =>
key.slice(key.length - 3, key.length) === 'Ids' && Array.isArray(value)
// traverse the passed model and update Ids where required, keeping other entries as is
const traverseModel = model =>
Object.entries(model).reduce(
(a, c) =>
isIdKey(c[0])
? // convert id to int
{ ...a, [c[0]]: idToInt(c[1]) }
: // convert id array to int
isIdArray(c[0], c[1])
? { ...a, [c[0]]: idsToInt(c[1]) }
: // traverse nested entries
shouldTraverse(c[1])
? { ...a, [c[0]]: applyFuncToModels(c[1]) }
: // keep regular entries
{ ...a, [c[0]]: c[1] },
{}
)
// start traversal of data
const applyFuncToModels = data =>
isCollection(data)
? data.map(model =>
// confirm we're working with a model, and not a value
model instance of Object ? traverseModel(model) : model)
: traverseModel(data)
return applyFuncToModels(keyedItem)
}
})
I had to solve this problem as well (fingers crossed that this gets included into the library) and my use case is simpler than the first answer.
function convertIdsToNumbers(o) {
Object.keys(o).forEach((k) => {
const v = o[k]
if (Array.isArray(v) || v instanceof Object) convertIdsToNumbers(v)
if (k === 'id' || /.*Id$/.test(k)) {
o[k] = Number(v)
}
})
}
const ApplicationSerializer = RestSerializer.extend({
root: false,
embed: true,
serialize(object, request) {
let json = Serializer.prototype.serialize.apply(this, arguments)
convertIdsToNumbers(json)
return {
status: request.status,
payload: json,
}
},
})

React class component methods: is my code imperative?

I'm new to react and as well to the terms of functional, imperative, declarative. And I get to know that pure function is easy to test. I am self taught to program with Javascript. So far, it is working but my goal is to learn to write clean and maintainable code.
my question is the method addProductToSaleList below is bad and untestable because it is imperative? and how can I do it differently.
class SaleComponent extends React.Component {
addProductToSaleList = (values, dispatch, props) => {
//filter product from productList
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
if (productFound) {
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.filter(detail => {
if (productFound.name === detail.product) {
return detail
}
return undefined
})[0]
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
} else {
alert('The product code you add is not exist in product list');
}
}
render() {
// Render saleList
}
}
I belive this question should go to Code Review, but I will give it a shot. Part of the code can be improved
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
First, filter function receives a callback and for each item that callback will be executed. If the callback returns a value interpreted as true, it will return the item in the new array the function will build. Otherwise, it will skip that item. Assuming you're trying to find one item in the code, you could use the function find which will return you that element directly (no need for [0]), or undefined if that item is not found. So your code could be rewrite to
const productFound = props.productList.find(product => values.productCode === product.code.toString());
Note: No IE support.
Then, if the value was not found, you could just alert and do an early return. (You might also want to handle errors differently, with a better format than plain alert).
The code would look like
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// rest of the function
in order to find details, you can use find method as well
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
and then just call the rest of the code
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
Another improvement:
You're receiving a dispatch function as a parameter, but you're not using it. So you could remove it from function's declaration
(values, props) => { ... }
And you could split the last part into two different functions, something like
const getAction = details => `${detailFound ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = detailFound;
return {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
};
}
and then just call
props.dispatcher(getAction(details), getObject(details, productFound));
The end result would look like
addProductToSaleList = (values, props) => {
//filter product from productList
const productFound = props.productList.find(product => values.productCode === product.code.toString());
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
const getAction = details => `${details ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = details;
return {
...rest,
qty: parseInt(details.qty, 10) + 1
};
}
props.dispatcher(getAction(details), getObject(details, productFound));
}
my question is the method addProductToSaleList below is bad and
untestable because it is imperative
Well your code is testable, there are no external dependencies. So you could pass mocked values and props and add unit tests to that. That means, passing a fake values and props (they are just plain js object) and make assertions over that.
For instance:
You could mock dispatcher function and given the fake values in productList and saleItem.details you could see if dispatcher is called with the proper values. You should test different combinations of that
Mock alert function (Again, I would use another UI approach) and verify it is called, and that no other code is called (asserting that your fake dispatcher is not called). Something like this:
let actionToAssert;
let objectToAssert;
let values = { productCode: 'somecode' };
let props = {
productList: // your item listm with id and price, name, etc,
saleItem: {
details: // your details array here
}
dispatcher: (action, newObject) => {
actionToAssert = action;
objectToAssert = newObject;
}
}
addProductToSaleList(values, props); // make here assertions over actionToAssert and objectToAssert

Javascript - Building conditions dynamically

I created a general function called unique() to remove duplicates from a specific array.
However I'm facing a problem: I want to build the conditions dynamically based on properties that I pass to the function.
Ex: Let's suppose that I want to pass 2 properties, so I want to check these 2 properties before "remove" that duplicated object.
Currently I'm using eval() to build this condition "&&", however according to my search it's really a bad practice.
So, my question is:
What's the proper way to do this kind of thing?
Below is my current code:
function unique(arr, ...props) {
const conditions = [];
for (let prop of props) {
conditions.push(`element['${prop}'] === elem['${prop}']`);
}
const condStr = conditions.join(' && ');
return arr.filter((element, index) => {
const idx = arr.findIndex((elem) => {
return eval(condStr);
});
return idx === index;
});
}
const arr1 = [{
id: 1,
name: 'Josh',
description: 'A description'
}, {
id: 2,
name: 'Hannah',
description: 'A description#2'
}, {
id: 1,
name: 'Josh',
description: 'A description#3'
}, {
id: 5,
name: 'Anyname',
description: 'A description#4'
}];
const uniqueValues = unique(arr1, 'id', 'name');
console.log('uniqueValues', uniqueValues);
This question is a bit subjective as far as implementation details, but the better way if you ask me is to pass in a callback function to hand over to filter.
In doing it this way, you can compose the function anyway you see fit. If you have a complex set of conditions you can use composition to build the conditions in the function before you pass it into your unique function https://hackernoon.com/javascript-functional-composition-for-every-day-use-22421ef65a10
A key to function composition is having functions that are composable. A composable function should have 1 input argument and 1 output value.
The hackernoon article is pretty good and goes much further in depth.
this will return a single function that applies all of your preconditions
function unique(arr, callback) {
return arr.filter(callback);
}
const compose = (...functions) => data =>
functions.reduceRight((value, func) => func(value), data)
unique(
[1, 3, 4, 5 ,7, 11, 19teen]
compose(
(someStateCondition) => { /** return true or false **/ },
(result) => { /** return result === someOtherStateCondition **/}
)
)
Use Array#every to compare all properties inline:
function unique(arr, ...props) {
return arr.filter((element, index) => {
const idx = arr.findIndex(
elem => props.every(prop => element[prop] === elem[prop]);
);
return idx === index;
});
}

Categories