Issue
On my website, data is displayed in a horizontal list. Each individual list item has a title and a button. I've displayed it here https://codesandbox.io/p/sandbox/suspicious-platform-vj5td9
My task is that when you click on a div (id="main") with the value "first element", the color of the border changes from green to red. When you click on a div (id="main") with a value of "second element", the red color of the border with the "first element" is removed (and becomes green as before) and the border becomes red instead of the green of the "second element". However, when clicking on the "submenu" button, the border should not turn red.
const data = [
{ id: "1", title: "first element" },
{ id: "2", title: "second element" },
{ id: "3", title: "third element" },
];
export default function App() {
return (
<div>
{data.map((item) => (
<div id="main" className="App">
<div id="text">{item.title}</div>
<button>submenu</button>
</div>
))}
</div>
);
}
If you don't understand something from my explanations, don't hesitate to write to me.
Solution
There were a few problems with your posted JSX, which we'll discuss before looking at my proposed solution to the problem.
So, the original code with explanatory, and critical, comments in the code:
const data = [
{ id: "1", title: "first element" },
{ id: "2", title: "second element" },
{ id: "3", title: "third element" },
];
export default function App() {
return (
/* here you're returning the contents wrapped in a <div>.
element; this isn't necessarily a problem but be aware
of the potential for creating a mess of unnecessary
<div> elements: */
<div>
/* here we're looping over the data defined above,
which is okay, except... */
{data.map((item) => (
/* we're creating two elements, each of which have
an 'id' attribute, and there are multiple of
these elements in the eventual HTML, which
is invalid (an id must be unique within the
document): */
<div id="main" className="App">
<div id="text">{item.title}</div>
<button>submenu</button>
</div>
))}
</div>
);
}
My proposed solution, then, is as follows:
import "./styles.css";
const data = [
{ id: "1", title: "first element" },
{ id: "2", title: "second element" },
{ id: "3", title: "third element" },
];
// defining a named function to handle the click
// event, using an Arrow function expression,
// which passes a reference to the Event Object
// to the function:
const clickHandler = (evt) => {
// we retrieve all elements within the document that
// have the 'activated' class-name:
document
.querySelectorAll(".activated")
// and use NodeList.prototype.forEach() to iterate
// over each of those elements and remove that
// class-name:
.forEach((el) => el.classList.remove("activated"));
// we then use the currentTarget property-value of
// the Event Object ('evt'), access its classList
// and add the class of 'activated':
evt.currentTarget.classList.add("activated");
};
export default function App() {
return (
/* here we use a fragment to avoid returning the
an unnecessary <div>: /*
<>
{
/* we define the <div id="main"> element
outside of the loop that generates the
content: */
<div id="main" className="App">
{data.map((item) => (
/* here we avoid using id attributes, and
instead use meaningful class-names (though
obviously you may wish to rename the
classes for your own use).
We then use the onClick approach to bind a
function (clickHandler) to the 'click' event
fired on the elements; I personally dislike
inline event-handling but it seems the
preferred means of doing it within React;
also (as I was preparing to finalise this)
I realised that React was complaining that
"Each child in a list should have a unique
"key" prop", so I added the "key" prop as
well: */
<div className="menu-element-card" onClick={clickHandler} key={item.id}>
<h2>{item.title}</h2>
/* in the even that these elements may,
in some circumstances, be nested within
a <form> I've taken the liberty of
adding 'type="button"' to the <button>
elements to prevent default/unintended
form-submission: */
<button type="button">sub-menu</button>
</div>
))}
<!-- closing the <div id="main"> element: -->
</div>
}
<!-- closing the fragment: -->
</>
);
}
Also, I updated the stylesheet to the following; purely for aesthetics – please adjust for your own needs – in order to make the borders a little more prominent and spread the elements out a little:
.App {
display: flex;
flex-flow: row nowrap;
gap: 1rem;
}
.menu-element-card {
border: 2px solid green;
border-inline-start-width: 0.3rem;
flex-grow: 1;
margin-block-end: 0.4rem;
padding-block: 0.4rem;
padding-inline-start: 0.5rem;
transition: border-color 300ms linear;
}
.activated {
border-inline-start-color: red;
}
With reference to the question from OP, in the comments:
in the condition I wrote that pressing the submenu button should not change the color of the border. Is it possible to fix this somehow?
Of course, I missed that requirement; in response though I've adjusted the clickHandler()
function to the following:
const clickHandler = (evt) => {
/* the evt.target is the element upon which the event
was triggered, here we're checking that that
element is not a <button>, and that the element
does not have an ancestor <button> element */
if (evt.target.matches(":not(button)") && !evt.target.closest("button")) {
document
.querySelectorAll(".activated")
.forEach((el) => el.classList.remove("activated"));
evt.currentTarget.classList.add("activated");
}
};
Further, in response to the additional comment from OP (below):
...can I change the content of divs depending on whether the div is active or not. For example: if the user clicks on a div and the border color changes from green to red, then within the divs the message “color changed” will be displayed?
This is certainly possible, and in (at least) a couple of ways depending on your requirements.
The first is primarily to use CSS, and a pseudo-element although obviously JavaScript is in use as it's React:
const data = [
// here I've added an additional property ('msg') to some
// of the Objects in the data Array in order to show how
// we can accommodate elements without a specified message:
{ id: "1", title: "first element", msg: "Item 1 was activated" },
{ id: "2", title: "second element" },
{ id: "3", title: "third element", msg: "Item 3 was activated" },
];
const clickHandler = (evt) => {
if (evt.target.matches(":not(button)") && !evt.target.closest("button")) {
document
.querySelectorAll(".activated")
.forEach((el) => el.classList.remove("activated"));
evt.currentTarget.classList.add("activated");
}
};
export default function App() {
return (
<>
{
<div id="main" className="App">
{data.map((item) => (
<div
className="menu-element-card"
// here we add a 'data-message' attribute
// (a prop could be used of course) to store
// the required message in the HTML which
// exposes that content to the CSS:
data-message={item.msg}
onClick={clickHandler}
key={item.id}
>
<h2>{item.title}</h2>
<button type="button">
sub-menu <span>nested element text</span>
</button>
</div>
))}
</div>
}
</>
);
}```
```lang-css
.App {
/* here we define one (of a few, throughout the file)
custom CSS properties which can be used in multiple
places: */
--transitionDuration: 300ms;
display: flex;
flex-flow: row nowrap;
gap: 1rem;
}
.menu-element-card {
/* defining more custom properties, this time in the
scope of their likely use:
*/
--accentBorderColor: hsl(0deg 70% 50% / 1);
--accentBorderWidth: 0.3rem;
--baseBorderColor: lime;
/* defining the borders with the various custom
properties, via the CSS var() function:
*/
border: 2px solid var(--baseBorderColor);
border-inline-start: var(--accentBorderWidth) solid var(--baseBorderColor);
flex-grow: 1;
margin-block-end: 0.4rem;
overflow: hidden;
padding-block: 0.4rem;
padding-inline-start: 0.5rem;
position: relative;
/* defining the transition: */
transition: border-color var(--transitionDuration) linear;
/* using native CSS nesting to style the ::before
pseudo-element of the .menu-element-card */
&::before {
background: linear-gradient(
to bottom right,
hsl(240deg 90% 80% / 1),
hsl(300deg 90% 80% / 1)
);
block-size: fit-content;
border-block-end: var(--accentBorderWidth) solid var(--baseBorderColor);
content: attr(data-message);
/* using inset to position the pseudo-element 0
distance from the top, right, bottom and left
edges: */
inset: 0;
position: absolute;
transition: all var(--transitionDuration) linear;
/* positioning the element outside of its parent: */
translate: 0 calc(-1 * (100% + var(--accentBorderWidth)));
/* specifying the properties which should transition
(assuming you want transitions, obviously): */
transition-property: border-color, translate;
}
&:not([data-message])::before {
/* those elements that do not have a data-message
attribute are not displayed: */
display: none;
}
}
.activated {
border-inline-start-color: var(--accentBorderColor);
&::before {
border-block-end-color: var(--accentBorderColor);
translate: 0 0;
}
}```
[Codesandbox.io demo](https://codesandbox.io/p/sandbox/nameless-glitter-89yfwp?file=%2Fsrc%2Fstyles.css%3A1%2C1-48%2C2).
Alternatively, there's the option of using the same `items` Array of Objects, and the following JSX:
```lang-jsx
export default function App() {
return (
<>
{
<div id="main" className="App">
{data.map((item) => (
<div
className="menu-element-card"
onClick={clickHandler}
key={item.id}
>
<h2>{item.title}</h2>
/* here we use a ternary operator to test if
the 'msg' property exists on the 'item'
Object; if it does we return the <p> element
with the that text, and a specified class,
otherwise we return null so that no empty
<p> elements are created: */
{item.msg ? <p class="activeMessage">{item.msg}</p> : null}
<button type="button">
sub-menu <span>nested element text</span>
</button>
</div>
))}
</div>
}
</>
);
}```
[Codesandbox.io demo](https://codesandbox.io/p/sandbox/nameless-glitter-89yfwp?file=%2Fsrc%2FApp.js%3A35%2C14).
Obviously there is some simple CSS to hide, and reveal, the `p.activeMessage` element.
Answered By - David Thomas
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.