Chapter 2: User Input Handling (Files & Interaction)#
Welcome back! In Chapter 1: Application Views, we learned how our application organizes its different screens or "rooms" using the LAYOUT
enum and how the MainView
component switches between them based on the application's state.
But how does the application know when to switch views? How does it get the data it needs to display on those views, like the photo the user wants to analyze or the GeoJSON file showing building outlines?
This is where User Input Handling comes in. It's the crucial part of the application that listens to the user, understands what they want to do, and gets the necessary information from them.
What's the Problem?#
Our app needs to be interactive. Users don't just passively look at it; they need to:
- Upload photos they've taken.
- Load geographic data files (like GeoJSON or LAZ).
- Click buttons to trigger actions (like processing data or switching views).
- Maybe even use keyboard shortcuts for quick navigation.
Without handling these inputs, the app would just sit there, static and unresponsive. We need mechanisms to capture these actions and data from the user.
What is User Input Handling?#
User Input Handling is the code responsible for:
- Listening: Detecting when a user interacts with the application (clicks, types, drags a file, etc.).
- Receiving Data: Getting the specific information from the input (the file content, the text typed, which button was clicked).
- Processing: Doing initial work with the received data (like reading a file, extracting simple details).
- Reacting: Triggering changes in the application state or calling other functions based on the input.
Think of it like the app having eyes, ears, and hands. It uses its "senses" (event listeners) to notice user actions, its "hands" (code) to grab the data, and its "brain" (processing logic) to figure out what that means and what to do next.
Handling File Uploads: Our Core Use Case#
A key part of this project is analyzing photos, which means the user needs a way to get their photos into the application. Let's focus on how the app handles uploading an image file, which is done primarily in the PhotoView
(Chapter 1: Application Views briefly mentioned this view).
When you're on the LAYOUT.PHOTO
screen, you see buttons like "Take a picture" and "Upload image". Clicking these buttons lets you select a file from your device.
How does this work in the code?
The File Input Element#
In web development, the standard way to let a user upload a file is using a specific HTML input element: <input type="file">
.
You can see this in the src/views/photo-view.tsx
file within the PhotoView
component:
// Inside PhotoView component in src/views/photo-view.tsx (simplified)
// ... styled components and imports ...
const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
});
// ... inside the component's return JSX ...
<Button
component="label" // Makes the button behave like a label for the input
variant="contained"
startIcon={<CameraAlt />}
>
{/* The actual file input, hidden visually but triggered by clicking the Button */}
<VisuallyHiddenInput
onChange={onImageChangeHandler} // This function is called when a file is selected
type="file"
accept="image/*" // Only accept image files
capture // Hint to use camera on mobile
/>
</Button>
Here's what's happening:
- We use a MUI (
@mui/material
)Button
component. - We set
component="label"
. This is a common pattern: clicking the<label>
element automatically clicks the associated<input>
element. We associate them by putting theinput
inside theButton
which acts as the label due to the prop. - We include the
<input type="file">
element inside theButton
. - We use a styled component
VisuallyHiddenInput
to make the actual file input box invisible (opacity: 0
,position: absolute
, etc.) because the native file input often doesn't look great and is hard to style. The user interacts with the nice-lookingButton
. - The crucial part is the
onChange
prop on the<input>
. This prop is set to a function calledonImageChangeHandler
. This function is an event handler. The browser automatically callsonImageChangeHandler
whenever the user successfully selects a file using the file dialog triggered by this input.
The Event Handler: onImageChangeHandler
#
When onChange
is triggered, the browser provides an event object to the onImageChangeHandler
function. This event object contains information about what happened, including the file(s) the user selected.
Look at the onImageChangeHandler
function in src/views/photo-view.tsx
:
// Inside PhotoView component in src/views/photo-view.tsx
const onImageChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
// Check if files were selected
if (!e.target.files || e.target.files.length === 0) {
onImageChange(undefined); // No file selected, clear previous
return;
}
// Get the first selected file (assuming single file upload here)
const selectedFile = e.target.files[0];
// Call the prop function passed from MainView
onImageChange(selectedFile);
};
This is a very simple handler:
- It receives the event object (
e
). - It checks
e.target.files
. This is aFileList
object containing the files selected by the user. If the user cancels the dialog, this might be empty. - It grabs the first file from the list (
e.target.files[0]
). For this feature, we only handle one image at a time. - It calls a function
onImageChange
and passes theselectedFile
to it.
Notice onImageChangeHandler
doesn't do much processing itself. Its main job is to get the file data from the event and then pass it along to another function (onImageChange
). Where does onImageChange
come from?
Passing Data Up: Props#
In React, data often flows down from parent components to child components via props. To pass data or trigger actions up from a child to a parent, the parent passes a function down as a prop, and the child calls that function when something happens.
In our case, MainView
renders PhotoView
and passes the onImageChange
function down as a prop:
// Inside MainView component in src/views/main-view.tsx (simplified)
// ... state definitions like const [selectedImg, setSelectedImg] = useState<File | null | undefined>(undefined); ...
return (
// ... other elements ...
{activeLayout === LAYOUT.PHOTO && (
<PhotoView
// ... other props ...
onImageChange={(result) => setSelectedImg(result)} // MainView passes this function down
// ... other props ...
/>
)}
// ... other elements ...
);
Here, MainView
defines a function (result) => setSelectedImg(result)
which updates the selectedImg
state variable. It passes this function down to PhotoView
via the onImageChange
prop.
So, when the user selects a file in PhotoView
, onImageChangeHandler
is called. It gets the file and calls props.onImageChange(selectedFile)
. This executes the function defined in MainView
, which updates MainView
's selectedImg
state.
What Happens Next? Processing and State Updates#
Once MainView
's state (specifically selectedImg
) is updated, React re-renders MainView
, and subsequently, PhotoView
. The PhotoView
component's rendering logic (return
JSX) uses the updated state (previewImg
, which is derived from selectedImg
elsewhere in MainView
). This is how the application displays a preview of the uploaded image.
But what about extracting metadata like EXIF tags? This processing happens after the file is selected and the state is updated. The logic for reading the file's contents, extracting EXIF, and updating other state variables (tags
) also lives in MainView
or is triggered by the state change there. We'll dive deeper into data processing in a later chapter (Chapter 8: Geographic Data Processing). For now, just know that selecting the file is the input that kicks off this processing chain.
Here's a simplified flow of the photo upload input:
sequenceDiagram
participant User
participant PhotoView
participant MainView
participant Metadata Extraction (later chapter)
User->PhotoView: Clicks "Upload image" button
PhotoView->Browser: Triggers file dialog via hidden input
User->Browser: Selects file and confirms
Browser->PhotoView: Triggers 'change' event on input
PhotoView->PhotoView: Calls onImageChangeHandler
onImageChangeHandler->PhotoView: Gets selected file
onImageChangeHandler->MainView: Calls onImageChange prop function with file
MainView->MainView: Updates selectedImg state (triggers re-render)
MainView->Metadata Extraction (later chapter): Triggers metadata extraction and GeoJSON processing
MainView->MainView: Updates tags and geo state
MainView->PhotoView: Renders PhotoView with previewImg and tags props
PhotoView-->>User: Displays image preview and "Metadata results" button
This diagram shows how the user's single action (selecting a file) flows through the components, updates the application's central state (MainView
), and triggers subsequent actions (like processing).
Other Types of Input#
File uploads are just one type of user input. The application handles others using similar principles (event listeners and handler functions):
-
Button Clicks: When you click the "Showcase" button in
PhotoView
, itsonClick
prop (onShowcaseClick
) is called. This prop function, passed fromMainView
, updates theactiveLayout
state toLAYOUT.SHOWCASE
, switching the view (as discussed in Chapter 1: Application Views).The// Inside PhotoView component in src/views/photo-view.tsx (simplified) <Button // ... props ... onClick={onShowcaseClick} // Calls the prop function when clicked > Showcase </Button>
onShowcaseClick
prop inPhotoViewProps
expects a function that takes no arguments and returns nothing (() => void
). InMainView
, this is connected to() => setActiveLayout(LAYOUT.SHOWCASE)
. -
Selecting from Dropdowns: In the
BuildingAttributes
component (used in the map views), there's a dropdown to select LAZ files. TheSelect
component has anonChange
prop.The// Inside BuildingAttributes component in src/components/building-attributes.tsx (simplified) <Select // ... other props ... value={lazFile?.name || ""} // The currently selected value label="Laz file" onChange={onLazChangeHandler} // Handler for selection change > {/* ... MenuItem options ... */} </Select>
onLazChangeHandler
function gets an event object containing the new selected value. It then finds the corresponding file object and callsprops.onLazChange
(passed fromMainView
orMapResultView
/MapShowcaseView
) to update the application state about which LAZ file is selected. -
Keyboard Shortcuts: The
useKeyboard
hook (src/hooks/useKeyboard.ts
) demonstrates handling keyboard input.This hook uses// Inside src/hooks/useKeyboard.ts (simplified) import { useCallback, useEffect } from "react"; export const useKeyboard = ( setView: (veiw: "firstPerson" | "map" | "orthographic") => void ) => { const handleKeyPress = useCallback( (event: KeyboardEvent) => { // Check the key pressed switch (event.key.toUpperCase()) { case "T": setView("orthographic"); // Call the function passed in break; case "P": setView("map"); break; case "D": setView("firstPerson"); break; default: } }, [setView] // Recreate handler if setView changes ); useEffect(() => { // Add event listener when the component mounts window.addEventListener("keypress", handleKeyPress); return () => { // Clean up the event listener when the component unmounts window.removeEventListener("keypress", handleKeyPress); }; }, [handleKeyPress]); // Re-run effect if handleKeyPress changes };
useEffect
to add a globalkeypress
event listener to the browser window. When a key is pressed,handleKeyPress
is called. It checksevent.key
and calls thesetView
function (passed into the hook) with the appropriate view type string ("orthographic", "map", "firstPerson"). ThissetView
function would likely be one that updates a state variable controlling the 3D view mode in a component likeMapResultView
orMapShowcaseView
.
In all these cases, the core pattern is similar: an event occurs on a specific element (or the window), a predefined handler function is called, the handler function processes the immediate input data, and then it often calls another function (usually a prop function from a parent component) to update application state or trigger more complex logic elsewhere.
Conclusion#
Handling user input is how our application becomes interactive. We learned that different types of input (file selection, button clicks, key presses, dropdowns) are captured using browser events and processed by corresponding event handler functions. These handlers typically extract the necessary data and then communicate it upwards, often by calling prop functions provided by parent components (like MainView
). This flow of information leads to updates in the application's state, triggering re-renders and subsequent processing or view changes.
Now that we know how to get data and commands from the user, the next step is to understand how to show them complex geographic data.
Next Chapter: 3D Geographic Visualization
Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/components/bottom-navigation.tsx), 2(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/components/building-attributes.tsx), 3(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/hooks/useKeyboard.ts), 4(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/views/photo-view.tsx)