Using an item in an array in a condition statement - reactjs

I have an array of modals in my redux store that looks like this:
modals: [
{id: "modal1", isDisplayed: false},
{id: "modal2", isDisplayed: true}
]
In my React component, I want to have simple condition statements to display or not display my modals. I'm having a bit of trouble setting my condition statements right.
{this.props.modals["modal1"].isDisplayed ? <Modal1 /> : null}
{this.props.modals["modal2"].isDisplayed ? <Modal2 /> : null}
What's the right syntax to check the isDisplayed property of a given modal in my modals array?

Your modals property in the store is an Array. You can't access it by id.
This code will work:
{ this.props.modals[0].isDisplayed && <Modal1 /> }
{ this.props.modals[1].isDisplayed && <Modal2 /> }
Alternatively you can store it in the following way:
modals: {
modal1: { isDisplayed: false },
modal2: { isDisplayed: true },
}
Then your code will work.

I would recommend using Array.find, however it can't be inlined well in an expression without null coalescing operator.
This would break when a modal is not found (when find returns undefined)
{ this.props.modals.find(x => x.id === "nonexistingmodal").isDisplayed && <ModalXYZ /> }
With a little helper function you can implement it safely with minor code changes:
const select = (from, selector) => from && selector(from)
{ select(this.props.modals.find(x => x.id === "modal3"), x => x.isDisplayed) && <Modal3 /> }
You should consider using a Map or just a plain object as a modal store in order to express key relationships.

Related

React useState not updating mapped content

I feel like im missing something that is staring me right in the face. I am trying to have content stored in an array of objects update when a checkbox is on or off. The console log is showing the object data is updating correctly so I assume my fault resides in not understanding useState fully?
const [statistics, setStatistics] = useState([
{
id: 1,
content: <div>Content1</div>,
state: true,
},
{
id: 2,
content: <div>Content2</div>,
state: true,
},
]);
In the component:
{statistics.map((item) => (item.state ? item.content : <></>))}
<input
type="checkbox"
onChange={(e) => {
let newArr = statistics;
e.target.checked
? (newArr[0].state = true)
: (newArr[0].state = false);
setStatistics(newArr);
console.log(statistics);
}}
/>
You are trying to change the state directly, instead you need to work with a copy of the state and make all changes to it.
Just replace in your code this string:
let newArr = statistics; // as link to base array
to
let newArr = [...statistics]; // as new copy of base array
and it will works.
React skips all state changes if they are made directly.
To create a new array as copy/clone of another array, in ES6, we can use the spread operator. You can not use = here, since it will only copy the reference to the original array and not create a new variable. Just read here for reference.
In your case, your newArray will refer to the old statistics and will not be detected as the new state. That is why no re-render takes place after you made changes to it.
So here, you can do this:
return (
<>
{statistics.map((item) => (item.state ? item.content : <></>))}
<input
type="checkbox"
onChange={(e) => {
setStatistics((prevStats) => {
const newStats = [...prevStats];
e.target.checked
? (newStats[0].state = true)
: (newStats[0].state = false);
return newStats;
});
}}
/>
</>
);

How to render conditionally depending on which button is clicked?

I'm new to ReactJS and I want to have a conditional rendering like the code below
I read the doc but I was a bit confused.
Do I have to handle true/false for each buttons similar to the doc?
render() {
return (
<div>
<h1>Render "ABC"/"DEF"/"XYZ" depends on which button is clicked</h1>
<Button>ABC</Button>
<Button>DEF</Button>
<Button>XYZ</Button>
</div>
)
}
You can use a map (an object in JS) with key-boolean pair if you want to control multiple button states. Like
const [buttonStates, setButtonStates] = useState({
"ABC": false,
"DEF": true,
"XYZ": true,
});
Then
{buttonStates["ABC"] && <>"ABC"</>}
{buttonStates["DEF"] && <>"DEF"</>}
{buttonStates["XYZ"] && <>"XYZ"</>}
You can control this situation with simply one state.
For Example :
const [whicheBtn, setWicheBtn] = useState("btnA")
and in your component, you should check for the value of the whicheBtn state.
render() {
return (
<div>
<h1>Render "ABC"/"DEF"/"XYZ" depends on which button is clicked</h1>
{whicheBtn == "ABC" && <Button>ABC</Button> }
{whicheBtn == "DEF" && <Button>DEF</Button>}
{whicheBtn == "XYZ" && <Button>XYZ</Button> }
</div>
)
}

Vuetify combobox Value Set up

I used vuetify v-select but just change to v-combobox. There is a problem with value. For the v-select it was
[{
"text":"(NEC (1)) NEC work orders",
"value":"26729191407887178"
}]
After i changed to v-combobox it will show "26729191407887178". Is that possible to show (NEC (1)) NEC work orders in v-combobox?
<v-combobox
v-model="pwra.pwra_code"
label="Pwra Code"
:items="pwraCodeList"
dense
outlined
:hide-details="true"
:rules="[rules.required]"
></v-combobox>
You can set the return-object of <v-combobox/> to false then make a computed variable (in this example, its pwraCode) that finds the code in the pwraCodeList array and return it. If it isn't found, just return the typed input string.
<v-combobox
...
v-model="pwraCode"
:return-object="false"
>
</v-combobox>
// script
data: () => ({
pwra: {
pwra_code: "26729191407887178"
},
pwraCodeList: [...]
}),
computed: {
pwraCode: {
get: function() {
// find the code if it exist, else, just return the typed input
const code = this.pwraCodeList.find(
code => code.value === this.pwra.pwra_code
);
return code || this.pwra.pwra_code;
},
set: function(value) {
this.pwra.pwra_code = value;
}
}
}
Here's a demo at codesandbox:
What you need is to use the item-text prop. Just point it to the text key.
<v-combobox
v-model="pwra.pwra_code"
label="Pwra Code"
:items="pwraCodeList"
item-text="text"
dense
outlined
:hide-details="true"
:rules="[rules.required]"
></v-combobox>
And what dreamwalker said is true - it's just an extended v-select.

How to conditionally set HTML attributes in JSX using reason-react?

I want to render an HTML checkbox whose checked state is controlled by data.
Give a stateless component that receives an item type { label: string, checked: bool},
Like so:
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, _children) => {
render: _self => {
<li> <input type_="checkbox" {/*looking for something like this*/ item.checked ? "checked" : "" /* doesn't compile */}/> {ReasonReact.string(item.label)} </li>
}
}
How do I add the presence of the attribute checked to the input tag based on the item.checked == true condition?
As #wegry said in a comment, it seems to fit your use case better to just pass the value directly since item.checked already is a boolean, and checked takes a boolean.
But to answer more generally, since JSX attributes are just optional function arguments under the hood, you can use a neat syntactic trick to be able to explicitly pass an option to it: Just precede the value with ?. With your example:
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, _children) => {
render: _self => {
<li> <input type_="checkbox" checked=?(item.checked ? Some(true) : None) /> {ReasonReact.string(item.label)} </li>
}
}
Or, to give an example where you already have an option:
let link = (~url=?, label) =>
<a href=?url> {ReasonReact.string(label)} </a>
This is documented in the section titled Explicitly Passed Optional on the Function page in the Reason docs.

React contenteditable cursor jumps to beginning

I am using the module react-simple-contenteditable to enable editing of a fill in the blank worksheet. The reason I must use a content editable element instead of an input element is because I want the text of the problem to wrap. For example, if a problem has one blank, it divides the text into three sections the part before the blank, the blank, and the part after. If I were to represent the outer two as separate divs (or input fields), then the text would not wrap like a paragraph. Instead, I must have a single contenteditable div that contains an input field for the blank and free text on either side.
The text is wrapping like I want it, but when I type text in the contenteditable field, the cursor jumps to the beginning. I don't understand why because I tried the example on the module's github site and it works perfectly, and although my implementation is a bit more complicated, it works essentially the same.
Here is my render function that uses <ContentEditable /> :
render() {
const textPieces =
<div className='new-form-text-pieces'>
{
this.props.problem.textPieces.map( (textPiece, idx) => {
if (textPiece.blank) {
return (
<div data-blank={true} className='blank' key={ textPiece.id } style={{display: 'inline'}}>
<input
placeholder="Answer blank"
className='new-form-answer-input'
value={ this.props.problem.textPieces[idx].text }
onChange={ (event) => this.props.handleTextPiecesInput(this.props.problemIdx, idx, event.target.value) }
/>
<button className='modify-blank remove-blank' onClick={ (event) => this.props.removeBlank(this.props.problemIdx, idx) }>-</button>
</div>
);
} else {
let text = this.props.problem.textPieces[idx].text;
const placeholder = idx === 0 ? 'Problem text' : '...continue text';
// text = text === '' ? placeholder : text;
if (text === '') {
text = <span style={{color:'gray'}}>{placeholder}</span>;
} else {
}
return (
this.props.isTextSplit ?
<TextPiece
key={ textPiece.id }
problemIdx={this.props.problemIdx}
textPieceIdx={idx}
dropBlank={this.props.dropBlank}
moveBlank={this.props.moveBlank}
>
<div style={{display: 'inline-block', }}>{text}</div>
</TextPiece>
: text
);
}
})
}
</div>;
return (
this.props.isTextSplit ? textPieces :
<ContentEditable
html={ReactDOMServer.renderToStaticMarkup(textPieces)}
className="my-class"
tagName="div"
onChange={ (event, value) => this.props.handleProblemChange(event, this.props.problemIdx, value) }
contentEditable='plaintext-only'
/>
);
}
Here is the onChange function:
handleProblemChange(event, problemIdx) {
const problems = cloneDeep(this.state.problems);
event.target.children[0].childNodes.forEach( (textPieceNode, idx) => {
if (textPieceNode.constructor === Text) {
problems[problemIdx].textPieces[idx].text = textPieceNode.wholeText;
} else {
problems[problemIdx].textPieces[idx].text = textPieceNode.childNodes[0].value;
}
});
this.setState({ problems });
}
And here is the state it refers to, just to make thing clear:
this.state = {
problems: [
{
id: shortid.generate(),
textPieces: [
{
text : "Three days was simply not a(n)",
blank : false,
id: shortid.generate(),
},
{
text : "acceptable",
blank : true,
id: shortid.generate(),
},
{
text : "amount of time to complete such a lot of work.",
blank : false,
id: shortid.generate(),
}
]
}
Thanks so much
Long story short, there is no easy way to do this. I have tried this myself and spent days trying. Basically you have to save the cursor position and reposition it yourself after the update. All of this can be achieved with window.getSelection()
https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection
But it can get really tricky depending on how much your content has changed.
I ended up using draftJS instead. Which is an abstraction over contenteditable div by facebook themselves.
https://draftjs.org/docs/overview.html#content
A bit longer to pick up but you will be able to do a lot more
I had a similar problem using VueJS.
Here is the component containing the contenteditable div :
<Text #update-content="updateContent" :current-item-content="item.html_content"/>
Here is the prop definition in Text.vue component :
const props = defineProps({
currentItemContent: {
type: String,
default: ''
}
})
Here is the contenteditable div in Text.vue component :
<div
id="text-editor"
ref="text_editor"
class="mt-3 h-full w-full break-words"
contenteditable="true"
#input="updateContent"
v-html="currentItemContent"
>
Here is the method triggered on #update-content event
const item = computed(() => { ... })
(...)
function updateContent(content) {
item.value.html_content = content
}
The problem here is injecting the item.html_content value as a props triggers a re-render of the contenteditable div.
Because it's mutating it's value in the updateContent method and as the computed (item) is beeing updated, so does the prop value, v-html detects the updated value and triggers a re-render.
To avoid this, i removed the v-html binding :
<div
id="text-editor"
ref="text_editor"
class="mt-3 h-full w-full break-words"
contenteditable="true"
#input="updateContent"
>
And initialized the value of the contenteditable div in a onMounted hook :
const text_editor = ref(null)
(...)
onMounted(() => {
if (props.currentItemContent !== '') {
text_editor.value.innerHTML = props.currentItemContent
}
})
I don't know if there is a better solution for this but it's working fine for me. Hope this helps someone

Resources