diff --git a/react/snippet-template.md b/react/snippet-template.md new file mode 100644 index 000000000..13b7acf17 --- /dev/null +++ b/react/snippet-template.md @@ -0,0 +1,29 @@ +--- +title: Component Name +type: snippet +tags: [components,state,effect] +cover: image +dateModified: 2021-06-13T05:00:00-04:00 +--- + +Explain briefly what the snippet does. + +- Explain briefly how the snippet works. +- Use bullet points for your snippet's explanation. +- Try to explain everything briefly but clearly. + +```jsx +const ComponentName = props => { + const [state, setState] = React.useState(null); + React.useEffect(() => { + setState(0); + }); + return
{props}
; +} +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + +); +``` diff --git a/react/snippets/accordion.md b/react/snippets/accordion.md new file mode 100644 index 000000000..ecf0169df --- /dev/null +++ b/react/snippets/accordion.md @@ -0,0 +1,86 @@ +--- +title: Collapsible accordion +type: snippet +tags: [components,children,state] +cover: beach-pineapple +dateModified: 2021-10-13T19:29:39+02:00 +--- + +Renders an accordion menu with multiple collapsible content elements. + +- Define an `AccordionItem` component, that renders a ` +
+ {children} +
+ + ); +}; + +const Accordion = ({ defaultIndex, onItemClick, children }) => { + const [bindIndex, setBindIndex] = React.useState(defaultIndex); + + const changeItem = itemIndex => { + if (typeof onItemClick === 'function') onItemClick(itemIndex); + if (itemIndex !== bindIndex) setBindIndex(itemIndex); + }; + const items = children.filter(item => item.type.name === 'AccordionItem'); + + return ( + <> + {items.map(({ props }) => ( + changeItem(props.index)} + children={props.children} + /> + ))} + + ); +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + + + Lorem ipsum + + + Dolor sit amet + + +); +``` diff --git a/react/snippets/alert.md b/react/snippets/alert.md new file mode 100644 index 000000000..fb7b65616 --- /dev/null +++ b/react/snippets/alert.md @@ -0,0 +1,107 @@ +--- +title: Closable alert +type: snippet +tags: [components,state,effect] +cover: flower-portrait-1 +dateModified: 2021-01-07T23:57:13+02:00 +--- + +Renders an alert component with `type` prop. + +- Use the `useState()` hook to create the `isShown` and `isLeaving` state variables and set both to `false` initially. +- Define `timeoutId` to keep the timer instance for clearing on component unmount. +- Use the `useEffect()` hook to update the value of `isShown` to `true` and clear the interval by using `timeoutId` when the component is unmounted. +- Define a `closeAlert` function to set the component as removed from the DOM by displaying a fading out animation and set `isShown` to `false` via `setTimeout()`. + +```css +@keyframes leave { + 0% { opacity: 1 } + 100% { opacity: 0 } +} + +.alert { + padding: 0.75rem 0.5rem; + margin-bottom: 0.5rem; + text-align: left; + padding-right: 40px; + border-radius: 4px; + font-size: 16px; + position: relative; +} + +.alert.warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; +} + +.alert.error { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert.leaving { + animation: leave 0.5s forwards; +} + +.alert .close { + position: absolute; + top: 0; + right: 0; + padding: 0 0.75rem; + color: #333; + border: 0; + height: 100%; + cursor: pointer; + background: none; + font-weight: 600; + font-size: 16px; +} + +.alert .close::after { + content: 'x'; +} +``` + +```jsx +const Alert = ({ isDefaultShown = false, timeout = 250, type, message }) => { + const [isShown, setIsShown] = React.useState(isDefaultShown); + const [isLeaving, setIsLeaving] = React.useState(false); + + let timeoutId = null; + + React.useEffect(() => { + setIsShown(true); + return () => { + clearTimeout(timeoutId); + }; + }, [isDefaultShown, timeout, timeoutId]); + + const closeAlert = () => { + setIsLeaving(true); + timeoutId = setTimeout(() => { + setIsLeaving(false); + setIsShown(false); + }, timeout); + }; + + return ( + isShown && ( +
+
+ ) + ); +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + +); +``` diff --git a/react/snippets/auto-link.md b/react/snippets/auto-link.md new file mode 100644 index 000000000..449b6c1e2 --- /dev/null +++ b/react/snippets/auto-link.md @@ -0,0 +1,41 @@ +--- +title: Automatic text linking +type: snippet +tags: [components,fragment,regexp] +author: chalarangelo +cover: red-petals +dateModified: 2020-11-03T20:42:15+02:00 +--- + +Renders a string as plaintext, with URLs converted to appropriate link elements. + +- Use `String.prototype.split()` and `String.prototype.match()` with a regular expression to find URLs in a string. +- Return matched URLs rendered as `` elements, dealing with missing protocol prefixes if necessary. +- Render the rest of the string as plaintext. + +```jsx +const AutoLink = ({ text }) => { + const delimiter = /((?:https?:\/\/)?(?:(?:[a-z0-9]?(?:[a-z0-9\-]{1,61}[a-z0-9])?\.[^\.|\s])+[a-z\.]*[a-z]+|(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})(?::\d{1,5})*[a-z0-9.,_\/~#&=;%+?\-\\(\\)]*)/gi; + + return ( + <> + {text.split(delimiter).map(word => { + const match = word.match(delimiter); + if (match) { + const url = match[0]; + return ( + {url} + ); + } + return word; + })} + + ); +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + +); +``` diff --git a/react/snippets/callto.md b/react/snippets/callto.md new file mode 100644 index 000000000..44d1385f1 --- /dev/null +++ b/react/snippets/callto.md @@ -0,0 +1,26 @@ +--- +title: Callable telephone link +type: snippet +tags: [components] +author: chalarangelo +unlisted: true +cover: rabbit-call +dateModified: 2021-01-04T12:32:47+02:00 +--- + +Renders a link formatted to call a phone number (`tel:` link). + +- Use `phone` to create a `` element with an appropriate `href` attribute. +- Render the link with `children` as its content. + +```jsx +const Callto = ({ phone, children }) => { + return {children}; +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + Call me! +); +``` diff --git a/react/snippets/carousel.md b/react/snippets/carousel.md new file mode 100644 index 000000000..d34048348 --- /dev/null +++ b/react/snippets/carousel.md @@ -0,0 +1,67 @@ +--- +title: Carousel +type: snippet +tags: [components,children,state,effect] +cover: shell-focus +dateModified: 2020-11-03T20:42:15+02:00 +--- + +Renders a carousel component. + +- Use the `useState()` hook to create the `active` state variable and give it a value of `0` (index of the first item). +- Use the `useEffect()` hook to update the value of `active` to the index of the next item, using `setTimeout()`. +- Compute the `className` for each carousel item while mapping over them and applying it accordingly. +- Render the carousel items using `React.cloneElement()` and pass down `...rest` along with the computed `className`. + +```css +.carousel { + position: relative; +} + +.carousel-item { + position: absolute; + visibility: hidden; +} + +.carousel-item.visible { + visibility: visible; +} +``` + +```jsx +const Carousel = ({ carouselItems, ...rest }) => { + const [active, setActive] = React.useState(0); + let scrollInterval = null; + + React.useEffect(() => { + scrollInterval = setTimeout(() => { + setActive((active + 1) % carouselItems.length); + }, 2000); + return () => clearTimeout(scrollInterval); + }); + + return ( +
+ {carouselItems.map((item, index) => { + const activeClass = active === index ? ' visible' : ''; + return React.cloneElement(item, { + ...rest, + className: `carousel-item${activeClass}` + }); + })} +
+ ); +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + carousel item 1, +
carousel item 2
, +
carousel item 3
+ ]} + /> +); +``` diff --git a/react/snippets/collapse.md b/react/snippets/collapse.md new file mode 100644 index 000000000..a8757f32f --- /dev/null +++ b/react/snippets/collapse.md @@ -0,0 +1,61 @@ +--- +title: Collapsible content +type: snippet +tags: [components,children,state] +cover: washed-ashore +dateModified: 2021-10-13T19:29:39+02:00 +--- + +Renders a component with collapsible content. + +- Use the `useState()` hook to create the `isCollapsed` state variable. Give it an initial value of `collapsed`. +- Use the ` +
+ {children} +
+ + ); +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + +

This is a collapse

+

Hello world!

+
+); +``` diff --git a/react/snippets/controlled-input.md b/react/snippets/controlled-input.md new file mode 100644 index 000000000..ff563eccb --- /dev/null +++ b/react/snippets/controlled-input.md @@ -0,0 +1,44 @@ +--- +title: Controlled input field +type: snippet +tags: [components,input] +cover: digital-nomad-5 +dateModified: 2020-11-03T21:08:39+02:00 +--- + +Renders a controlled `` element that uses a callback function to inform its parent about value updates. + +- Use the `value` passed down from the parent as the controlled input field's value. +- Use the `onChange` event to fire the `onValueChange` callback and send the new value to the parent. +- The parent must update the input field's `value` prop in order for its value to change on user input. + +```jsx +const ControlledInput = ({ value, onValueChange, ...rest }) => { + return ( + onValueChange(value)} + {...rest} + /> + ); +}; +``` + +```jsx +const Form = () => { + const [value, setValue] = React.useState(''); + + return ( + + ); +}; + +ReactDOM.createRoot(document.getElementById('root')).render( +
+); +``` diff --git a/react/snippets/count-down.md b/react/snippets/count-down.md new file mode 100644 index 000000000..33fc98d4d --- /dev/null +++ b/react/snippets/count-down.md @@ -0,0 +1,66 @@ +--- +title: Countdown timer +type: snippet +tags: [components,state] +cover: sea-view-2 +dateModified: 2021-10-13T19:29:39+02:00 +--- + +Renders a countdown timer that prints a message when it reaches zero. + +- Use the `useState()` hook to create a state variable to hold the time value. Initialize it from the props and destructure it into its components. +- Use the `useState()` hook to create the `paused` and `over` state variables, used to prevent the timer from ticking if it's paused or the time has run out. +- Create a method `tick`, that updates the time values based on the current value (i.e. decreasing the time by one second). +- Create a method `reset`, that resets all state variables to their initial states. +- Use the the `useEffect()` hook to call the `tick` method every second via the use of `setInterval()` and use `clearInterval()` to clean up when the component is unmounted. +- Use `String.prototype.padStart()` to pad each part of the time array to two characters to create the visual representation of the timer. + +```jsx +const CountDown = ({ hours = 0, minutes = 0, seconds = 0 }) => { + const [paused, setPaused] = React.useState(false); + const [over, setOver] = React.useState(false); + const [[h, m, s], setTime] = React.useState([hours, minutes, seconds]); + + const tick = () => { + if (paused || over) return; + if (h === 0 && m === 0 && s === 0) setOver(true); + else if (m === 0 && s === 0) { + setTime([h - 1, 59, 59]); + } else if (s == 0) { + setTime([h, m - 1, 59]); + } else { + setTime([h, m, s - 1]); + } + }; + + const reset = () => { + setTime([parseInt(hours), parseInt(minutes), parseInt(seconds)]); + setPaused(false); + setOver(false); + }; + + React.useEffect(() => { + const timerID = setInterval(() => tick(), 1000); + return () => clearInterval(timerID); + }); + + return ( +
+

{`${h.toString().padStart(2, '0')}:${m + .toString() + .padStart(2, '0')}:${s.toString().padStart(2, '0')}`}

+
{over ? "Time's up!" : ''}
+ + +
+ ); +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + +); +``` diff --git a/react/snippets/data-list.md b/react/snippets/data-list.md new file mode 100644 index 000000000..53cd9ed92 --- /dev/null +++ b/react/snippets/data-list.md @@ -0,0 +1,29 @@ +--- +title: Data list +type: snippet +tags: [components] +cover: interior-14 +dateModified: 2020-11-03T21:26:34+02:00 +--- + +Renders a list of elements from an array of primitives. + +- Use the value of the `isOrdered` prop to conditionally render an `
    ` or a `
      ` list. +- Use `Array.prototype.map()` to render every item in `data` as a `
    • ` element with an appropriate `key`. + +```jsx +const DataList = ({ isOrdered = false, data }) => { + const list = data.map((val, i) =>
    • {val}
    • ); + return isOrdered ?
        {list}
      :
        {list}
      ; +}; +``` + +```jsx +const names = ['John', 'Paul', 'Mary']; +ReactDOM.createRoot(document.getElementById('root')).render( + <> + + + +); +``` diff --git a/react/snippets/data-table.md b/react/snippets/data-table.md new file mode 100644 index 000000000..e0e9c2c2a --- /dev/null +++ b/react/snippets/data-table.md @@ -0,0 +1,42 @@ +--- +title: Data table +type: snippet +tags: [components] +cover: armchair +dateModified: 2020-11-03T21:26:34+02:00 +--- + +Renders a table with rows dynamically created from an array of primitives. + +- Render a `` element with two columns (`ID` and `Value`). +- Use `Array.prototype.map()` to render every item in `data` as a `` element with an appropriate `key`. + +```jsx +const DataTable = ({ data }) => { + return ( +
      + + + + + + + + {data.map((val, i) => ( + + + + + ))} + +
      IDValue
      {i}{val}
      + ); +}; +``` + +```jsx +const people = ['John', 'Jesse']; +ReactDOM.createRoot(document.getElementById('root')).render( + +); +``` diff --git a/react/snippets/file-drop.md b/react/snippets/file-drop.md new file mode 100644 index 000000000..6e851b378 --- /dev/null +++ b/react/snippets/file-drop.md @@ -0,0 +1,107 @@ +--- +title: File drag and drop area +type: snippet +tags: [components,input,state,effect,event] +author: chalarangelo +cover: man-red-sunset +dateModified: 2021-10-13T19:29:39+02:00 +--- + +Renders a file drag and drop component for a single file. + +- Create a ref, called `dropRef` and bind it to the component's wrapper. +- Use the `useState()` hook to create the `drag` and `filename` variables. Initialize them to `false` and `''` respectively. +- The variables `dragCounter` and `drag` are used to determine if a file is being dragged, while `filename` is used to store the dropped file's name. +- Create the `handleDrag`, `handleDragIn`, `handleDragOut` and `handleDrop` methods to handle drag and drop functionality. +- `handleDrag` prevents the browser from opening the dragged file. `handleDragIn` and `handleDragOut` handle the dragged file entering and exiting the component. `handleDrop` handles the file being dropped and passes it to `onDrop`. +- Use the `useEffect()` hook to handle each of the drag and drop events using the previously created methods. + +```css +.filedrop { + min-height: 120px; + border: 3px solid #d3d3d3; + text-align: center; + font-size: 24px; + padding: 32px; + border-radius: 4px; +} + +.filedrop.drag { + border: 3px dashed #1e90ff; +} + +.filedrop.ready { + border: 3px solid #32cd32; +} +``` + +```jsx +const FileDrop = ({ onDrop }) => { + const [drag, setDrag] = React.useState(false); + const [filename, setFilename] = React.useState(''); + let dropRef = React.createRef(); + let dragCounter = 0; + + const handleDrag = e => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDragIn = e => { + e.preventDefault(); + e.stopPropagation(); + dragCounter++; + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) setDrag(true); + }; + + const handleDragOut = e => { + e.preventDefault(); + e.stopPropagation(); + dragCounter--; + if (dragCounter === 0) setDrag(false); + }; + + const handleDrop = e => { + e.preventDefault(); + e.stopPropagation(); + setDrag(false); + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + onDrop(e.dataTransfer.files[0]); + setFilename(e.dataTransfer.files[0].name); + e.dataTransfer.clearData(); + dragCounter = 0; + } + }; + + React.useEffect(() => { + let div = dropRef.current; + div.addEventListener('dragenter', handleDragIn); + div.addEventListener('dragleave', handleDragOut); + div.addEventListener('dragover', handleDrag); + div.addEventListener('drop', handleDrop); + return () => { + div.removeEventListener('dragenter', handleDragIn); + div.removeEventListener('dragleave', handleDragOut); + div.removeEventListener('dragover', handleDrag); + div.removeEventListener('drop', handleDrop); + }; + }); + + return ( +
      + {filename && !drag ?
      {filename}
      :
      Drop a file here!
      } +
      + ); +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + +); +``` diff --git a/react/snippets/lazy-load-image.md b/react/snippets/lazy-load-image.md new file mode 100644 index 000000000..0382cb8cd --- /dev/null +++ b/react/snippets/lazy-load-image.md @@ -0,0 +1,79 @@ +--- +title: Lazy-loading image +type: snippet +tags: [components,effect,state] +cover: strawberries +author: chalarangelo +dateModified: 2022-07-29T05:00:00-04:00 +--- + +Renders an image that supports lazy loading. + +- Use the `useState()` hook to create a stateful value that indicates if the image has been loaded. +- Use the `useEffect()` hook to check if the `HTMLImageElement.prototype` contains `'loading'`, effectively checking if lazy loading is supported natively. If not, create a new `IntersectionObserver` and use `IntersectionObserver.observer()` to observer the `` element. Use the `return` value of the hook to clean up when the component unmounts. +- Use the `useCallback()` hook to memoize a callback function for the `IntersectionObserver`. This callback will update the `isLoaded` state variable and use `IntersectionObserver.disconnect()` to disconnect the `IntersectionObserver` instance. +- Use the `useRef()` hook to create two refs. One will hold the `` element and the other the `IntersectionObserver` instance, if necessary. +- Finally, render the `` element with the given attributes. Apply `loading='lazy'` to make it load lazily, if necessary. Use `isLoaded` to determine the value of the `src` attribute. + +```jsx +const LazyLoadImage = ({ + alt, + src, + className, + loadInitially = false, + observerOptions = { root: null, rootMargin: '200px 0px' }, + ...props +}) => { + const observerRef = React.useRef(null); + const imgRef = React.useRef(null); + const [isLoaded, setIsLoaded] = React.useState(loadInitially); + + const observerCallback = React.useCallback( + entries => { + if (entries[0].isIntersecting) { + observerRef.current.disconnect(); + setIsLoaded(true); + } + }, + [observerRef] + ); + + React.useEffect(() => { + if (loadInitially) return; + + if ('loading' in HTMLImageElement.prototype) { + setIsLoaded(true); + return; + } + + observerRef.current = new IntersectionObserver( + observerCallback, + observerOptions + ); + observerRef.current.observe(imgRef.current); + return () => { + observerRef.current.disconnect(); + }; + }, []); + + return ( + {alt} + ); +}; +``` + +```jsx +ReactDOM.createRoot(document.getElementById('root')).render( + +); +``` diff --git a/react/snippets/limited-textarea.md b/react/snippets/limited-textarea.md new file mode 100644 index 000000000..6f6f263b7 --- /dev/null +++ b/react/snippets/limited-textarea.md @@ -0,0 +1,46 @@ +--- +title: Textarea with character limit +type: snippet +tags: [components,state,callback,event] +cover: flower-portrait-2 +dateModified: 2021-10-13T19:29:39+02:00 +--- + +Renders a textarea component with a character limit. + +- Use the `useState()` hook to create the `content` state variable. Set its value to that of `value` prop, trimmed down to `limit` characters. +- Create a method `setFormattedContent`, which trims the content down to `limit` characters and memoize it, using the `useCallback()` hook. +- Bind the `onChange` event of the `