Skip to content

Shared Layout Exercises

Finishing our non-fungible WidgetProcessor

In the previous lesson, we saw how to use a “non-fungible” approach, so that each widget has a unique identity. This allows us to process individual widgets:

The solution I came up with isn't fully complete. Your mission in this exercise is to finish this implementation!

This (short!) video explains what you need to do:

Video Summary

This exercise has two parts:

  1. Wiring up the up/down arrow.
  2. When a widget is moved between boxes, it should be placed at the end of its new group.

For #1, the goal is to match what the behaviour was in the original fungible solution. The "Down" arrow should process the last unprocessed widget. The "Up" arrow should unprocess the first processed widget.

For #2, it's a bit tough to explain. It'll hopefully be clearer in the video.

When we click a widget in the outbox, it jumps back to the inbox, but it remembers its original position:

This feels weird to me. Instead, when a widget is processed and reverted, it should go to the end of the group:

More specifically, the logic is:

  • When a widget is processed, it should be pulled to the start of the group.
  • When a widget is unprocessed / reverted, it should be pushed to the end of the group.

Acceptance Criteria:

  • Clicking the "Down" button should mark a single widget as processed. It should select the final unprocessed widget in the inbox.
  • Clicking the "Up" button should mark the very first processed widget as unprocessed.
  • If there are no widgets in the relevant box when the Up/Down buttons are clicked, it should have no effect. No error should be thrown.
  • When widgets are processed, they should stack at the start of the .outbox.
  • When widgets are reverted, they should stack at the end of the .inbox.
  • This start/end stack logic should apply both when clicking individual widgets, and when using the "Up"/"Down" buttons.

Code Playground

import React from 'react';
import { LayoutGroup, motion } from 'framer-motion';
import { ChevronDown, ChevronUp } from 'react-feather';
import range from 'lodash.range';

import VisuallyHidden from './VisuallyHidden';

function WidgetProcessor({ widgets, processWidget }) {
const unprocessedWidgets = widgets.filter(
(widget) => widget.status === 'unprocessed'
);
const processedWidgets = widgets.filter(
(widget) => widget.status === 'processed'
);

return (
<LayoutGroup>
<div className="wrapper">
<div className="inbox">
{unprocessedWidgets.map((widget) => {
return (
<motion.button
layoutId={widget.id}
key={widget.id}
className="widget"
onClick={() =>
processWidget(widget.id, 'processed')
}
/>
);
})}
</div>

<div className="actions">
<button>
<VisuallyHidden>
Process widget
</VisuallyHidden>
<ChevronDown />
</button>
<button>
<ChevronUp />
<VisuallyHidden>
Revert widget
</VisuallyHidden>
</button>
</div>

<div className="outbox">
{processedWidgets.map((widget) => {
return (
<motion.button
layoutId={widget.id}
key={widget.id}
className="widget"
onClick={() =>
processWidget(widget.id, 'unprocessed')
}
/>
);
})}
</div>
</div>
</LayoutGroup>

Solution:

In this video, I briefly mention the “Principle of Least Privilege”. You can hop over to that lesson if you'd like a quick refresher.

Coin Sorter

Here's what we'll build in this exercise:

We have a grid with 4 compartments. The coins appear to glide from compartment to compartment when each is selected.

The implementation below includes all of the raw logic, but none of the Framer Motion stuff. Your mission is to update the code so that the coins glide smoothly from box to box.

Your solution should be perfectly smooth; the coins shouldn't be “bunched up” at the start of the transition. This clip shows an exaggerated version of the issue you might run into:

Also, this exercise has a stretch goal. We want to apply a subtle stagger to the coins, so that the coins move with increasing lethargy. Here's another clip that shows an exaggerated version of the effect:

Surprisingly, this stagger can be accomplished using the parts of the Framer Motion API we've already learned. You don't need to learn anything new to apply this effect.

Acceptance Criteria:

  • Selecting a new compartment should cause the coins to glide smoothly from their current compartment.
  • The coins should not get bunched up at the start of the transition:
  • Stretch goal: The coins should not be perfectly uniform in terms of their animation settings. Instead, the 6 coins should be on a spectrum from quick to slow.

Code Playground

import React from 'react';
import range from 'lodash.range';
import { motion } from 'framer-motion';

import styles from './CoinSorter.module.css';

const NUM_OF_BOXES = 4;

function CoinSorter({ numOfCoins }) {
const [selectedBox, setSelectedBox] = React.useState(0);

return (
<div className={styles.wrapper}>
{range(NUM_OF_BOXES).map((boxIndex) => (
<button
key={boxIndex}
className={`${styles.box} ${
selectedBox === boxIndex ? styles.selected : ''
}`}
onClick={() => setSelectedBox(boxIndex)}
>
{selectedBox === boxIndex &&
range(numOfCoins).map((coinIndex) => {
return (
<div
key={coinIndex}
className={styles.coin}
/>
);
})}
</button>
))}
</div>
);
}

export default CoinSorter;

Solution: