String manipulation in JS and React - javascript

I'm doing some string manipulation with API returned fields, and they have some unique characteristics, hence why I'm looking to try to adjust them.
For example:
one field comes from the JSON as: "contract_time": "full_time"
Provided this, I would like to try and manipulate it so that I get the output "Full Time".
I am calling it directly as the below:
<BadgeComponentFirst>
<Typography color="red" fontSize="0.6em">
{job.contract_time}
</Typography>
How would I pass such string manipulation to an object to first, remove the '_' and capitalise and the first of each word?
Thanks

The approach you need should differ if the API values for that field are a known in advance or not.
If the values are known in advance, use an object to map the known values to their user-facing equivalent:
const CONTRACT_TIMES = {
full_time: "Full Time",
part_time: "Part Time",
};
<Typography color="red" fontSize="0.6em">
{CONTRACT_TIMES[job.contract_time] || "Unknown"}
</Typography>
If the API can return any value and you just want to display a cleaned up version, then write a function that does the manipulation you need:
function getFriendly(str) {
return str.split("_").map(getFriendlyWord).join(" ");
}
function getFriendlyWord(word) {
return word.slice(0, 1).toUpperCase() + word.slice(1);
}
<Typography color="red" fontSize="0.6em">
{getFriendly(job.contract_time)}
</Typography>

You want to split each word on '_' into an array, you can then map over the array and capitalize the first letter of each word. Then you want to join the words back together, separated by spaces. You can do:
let job = {
contract_time: "full_time"
}
console.log(job.contract_time.split("_").map(word => word[0].toUpperCase() + word.substr(1)).join(' '));
console: Full Time

define a method transforms your data:
const capitalize = (str) => str[0].toUpperCase() + str.slice(1);
const verbose = (snake_cased) => {
snake_cased.split('_').map(capitalize).join(' ');
};
And then you are free to use this transformer anywhere inside of JSX code without any limits. The only thing should keep in mind - this function will be fired every render. If this calculation is complicated - you may need some optimisation techniques.
const Component = () => (
<BadgeComponentFirst>
<Typography color="red" fontSize="0.6em">
{verbose(job.contract_time)}
</Typography>
</BadgeComponentFirst>
);

Related

Alter Object Array in React with useState-hook without deep copying

What is the best and clean way to alter Object Arrays?
I have a code that look´s like this.
const [get_post, set_post] = useState([
{name: "First"},
{name: "Second"},
{name: "Third"},
])
I would like to add and edit keys, on a certain index. So I do it like this:
<button onClick={()=>
set_post([...get_post, get_post[0].content = "This is index zero" ])
}> Manipulate! </button>
My result is this:
[
{"name": "First", "content": "This is index zero"},
{"name": "Second"},
{"name": "Third"},
"This is index zero" <-- This line is the problem
]
I have googled this a lot and this seems to be a common subject, however.
This post describe the same problem and solution with a keyed object, which doesn't help me.
React Hooks useState() with Object
This post support 3rd party libs and/or deep copying, which I suspect isn't the "right" way of doing it either.
Whats the best way to update an object in an array in ReactJS?
This thread also support a lot of deep copys and maps, which I suppose I don't need (It's an array, I'm should be able to adress my object by index)?
How do I update states `onChange` in an array of object in React Hooks
Another deep copy solution
https://codereview.stackexchange.com/questions/249405/react-hooks-update-array-of-object
The list goes on...
Basically I want the result I got without the extra line,
and if even possible:
Without deep copying the state to inject back in.
Without 3rd party libraries.
Without using a keyed object.
Without running a map/filter loop inside set_post.
Edit: The reason why map should be unnecessary in setPost.
In my particular scenario the Module that renders the getPost already is a map-loop. Trying to avoid nested loops.
(My logic simplified)
const [get_post, set_post] = useState([
{name: "First"},
{name: "Second"},
{name: "Third"},
])
//Render module
//Fixed numbers of controllers for each Object in Array.
get_post.map((post, index)=> {
<>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 1" ])}
}>
Controller 1
</button>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 2" ])}
}>
Controller 2
</button>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 3" ])}
}>
Controller 3
</button>
//...
</>
})
If you just want to alter the first property, extract it from the array first.
You can use the functional updates method to access the current state value and return a new one with the changes you want.
set_post(([ first, ...others ]) => [{
...first,
content: "This is index zero"
}, ...others])
To alter any particular index, you can map the current array to a new one, creating a new object for the target index when you reach it
let x = the_target_index
set_post(posts => posts.map((post, i) => i === x ? {
...post,
content: `This is index ${x}`
} : post))
A slightly different version of this that matches what you seem to want to do in your answer would be
set_post(posts => {
posts[x] = { ...posts[x], content: `This is index ${x}` }
// or even something like
// posts[x].content = `This is index ${x}`
return [...posts] // clone the array
})
I have found a solution that works! I haven't seen this posted elsewhere so it might be interesting to look into.
To change or add a key/value to an object in an array by index just do this:
<button onClick={() => {
set_post( [...get_post , get_post[index].content = "my content" ] )
set_post( [...get_post ] ) //<-- this line removes the last input
}
}>
As Phil wrote, the earlier code is interpreted as:
const val = "This is index zero";
get_post[0].content = val;
get_post.push(val);
This seems to remove the latest get_post.push(val)
According to the React Docs, states can be batched and bufferd to for preformance
https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
When React batches set_post it should behave like this.
If the one-line command
set_post( [...get_post , get_post[index].content = "my content" ] )
gives
const val = "This is index zero";
get_post[0].content = val;
get_post.push(val);
The double inject would trigger the buffer and reinject the state at the end insted of array.push(). Something like this.
var buffer = get_post
buffer[0].content = val
//Other updated to get_post...
get_post = buffer //(not push)
There for, this should be a perfectly good solutions.

In my React application, how can I add a newline after each item in this map function?

I have this redux selector I've been working on in my React application. I've made good progress but I've hit a wall with this last issue i'm working on and I feel like it shouldn't be that difficult to solve but I'm struggling.
What I'm trying to achieve is after each item has been mapped, the next item must go to a new line.
export const vLineRejectionSelector = createSelector(
selectedVIdSelector,
linesSelector,
(id, lines) =>
lines
.filter(line => line.id === id)
.map(
(rejectString, index) =>
`Line: ${index + 1} ${rejectString.rejectReason}`
)
);
The only relevant code to look at in this is the map function. I want each item to go to a new line as its being mapped.
The output should look something like:
Line 1: Reject Reason One
Line 2: Reject Reason Two
Instead the output looks like:
Line1: Reject ReasonOneLine2: Reject Reason Two
This is being rendered in JSX as well
The value of this is passed around as a prop and gets rendered in the JSX like:
<Typography variant="body2">
{rejectReason}
{reasons && reasons.join(', ')}
</Typography>
Its value is {rejectReason}.
I would advise against composing JSX in your selector and rather just return the lines as an array as you are doing currently, but then map it to either a list or a simple <br /> joined list in the render() function. This keeps your selector more easily testable and also doesn't mix state selection concerns with presentational concerns.
E.g:
in your container
const mapStateToProps = (state) => {
return {
rejectReason: vLineRejectionSelector(state)
}
}
const SomeComponentContainer = connect(
mapStateToProps,
mapDispatchToProps
)(SomeComponent)
export default SomeComponentContainer
and then in your SomeComponents render function:
<Typography variant="body2">
{this.props.rejectReason.map((rejectReason) => <>Line: {index + 1} {rejectReason}<br /></>)}
</Typography>
Try to use <br /> tag at the end of each line.
Like Line: ${index + 1} ${rejectString.rejectReason}<br />

ReactJS: Join map output with concatenating value

In my ReactJS application I am getting the mobile numbers as a string which I need to break and generate a link for them to be clickable on the mobile devices. But, instead I am getting [object Object], [object Object] as an output, whereas it should be xxxxx, xxxxx, ....
Also, I need to move this mobileNumbers function to a separate location where it can be accessed via multiple components.
For example: Currently this code is located in the Footer component and this code is also need on the Contact Us component.
...
function isEmpty(value) {
return ((value === undefined) || (value === null))
? ''
: value;
};
function mobileNumbers(value) {
const returning = [];
if(isEmpty(value))
{
var data = value.split(',');
data.map((number, index) => {
var trimed = number.trim();
returning.push(<NavLink to={`tel:${trimed}`} key={index}>{trimed}</NavLink>);
});
return returning.join(', ');
}
return '';
};
...
What am I doing wrong here?
Is there any way to create a separate file for the common constants / functions like this to be accessed when needed?
First question:
What am I doing wrong here?
The issue what you have is happening because of Array.prototype.join(). If creates a string at the end of the day. From the documentation:
The join() method creates and returns a new string by concatenating all of the elements in an array (or an array-like object), separated by commas or a specified separator string. If the array has only one item, then that item will be returned without using the separator.
Think about the following:
const navLinks = [{link:'randomlink'}, {link:'randomlink2'}];
console.log(navLinks.join(','))
If you would like to use concatenate with , then you can do similarly like this:
function mobileNumbers(value) {
if(isEmpty(value)) {
const data = value.split(',');
return data.map((number, index) => {
const trimed = number.trim();
return <NavLink to={`tel:${trimed}`} key={index}>{trimed}</NavLink>;
}).reduce((prev, curr) => [prev, ', ', curr]);
}
return [];
};
Then you need to use map() in JSX to make it work.
Second question:
Is there any way to create a separate file for the common constants / functions like this to be accessed when needed?
Usually what I do for constants is that I create in the src folder a file called Consts.js and put there as the following:
export default {
AppLogo: 'assets/logo_large.jpg',
AppTitle: 'Some app name',
RunFunction: function() { console.log(`I'm running`) }
}
Then simply import in a component when something is needed like:
import Consts from './Consts';
And using in render for example:
return <>
<h1>{Consts.AppTitle}</h1>
</>
Similarly you can call functions as well.
+1 suggestion:
Array.prototype.map() returns an array so you don't need to create one as you did earlier. From the documentation:
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
I hope this helps!

Looping through a Map in React

I have a Map object:
let dateJobMap = new Map();
for (let jobInArray of this.state.jobs) {
let deliveryDate: Date = new Date(jobInArray.DeliveryDate);
let deliveryDateString: string = deliveryDate.toLocaleDateString("en-US");
if (dateJobMap.has(deliveryDateString)) {
let jobsForDate: IDeliveryJob[] = dateJobMap.get(deliveryDateString);
jobsForDate.push(jobInArray);
}
else {
let jobsForDate: IDeliveryJob[] = [jobInArray];
dateJobMap.set(deliveryDateString, jobsForDate);
}
}
In my render method, I want to call a TruckJobComp object for each delivery job in the value's array to display it:
<div className={ styles.column }>
<p className={ styles.description }>{escape(this.props.description)}</p>
{
dateJobMap.forEach(function(jobsForDate, dateString) {
jobsForDate.map(job => (
<TruckJobComp job = { job } />
))
})
}
</div>
This seems like it should work but doesn't. It never creates a TruckJobComp. I do a .forEach iteration on my Map, and for each value's array, I use .map to get the individual job object to send to TruckJobComp object.
When I create a temp array to grab the jobs from the last loop:
let tempJobs: IDeliveryJob[];
and in the loop add in:
if (dateJobMap.has(deliveryDateString)) {
let jobsForDate: IDeliveryJob[] = dateJobMap.get(deliveryDateString);
jobsForDate.push(jobInArray);
tempJobs = jobsForDate;
}
and then use that array in the render:
<div className={ styles.column }>
<p className={ styles.description }>{escape(this.props.description)}</p>
{
tempJobs.map(job => (
<TruckJobComp job = { job }/>
))
}
</div>
It displays as expected.
I do have a warnings in Visual Studio Code:
Warning - tslint - ...\TruckDeliverySchedule.tsx(104,38): error no-function-expression: Use arrow function instead of function expression
I don't know enough to understand. Line 104 corresponds with:
dateJobMap.forEach(function(jobsForDate, dateString) {
I am very new to this so I'm not 100% sure how most of this works. Just trying to put pieces I've learned together to get things to work.
Second Edit:
{escape(this.props.description)}
{
[...dateJobMap.keys()].map(jobsForDate => // line 154
jobsForDate.map(job => (
<TruckJobComp job = { job } />
))
)
}
Produces error:
[09:06:56] Error - typescript - src\...\TruckDeliverySchedule.tsx(154,27): error TS2461: Type 'IterableIterator<any>' is not an array type.
dateJobMap.forEach(...) returns undefined, so it cannot be mapped to a collection of elements.
ES6 maps have forEach method for compatibility purposes (generally for..of is preferred to iterate over iterables) and don't have map method. A map should be converted to array first, then it could be mapped to an element. Since values aren't used, only keys need to be retrieved:
{
[...dateJobMap.keys()].map(jobsForDate =>
jobsForDate.map(job => (
<TruckJobComp job = { job } />
))
)
}
All this warning is saying is that instead of using the syntax function(jobsForDate, dateString) {} you should use the syntax (jobsForDate, dateString) => {}.
The reason could be the way this is scoped in arrow functions versus function expressions. See this post.
My guess as to the reason your first approach didn't work but your second one did is that forEach doesn't actually return an array, and if it did, calling map within forEach would return an array of arrays (but, again, it doesn't). Not sure how React would handle that, but React does know how to handle a single array, which is what your last approach returns.

JavaScript: Finding successive matches with .exec()

I have an object with a myriad of properties, such as color and brand, that describes a product. I'm looking for a way to dynamically generate product descriptions in paragraph form (because API doesn't provide one), and I came up with a way to do so by writing "templates" that have "props" surrounded in brackets {{}}. I wrote a function to "parse" the template by injecting the object properties in the string by replacing the "props" with the value of the key.
For example:
Object: {color: 'white'}
Template: "The bowl is {{color}}."
Result: "The bowl is white."
For some reason, my parse function isn't working. {{general_description}} isn't parsed.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/g;
const parse = (template) => {
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
template = template.replace(match, obj[key]);
}
return template;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
I followed the example provided in the MDN docs under Examples > Finding successive matches. It says that I need to first store the regular expression in a variable (e.g., templatePropRe), for the expression cannot be in the while loop condition or it will loop indefinitely. However, if I do that, my problem is resolved. See here...nothing broke.
I rewrote the function using String.prototype.match, and it works as expected, but I don't have access to the capture so I need to first strip off the brackets using stripBrackets. See the working example using match here.
What I want to know is why doesn't my parse() function that utilizes RegExp.prototype.exec work properly?
Remove the /g flag from your regex. According to the documentation, when this flag is present, it updates the regex object's lastIndex property which indicates from where next call to exec() will start to search a match.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/;
const parse = (template) => {
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
template = template.replace(match, obj[key]);
}
return template;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
This happened because you modify and check the same string in your code.
Whereas regExp saves index of matched substring after each execution you change length of the string and regEx with next execution starts from other point than you expect.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/g;
const parse = (template) => {
var resultStr = template;
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
resultStr = resultStr.replace(match, obj[key]);
}
return resultStr;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
Instead of performing the 2-step replacement (finding a match and then replacing the first occurrence with the required value) (that is prone with issues like the one you encountered when a new string is passed to the same RegExp with old, already invalid, index), you may use a callback method as a replacement argument inside a String#replace method. That way, the resulting string will be constructed on the fly upon each match making the code execute faster.
See an example fix below:
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const parse = (template) => {
return template.replace(/{{(\w*)}}/g, ($0, $1) => obj[$1] ? obj[$1] : $0 );
// ES5 way:
// return template.replace(/{{(\w*)}}/g, function($0, $1) {
// return obj[$1] ? obj[$1] : $0;
// });
}
console.log(parse('{{keep}} This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
Note that here, after finding a match, the ($0, $1) => obj[$1] ? obj[$1] : $0 code does the following: the whole match is assigned to $0 variable and the Group 1 value is assigned to $1; then, if there is a key with the name $1 in obj, the value will be put instead of the match into the right place in the resulting string. Else, the whole match is put back (replace with '' if you want to remove a {{...}} with a non-existent key name).

Categories