Welcome back to our irregularly scheduled series of posts subtitled “Scopique Swears a Blue Streak At React”.
Since my initial foray into the world of React, I quickly cooled on the investigation mainly because I didn’t have a lot of use for it; I don’t use it at work, and I didn’t have a personal project I wanted to use it for. Since that time, I did come up with a personal project, and did decide that now would be a good time to put what I learned about React to good use. Unfortunately for me my impatience always gets the better of me, and I also made the mistake of reading a book targeted specifically at React with TypeScript, which has since thrown off my knowledge immensely. This is not the post to talk about that, however, since I’m going to show a bit of success that I’ve had with draggable components.
This project that I am working on needs to have the ability to generate small windows that can be dragged around a work area. When the user clicks an “add panel” button I need a new, empty panel, but eventually the user will be able to fill the panel with content that is saved to a data store, so the app will also need to load data from that data store to generate a list of panels that the user can open on demand. Right now the app is a sandbox that is using dummy data defined in a local file to mimic pulls from a data store, so all the app does is allow me to load two predefined panels as well as generate new panels when I click a button.
There are four components I’m concerned with here. The first is the PanelBlocks fake data store. This defines two interfaces and one pre-populated data construct with two panel elements defined — i.e. our “dummy data”. The second is the rendered component BasicPanel. This takes a single data element from PanelBlocks and builds a visible output. This is also the item that can be dragged around the work space. Third comes the WorkspaceToolbar component. This contains a button which the user can click to generate a new panel. Finally, there’s a Sandbox component. This renders an instance of WorkspaceToolbar and a container for the BasicPanel components and also handles the adding of new, blank components. It is also responsible for maintaining the state, which is where the app is currently storing the panels have been created.
Here I’ve defined two interfaces. The first of note is actually the second shown; a Panel contains four properties: id, title, body, and state. Title and body are optional because when a panel is created it will be empty. The panel will also need an id, though, as each panel has to have a unique identifier (and key for React’s refreshing abilities). State will allow the panel to show (open) or not (closed), but I haven’t gotten that far yet.
PanelBlocks, then, contains a single property: blocks. This is an array of Panel objects. PanelBlocks can be passed around as an argument, and the contents can be accessed via PanelBlocks.blocks.
Finally, there’s PanelData. This is of type PanelBlocks, so the definition of this object contains some dummy data in the form of an empty panel, and a panel with a title and body content.
The key conceit of React is that if you can logically encapsulate an element in a discreet processing package to be rendered on screen, do it. WorkspaceToolbar would ideally be a segment of real-estate which would contain controls for working with the workspace, such as listing panels that have been created or, in this simple case, adding a new, empty panel to the workspace.
Because this component exists as a detached entity, and because React can only pass data up via events that are defined on a level above and passed down to a level below, an interface has been defined called EventHandlers. The only prop currently present is onAddPanelClick, which accepts an argument e of type React.FormEvent. This type is a form-level event which is raised when a form element raises an event (duh!). Here, the event that the handler is specifically looking for is HTMLInputElement. Since the button is defined as an input, we can use this event handler for the button’s onClick event. Finally, because this is a function, the event handler is returning void, meaning it’s not returning anything at all (but it will be doing something).
Note, though, that this is not the actual event handler! It’s more like a delegate, a signature that defines what to expect when the app does encounter a handler passed where the signature is expected. The interface defined here doesn’t do anything on it’s own.
When WorkspaceToolbar is used, then, it will expose a property onAddPanelClick which can be seen defined as part of the component definition, of type EventHandlers. This component renders a button input, and the onClick event is wired to whatever event is represented by the onAddPanelClick argument this component is passed.
BasicPanel if deceptively more complex than it looks. At it’s most basic, this component will render a rough 3:2 rectangle. However, this component imports react-draggable and uses the <Draggable> tags that “magically” makes this component…well…draggable. The bounds attribute has been set to “parent” so the resulting render will only be allowed to move around inside the element that contains it (that element is defined in the Sandbox component).
The interface Panel makes an appearance here because this component needs the properties that were defined therein: id, title, body, and state, since this app uses TypeScript and it makes sense to enforce the typing of the arguments that the component expects. These could be passed as loose parameters, but because the panels will eventually be generated from a data store, it’s best to maintain a structure that can be relied upon. Only the id and body properties are being used currently, but that’s because this is just a test.
At the top level is Sandbox. This component does a lot, so let’s break it down.
Aside from the basic stuff, useState is imported, which is the latest way that React “remembers stuff” (it’s not super robust, but it works for our purposes). BasicPanel, and WorkspaceToolbar have already been discussed (ignore MainPage which is outside the scope of this post). Finally, PanelData brings the dummy data forward so it can be used. Right now, it’s assumed that Sandbox is as specific as we can get before the app starts to divide responsibilities at a more granular level; if the app was connected to a data store, Sandbox would be responsible for getting the data from an API.
The first directive of the component definition sets up a state variable called panels, and a method that will be used to update that variable called setPanels. This is how React deals with setting state, and follows a convention seen almost anywhere useState is discussed, which is to name the variable, and then the update method with set as a prefix. Setting this array equal to useState() sets the ball rolling as a state container, and in this case the value of panels is initialized with the dummy data from PanelData.blocks. This will happen once, when the component is rendered and the state variable is initialized.
Remember when WorkspaceToolbar defined an interface with an onAddPanelClick property accepting an arg e of type React.FormEvent and returned void? This is the actual event handler with the same signature that will be passed into WorkspaceToolbar.
Why do this? React is “one way”, meaning that if a component parent wants to talk to a component child, then the parent needs to send content into the child via an attribute. If the child wants to talk to the parent, or wants to talk to siblings, then the parent needs to supply the child or children with a vehicle that, when executed, is executed at the parent level. When passing a function from a parent into a child component, execution of the function inside the child actually executes where the function is defined…on the parent. It’s a monumental headache to plan this out, especially when data has to roll around the app and components need to communicate with one another, but in this simple test case the app only needs for the parent (Sandbox) to handle the event of the button press (inside WorkspaceToolbar) in order to add data to the state (stored in Sandbox).
onAddPanel is kind of simplistic. It uses a loop to find the last, greatest id value in our data collection held within the state variable panels and it increments and stores that value in the variable _lastID. Then, a new, basic Panel object is constructed by providing only the id and state. This is then merged into the existing state value of panels. Finally, setPanels is called with the updated version of _panels which contains all of the existing data plus our new, empty data with an incremented id value. Note that there’s no way to remove panels from state. Yet.
The Sandbox component definition is wrapped in MainPage, which is a custom component that normalizes page elements like header and footers. WorkspaceToolbar is rendered, and then a normal div with a class named “workspace” is added. This element creates a centered workspace area to 75vw and 75vh with a black border to serve as our boundary for panels.
Within this div the panels state property is iterated through using the best method ever, map. Map takes every element in a collection, aliases it as panel (in this case), and applies the actions of the arrow function to each instance. Here, each loop through renders an instance of the BasicPanel component, passing each property of the current loop into BasicPanel via exposed attributes.
Running the App
When the app is first run, the Sandbox workspace contains only 2 panels: ID 1 which has no content, and ID 2 which has a body content “This is a cool panel”. These represent data from our data store, currently hard-coded as a collection of Panel objects in the PanelData component.