React file upload behaviour duplicated across components - reactjs

I have created an image upload thingamajig called ImageSelector as a reusable component. It's for a custom CMS where the user first selects a header image, then selects several images for a gallery.
So the top part of the form uses the <ImageSelector /> component and the bottom part of the form uses an array of the same component. But for some reason whenever I select an image using the second ImageSelector at the bottom of the form, it only changes the image for the first ImageSelector.
It surely can't be the components' state that is bound together as I've never experienced that to be a problem, so it must be something to do with the way the browser caches files right? My question is, how can I make a reusable image upload component in React and avoid this duplication behaviour?
ImageSelector component:
(styles removed for brevity)
import { useState } from "react";
import { AiFillCamera } from "react-icons/ai";
import styled from "#emotion/styled";
export default function ImageSelector({ placeholder, shape }) {
const [currentImage, setCurrentImage] = useState();
const [currentImageUrl, setCurrentImageUrl] = useState();
function handleChangeImage(e) {
setCurrentImage(e.target.files[0]);
setCurrentImageUrl(URL.createObjectURL(e.target.files[0]));
}
return (
<ProfileImage $shape={shape}>
<div>
<ProfileImageOverlay $image={!!currentImage}>
<Label htmlFor="selector-image" $image={!!currentImage}>
{placeholder ? placeholder : <AiFillCamera />}
</Label>
<input
id="selector-image"
type="file"
name="selector-image"
onChange={handleChangeImage}
/>
</ProfileImageOverlay>
{currentImage && (
<img src={currentImageUrl} alt="selected image" width="170px" height="170px" />
)}
</div>
</ProfileImage>
);
}

Turns out this duplicate behaviour is what happens when you forget to change the html id attribute of subsequent instances of the input field!
Derp.

Related

React Jest Testing : How to test image is left aligned according to input parameter

I have to create react component having image and text box, so My task is to test
Image or Text box is Left or right aligned , as needed
Image or Text box is Top or bottom aligned, as needed
By passing input variant as textLeft or textRight
Example Component
Any Help?
If they both had the same aria-label or another selector you could use the getAllBy query which would give you an array of items in order they find them so then you could check the children by using within to query inside those.
Something like this would work.
const Component = () => {
return (
<>
<div aria-label="card">
<img alt="image example" />
</div>
<div aria-label="card">
<h1>This is heading</h1>
</div>
</>
)
}
import { render, screen, within } from '#testing-library/react'
test('React Testing Library works!', () => {
render(<Component />)
const cards = screen.getAllByLabelText('card')
expect(cards).toHaveLength(2)
expect(within(cards[0]).getByAltText('image example'))
expect(within(cards[1]).queryByAltText('image example')).not.toBeInTheDocument()
expect(within(cards[1]).getByText('This is heading'))
expect(within(cards[0]).queryByText('This is heading')).not.toBeInTheDocument()
})

Component Render Issues (Long Read)

I am dynamically getting data from my database to display on my website but I've encountered a problem and don't know how to fix it.
I have a main Home page which loads in a bunch of tiles that you can click on and take you to another page on my SPA. Some of the tiles you can click on can render STL objects from a file or some tiles will not when clicked on.
I have encountered an issue with my components that render STL files.
Issues?
Clickable Component is Multi Rendering my Model Component (or something like that?)
I have unmount and remount <Model/> for each page view?
General Issue with Project Setup?
Issue with STL (Model) component can be found at the bottom
Provided
Home.js (main component)
ClickableImage (component that returns [])
Layout.js (dynamic content pages)
Model.js (Component that loads my model)
Reproducible Code Current Issue CodeSandBox
Updates
11:03pm Feb 23 : Added in CodeSandBox
codesandbox code explanation. 2 Images are displayed, clicking on first image will take you to a layout that doesn't load in the STL file. The second image does. That's when everything breaks.
In Model.js under `/src/components/Layout/
Home.js
class Home extends Component {
render() {
const {projects} = this.props;
return (
<section className="max-container">
<div className="home-layout">
<div className="grid pl-4 pr-4">
<ClickableImage projects={projects}/>
</div>
</div>
</section>
);
}
}
ClickableImage
const ClickableImage = ({projects}) => {
const mappedData = projects && projects.map((project, index) => {
return (
<Link to={'/project3D/' + project.projectId} key={index}>
<img
src={project.banner}
alt="img" className="clickImage"/>
</Link>
);
})
return (
mappedData
);
}
Layout.js (/project3D/)
...
<div className="content-div">
<Canvas camera={{ position: [0, 10, 100] }}>
<Suspense fallback={null}>
<Model url={"./RaspberryPiCase.stl"} />
</Suspense>
<OrbitControls />
</Canvas>
</div>
...
So that is my general layout of everything on my page.
Home -> Click Image -> Layout Page.
Now this is where things get a little weird.
The Canvas portion on my layout page gives me this error when trying to load it.
Uncaught invalid array length react-reconciler.development.js:7648
The above error occurred in the <Model> component:
Model#http://localhost:3000/static/js/main.chunk.js:2430:15
Suspense
Canvas#http://localhost:3000/static/js/0.chunk.js:137042:66
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
But when I copy the Canvas section to my Home Component it will render my STL file with no problem and then it will also load my STL file on my other page as well.
Here are images to show you the problem without pasting <Canvas> in my home component.
PreHome Paste
After Pasting Canvas in Home Component
Model.js
import React, {useEffect, useRef} from "react";
import {STLLoader} from "three/examples/jsm/loaders/STLLoader";
import {useLoader, useThree} from "react-three-fiber";
export const Model = ({url}) => {
const geom = useLoader(STLLoader, url);
const ref = useRef();
const {camera} = useThree();
useEffect(() => {
camera.lookAt(ref.current.position);
}, [camera]);
return (
<>
<mesh ref={ref}>
<primitive object={geom} attach="geometry"/>
<meshStandardMaterial color={"orange"}/>
</mesh>
<ambientLight/>
<pointLight position={[10, 10, 10]}/>
</>
);
};
I just looked into it some more but apparently useLoader is what's most likely throwing my error, and I don't know why. (Only know this because when I comment it out the errors go away)
lmk if more info is required.
The problem is that useLoader() is having issues fetching the file you are providing in the url variable.
In your example, the value you end up providing as "url" is "./RaspberryPiCase.stl".
To fix your issue, simply provide the full absolute URL where your STL file can be fetched from.
Based on your provided Codesandbox, it's on your project /public folder, so a simple way to fix your issue is doing:
export const Model = ({url}) => {
const fullUrl = `${window.href.origin}${url.replace(".", "")}`;
const geom = useLoader(STLLoader, fullUrl);
// ...
See it live on a fork of your Codesandbox.
Additionally, you can follow this discussion in case your actual resource URL was not actually public (ie. required some kind of authentication). In that scenario, you can fetch the resource .gtl file first as an arrayBuffer, and then pass it to your loader.

Need to use react hooks to display a component based on button

So I have multiple components and a list of buttons. When I hit a button, I want to use reacthooks to display the corresponding component in {displayComponent}. I only want one component displaying at a time (so if you hit another button it will replace whatever was there previously).
Can someone tell me where i've gone wrong? This isn't working (it's just displaying all three components and the visibility doesn't change). My idea was to have the component display based on if displayComponent matched it's id.
How can I fix it and is there are more efficient way to accomplish this?
import Component1 from "./components/comp1";
import Component2 from "./components/comp2";
import Component3 from "./components/comp3";
function ComponentSelection() {
let [displayComponent, setDisplayComponent] = useState(null);
return(
<Button onClick={setDisplayComponent = "comp1">Click to display component 1</Button>
<Button onClick={setDisplayComponent = "comp2"}>Click to display component 2</Button>
<Button onClick={setDisplayComponent = "comp3"}>Click to display component 3</Button>
{displayComponent}
<Component1 id='comp1' display={displayComponent === 'comp1'} />
<Component2 id='comp2' display={displayComponent === 'comp2'} />
<Component3 id='comp3' display={displayComponent === 'comp3'} />
)
}
export default ComponentSelection
You misunderstood what hooks are. The second element in the returned array is a function to set it. Try this instead:
<Button onClick={() => setDisplayComponent("comp1")}>Click to display component 1</Button>

Material-ui's Switch component onChange handler is not firing

I've put some Switches in an app and they work fine. Then I put the Switches in another app, but they don't work when clicked.
Both apps are using the same component. Here it is working in one app:
And here's the other app, not working:
In the second app, the onChange handler doesn't seem to ever fire.
The code in both apps looks like the following:
<Switch
checked={(console.log('checked:', status === 'visible'), status === 'visible')}
onChange={(e, c) => console.log('event:', e, c)}
/>
In the first app I see the output of those console.logs, while in the second app I only see the initial console.log of the checked prop, but I never see any of the onChange prop.
I checked if any ancestor elements have click handlers, and I didn't find any that are returning false, calling stopPropagation, or calling preventDefault.
Notice in the gif that when I click, the ripple effect still works, so click handling is obviously still working.
Any ideas why onChange may not be firing?
UPDATE! I replaced the switches with regular <input type="checkbox"> elements, and it works great! See:
Looks to me like something is wrong with material-ui's <Switch> component. I have a hunch that I will investigate when I get a chance: there might be more than one React singleton in the application. I'll be back to post an update.
I think, this is a weird fix and it is working smoothly for me. So, instead of handleChange I am using handleClick. I am not using event here, instead I am passing a string which is obviously the name of the state or id in case of arrays.
<Switch
checked={this.state.active}
onClick={() => this.handleToggle('active')}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
handleToggle = (name: string) => {
this.setState({ active: !this.state.active });
};
I tried handleChange, but the problem still persists. I hope this will get fixed soon.
I've had the same issue with Checkbox and Switch in the WordPress admin area.
Turns out, there was global CSS rule like:
input[type="checkbox"] {
height: 1rem;
width: 1rem;
}
Clicking the upper left corner of the element works, though.
As a solution, I reset some styles in my app root.
EDIT: Nevermind, I just put my whole app into shadow DOM. There are a few gotchas, I'll list them here:
You have to provide a custom insertion point for Material-UI style elements inside the shadow DOM. In general, you have to make nothing gets put outside of you shadow DOM.
You have to load/link the font inside the shadow DOM and outside the shadow DOM (the light DOM).
Use ScopedCssBaseline instead of the global reset.
Dialogs have to have their container prop specified.
This is how I've set things up with Material-UI:
// configure-shadow-dom.js
import { create } from 'jss';
import { jssPreset } from '#material-ui/core/styles';
const shadowHostId = 'my-app-root-id'
export const appRoot = document.createElement('div')
appRoot.setAttribute('id', 'app-root')
const styleInsertionPoint = document.createComment('jss-insertion-point')
export const jss = create({
...jssPreset(),
insertionPoint: styleInsertionPoint,
})
const robotoFontLink = document.createElement('link')
robotoFontLink.setAttribute('rel', 'stylesheet')
robotoFontLink.setAttribute('href', 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap')
const shadowHost = document.getElementById(shadowHostId)
shadowHost.attachShadow({ mode: 'open' })
const shadowRoot = shadowHost.shadowRoot
shadowRoot.appendChild(robotoFontLink)
shadowRoot.appendChild(styleInsertionPoint)
shadowRoot.appendChild(appRoot)
// index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import ScopedCssBaseline from '#material-ui/core/ScopedCssBaseline';
import { StylesProvider } from '#material-ui/core/styles';
import App from './App';
import { jss, appRoot } from './configure-shadow-dom';
ReactDOM.render(
<React.StrictMode>
<StylesProvider jss={jss}>
<ScopedCssBaseline>
<App />
</ScopedCssBaseline>
</StylesProvider>
</React.StrictMode>,
appRoot
);
It turns out that in my case, there was a CSS in the page, something like
.some-component { pointer-events: none; }
.some-component * { pointer-events: auto; }
where .some-component was containing my material-ui buttons and switches. I had to manually set pointer-events to all (or some value, I don't remember at at the moment) for the elements inside the switches.
So, that's one thing to look out for: check what pointer-events style is doing.
in Switch component, changing onChange to onClick worked for me:
<Switch
checked={this.state.active}
onClick={() => this.handleToggle('active')}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
still running into similar issues getting an event from the switch. here's a quick fix using internal state (you should be using hooks now and functional components, if not you are missing out, but you can use setState to do what i'm showing here in a class component.)
const [switchChecked, setSwitchChecked] = useState(false);
.......
<Switch checked={switchChecked} onChange={() => {setSwitchChecked(!switchChecked);}}/>
you'll need to have a value for switchChecked in the components local state.
Try this one checked or unchecked passed as a second argument.
<Switch
checked={this.state.active}
onClick={(e,flag) => this.handleToggle('active', flag)}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>```
The issue is that you have to sniff the checked attribute from the event: event.target.checked
Full solution:
import { Switch } from '#material-ui/core';
import { useState } from 'react';
function App() {
const [checked, setChecked] = useState(false);
const switchHandler = (event) => {
//THIS IS THE SOLUTION - use event.target.checked to get value of switch
setChecked(event.target.checked);
};
return (
<div>
<Switch checked={checked} onChange={switchHandler} />
</div>
);
}
export default App;

is there a way to have templates in react

I have built an element which is kind of template. (e.g, thumbnail container with image at the top and something in the footer with dynamic content between them)
the dynamic content can be different types of DOM elements, based on the state.
I did it with adding logic in the render method which "injects" the dynamic part.
Does this make sense (having logic in the render method which returns different react components)?
Is there a better way for templating? (i'm not looking for projects that add this capability, wanted to know if there's a "react way" to do so.
Thanks!
edit: here's the code I was referring to (coffeescript):
internalContent: ->
switch #props.title
when "title1" then SomeReactFactory(props)
when "title2" then SomeOtherReactFactory(props)
render ->
...
DOM.div
className: 'panel'
#internalContent()
the internalContent() method is dynamically adding some React Component based on the prop
This is the React way.. And you should make use of it to target your specific domain.
For example a Button in React could be written like this:
const MyButton = ({ text, type = "normal", color = "blue", onClick }) => {
return (
<button
onClick={onClick}
style={{backgroundColor: color }}
className={"my-button my-button--type" + type}>
{text}
</button>);
};
Or a layout component:
const MyLayout = ({side, nav, main}) => {
return (
<div className="container">
<nav>{nave}</nav>
<aside>{side}</aside>
<div className="main">{main}</div>
</div>
)
}
Now you can composite it for example like this:
class App extends Component {
...
render() {
<MyLayout
nav={<MyNav/>}
side={<MySideBar items={...} />}
main={<MyButton onClick={this.onClick} text="Main Button"}
/>
}
}
Dont try to pack everything in a big Component which will do everything, the trick in React is to make small reusable Components and composite them.
You could also create a bunch of components which you can use across many projects.

Resources