PageSourceSearch

https://react-datasheet-grid.netlify.app/assets/js/32918f21.d0368b44.js

js react-datasheet-grid.netlify.app collected 2026-10-03 11:19:57 UTC 33,708 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[871],{12873:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>V,contentTitle:()=>P,default:()=>O,frontMatter:()=>R,metadata:()=>W,toc:()=>I});var a=t(87462),o=t(67294),l=t(3905),r=t(6467),s=t(37186),i=t(91262);const c=o.memo((e=>{let{active:n,rowData:t,setRowData:a,focus:l,stopEditing:r,columnData:c}=e;const u=(0,o.useRef)(null);return(0,o.useLayoutEffect)((()=>{l?u.current?.focus():u.current?.blur()}),[l]),o.createElement(i.Z,null,(()=>o.createElement(s.ZP,{ref:u,styles:{container:e=>({...e,flex:1,alignSelf:"stretch",pointerEvents:l?void 0:"none"}),control:e=>({...e,height:"100%",border:"none",boxShadow:"none",background:"none"}),indicatorSeparator:e=>({...e,opacity:0}),indicatorsContainer:e=>({...e,opacity:n?1:0}),placeholder:e=>({...e,opacity:n?1:0})},isDisabled:c.disabled,value:c.choices.find((e=>{let{value:n}=e;return n===t}))??null,menuPortalTarget:document.body,menuIsOpen:l,onChange:e=>{let{value:n}=e;a(n),setTimeout(r,0)},onMenuClose:()=>r({nextRow:!1}),options:c.choices})))}));c.displayName="SelectComponent";const u=[{value:"chocolate",label:"Chocolate"},{value:"strawberry",label:"Strawberry"},{value:"vanilla",label:"Vanilla"}],p=e=>({component:c,columnData:e,disableKeys:!0,keepFocus:!0,disabled:e.disabled,deleteValue:()=>null,copyValue:n=>{let{rowData:t}=n;return e.choices.find((e=>e.value===t))?.label},pasteValue:n=>{let{value:t}=n;return e.choices.find((e=>e.label===t))?.value??null}}),d=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{...p({choices:u}),title:"Flavor"}]}))},m=()=>{const[e,n]=(0,o.useState)([{flavor:"chocolate",quantity:3},{flavor:"strawberry",quantity:5},{flavor:null,quantity:null}]),t=[{...(0,r.vU)("flavor",p({choices:u})),title:"Flavor"},{...(0,r.vU)("quantity",r._),title:"Quantity"}];return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:t}))},h=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:s.ZP,title:"Flavor"}]}))},g=()=>o.createElement(s.ZP,{styles:{container:e=>({...e,flex:1,alignSelf:"stretch"}),control:e=>({...e,height:"100%",border:"none",boxShadow:"none",background:"none"}),indicatorSeparator:e=>({...e,opacity:0})}}),f=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:g,title:"Flavor"}]}))},y=e=>{let{active:n,focus:t}=e;return o.createElement(s.ZP,{styles:{container:e=>({...e,flex:1,alignSelf:"stretch",pointerEvents:t?void 0:"none"}),control:e=>({...e,height:"100%",border:"none",boxShadow:"none",background:"none"}),indicatorSeparator:e=>({...e,opacity:0}),indicatorsContainer:e=>({...e,opacity:n?1:0}),placeholder:e=>({...e,opacity:n?1:0})},options:u})},v=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null,null,null,null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:y,title:"Flavor"}]}))},k=e=>{let{active:n,focus:t}=e;return o.createElement(i.Z,null,(()=>o.createElement(s.ZP,{styles:{container:e=>({...e,flex:1,alignSelf:"stretch",pointerEvents:t?void 0:"none"}),control:e=>({...e,height:"100%",border:"none",boxShadow:"none",background:"none"}),indicatorSeparator:e=>({...e,opacity:0}),indicatorsContainer:e=>({...e,opacity:n?1:0}),placeholder:e=>({...e,opacity:n?1:0})},menuPortalTarget:document.body,menuIsOpen:t,options:u})))},b=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:k,title:"Flavor"}]}))},w=e=>{let{active:n,focus:t}=e;const a=(0,o.useRef)(null);return(0,o.useLayoutEffect)((()=>{t?a.current?.focus():a.current?.blur()}),[t]),o.createElement(i.Z,null,(()=>o.createElement(s.ZP,{ref:a,styles:{container:e=>({...e,flex:1,alignSelf:"stretch",pointerEvents:t?void 0:"none"}),control:e=>({...e,height:"100%",border:"none",boxShadow:"none",background:"none"}),indicatorSeparator:e=>({...e,opacity:0}
1),indicatorsContainer:e=>({...e,opacity:n?1:0}),placeholder:e=>({...e,opacity:n?1:0})},menuPortalTarget:document.body,menuIsOpen:t,options:u})))},S=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:w,title:"Flavor"}]}))},C=e=>{let{active:n,focus:t,stopEditing:a}=e;const l=(0,o.useRef)(null);return(0,o.useLayoutEffect)((()=>{t?l.current?.focus():l.current?.blur()}),[t]),o.createElement(i.Z,null,(()=>o.createElement(s.ZP,{ref:l,styles:{container:e=>({...e,flex:1,alignSelf:"stretch",pointerEvents:t?void 0:"none"}),control:e=>({...e,height:"100%",border:"none",boxShadow:"none",background:"none"}),indicatorSeparator:e=>({...e,opacity:0}),indicatorsContainer:e=>({...e,opacity:n?1:0}),placeholder:e=>({...e,opacity:n?1:0})},menuPortalTarget:document.body,onMenuClose:()=>a({nextRow:!1}),menuIsOpen:t,options:u})))},x=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:C,keepFocus:!0,title:"Flavor"}]}))},N=e=>{let{active:n,focus:t,stopEditing:a}=e;const l=(0,o.useRef)(null);return(0,o.useLayoutEffect)((()=>{t?l.current?.focus():l.current?.blur()}),[t]),o.createElement(i.Z,null,(()=>o.createElement(s.ZP,{ref:l,styles:{container:e=>({...e,flex:1,alignSelf:"stretch",pointerEvents:t?void 0:"none"}),control:e=>({...e,height:"100%",border:"none",boxShadow:"none",background:"none"}),indicatorSeparator:e=>({...e,opacity:0}),indicatorsContainer:e=>({...e,opacity:n?1:0}),placeholder:e=>({...e,opacity:n?1:0})},menuPortalTarget:document.body,menuIsOpen:t,onMenuClose:()=>a({nextRow:!1}),options:u})))},D=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:N,disableKeys:!0,keepFocus:!0,title:"Flavor"}]}))},E=e=>{let{active:n,focus:t,rowData:a,setRowData:l,stopEditing:r}=e;const c=(0,o.useRef)(null);return(0,o.useLayoutEffect)((()=>{t?c.current?.focus():c.current?.blur()}),[t]),o.createElement(i.Z,null,(()=>o.createElement(s.ZP,{ref:c,styles:{container:e=>({...e,flex:1,alignSelf:"stretch",pointerEvents:t?void 0:"none"}),control:e=>({...e,height:"100%",border:"none",boxShadow:"none",background:"none"}),indicatorSeparator:e=>({...e,opacity:0}),indicatorsContainer:e=>({...e,opacity:n?1:0}),placeholder:e=>({...e,opacity:n?1:0})},menuPortalTarget:document.body,value:u.find((e=>{let{value:n}=e;return n===a}))??null,menuIsOpen:t,onChange:e=>{let{value:n}=e;l(n),setTimeout(r,0)},onMenuClose:()=>r({nextRow:!1}),options:u})))},T=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:E,disableKeys:!0,keepFocus:!0,title:"Flavor"}]}))},F=()=>{const[e,n]=(0,o.useState)(["chocolate","strawberry",null]);return o.createElement("div",{style:{marginBottom:20}},o.createElement(r.co,{value:e,onChange:n,columns:[{component:E,disableKeys:!0,keepFocus:!0,deleteValue:()=>null,copyValue:e=>{let{rowData:n}=e;return u.find((e=>e.value===n))?.label},pasteValue:e=>{let{value:n}=e;return u.find((e=>e.label===n))?.value??null},title:"Flavor"}]}))},R={sidebar_position:3},P="Implementing a select column",W={unversionedId:"examples/implementing-select",id:"examples/implementing-select",title:"Implementing a select column",description:"The goal here is to implement a simple select that can serve as a base for a prod ready",source:"@site/docs/examples/implementing-select.mdx",sourceDirName:"examples",slug:"/examples/implementing-select",permalink:"/docs/examples/implementing-select",draft:!1,editUrl:"https://github.com/nick-keller/react-datasheet-grid/edit/master/website/docs/examples/implementing-select.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{sidebar_position:3},sidebar:"tutorialSidebar",previous:{title:"Adding unique ids to rows",permalink:"/docs/examples/unique-ids"},next:{title:"Tracking rows changes",permalink:"/docs/examples/tracking-rows-changes"}},V={},I=[{value:"Setup",id:"setup",level:2},{value:"Styling",id:"styling",level:2},{value:"Fixing the menu",id:"fixing-the-menu",level:2},{value:"Fixing the search",id:"fixing-the-search",level:2},{value:"Keeping focus",id:"keeping-focus",level:2},{value:"Fixing the up and down keys",id:"fixing-the-up-and-down-keys",level:2},{value:"Binding data",id:"binding-data",level:2},{value:"Handling copy pasting",id:"handling-copy-pasting",level:2},{value:"Making the column generic",id:"making-the-column-generic",level:2},{value:"Final result",id:"final-result",level:2},{value:"Using with multiple columns",id:"using-with-multiple-columns",level:2}],B={toc:I};
1function O(e){let{components:n,...t}=e;return(0,l.kt)("wrapper",(0,a.Z)({},B,t,{components:n,mdxType:"MDXLayout"}),(0,l.kt)("h1",{id:"implementing-a-select-column"},"Implementing a select column"),(0,l.kt)("p",null,"The goal here is to implement a simple select that can serve as a base for a prod ready\ncomponent of your application. It will be easily extensible, and re-usable anywhere in your app.\nThe end result will look like this:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"function App() {\n  // `data` is a simple array of `string | null`, in a real world example\n  // we would probably have multiple columns and use objects instead\n  // See section => Using with multiple columns\n  const [ data, setData ] = useState(['chocolate', 'strawberry', null])\n\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={[\n        {\n          // We use our custom select column \ud83c\udf89\n          ...selectColumn({\n            choices: [\n              { value: 'chocolate', label: 'Chocolate' },\n              { value: 'strawberry', label: 'Strawberry' },\n              { value: 'vanilla', label: 'Vanilla' },\n            ],\n          }),\n          // ...and add a title\n          title: 'Flavor',\n        },\n      ]}\n    />\n  )\n}\n")),(0,l.kt)(d,{mdxType:"FinalResult"}),(0,l.kt)("h2",{id:"setup"},"Setup"),(0,l.kt)("p",null,"We will be using the popular ",(0,l.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/react-select"},"react-select")," library. First we need to\ninstall it:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre"},"npm i react-select\n\n# Using Typescript?\nnpm i --save-dev @types/react-select\n")),(0,l.kt)("p",null,"The rest of this tutorial will be using ",(0,l.kt)("strong",{parentName:"p"},"Typescript"),", if your are using plain JS simply strip away any type\nannotation and you should be good to go."),(0,l.kt)("p",null,"Now we can simply use the ",(0,l.kt)("inlineCode",{parentName:"p"},"Select")," component in a column using the ",(0,l.kt)("a",{parentName:"p",href:"../api-reference/columns#component"},"component")," prop:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"import Select from 'react-select'\n\nfunction App() {\n  const [ data, setData ] = useState(['chocolate', 'strawberry', null])\n\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={[\n        {\n          component: Select\n          title: 'Flavor',\n        },\n      ]}\n    />\n  )\n}\n")),(0,l.kt)(h,{mdxType:"Step1"}),(0,l.kt)("p",null,"The result is not great but at least it works, we simply need to do a little styling. To do so we need to customize the\nprops and behavior of the ",(0,l.kt)("inlineCode",{parentName:"p"},"Select")," component by wraping it in a component of our own:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx",metastring:"{3-5,16}","{3-5,16}":!0},"import Select from 'react-select'\n\nconst SelectComponent = () => {\n  return <Select />\n}\n\nfunction App() {\n  const [ data, setData ] = useState(['chocolate', 'strawberry', null])\n\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={[\n        {\n          component: SelectComponent,\n          title: 'Flavor',\n        },\n      ]}\n    />\n  )\n}\n")),(0,l.kt)("p",null,"We now have a solid base to start styling!"),(0,l.kt)("h2",{id:"styling"},"Styling"),(0,l.kt)("p",null,"According to ",(0,l.kt)("a",{parentName:"p",href:"https://react-select.com/styles"},"react-select doc")," we can use the ",(0,l.kt)("inlineCode",{parentName:"p"},"styles")," prop to style any part of the\nselect component. So far we only need to style 3 elements:"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},(0,l.kt)("strong",{parentName:"li"},"container"),": make it take the full width and height of the cell"),(0,l.kt)("li",{parentName:"ul"},(0,l.kt)("strong",{parentName:"li"},"control"),": make it take the full height of the container and remove any border"),(0,l.kt)("li",{parentName:"ul"},(0,l.kt)("strong",{parentName:"li"},"indicatorSeparator"),": just hide it using opacity, it's easier on the eye")),(0,l.kt)("pre",null,(0,l.kt)("c
1ode",{parentName:"pre",className:"language-tsx"},"const SelectComponent = () => {\n  return (\n    <Select\n      styles={{\n        container: (provided) => ({\n          ...provided,\n          flex: 1, // full width\n          alignSelf: 'stretch', // full height\n        }),\n        control: (provided) => ({\n          ...provided,\n          height: '100%',\n          border: 'none',\n          boxShadow: 'none',\n          background: 'none',\n        }),\n        indicatorSeparator: (provided) => ({\n          ...provided,\n          opacity: 0,\n        }),\n      }}\n    />\n  )\n}\n")),(0,l.kt)(f,{mdxType:"Step2"}),(0,l.kt)("p",null,"Interacting with our select does not feel right, but it is already looking a lot better. To fix this issue\nwe need to prevent the mouse cursor from interacting with the select unless the cell is focused. We can use the ",(0,l.kt)("a",{parentName:"p",href:"../api-reference/cell-components#focus"},"focus")," prop to set\nthe CSS property ",(0,l.kt)("a",{parentName:"p",href:"https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events"},(0,l.kt)("inlineCode",{parentName:"a"},"pointerEvents"))," to ",(0,l.kt)("inlineCode",{parentName:"p"},"none"),"\nwhen the cell is not focused:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx",metastring:"{11}","{11}":!0},"import { CellProps } from 'react-datasheet-grid'\n\nconst SelectComponent = ({ focus }: CellProps) => {\n  return (\n    <Select\n      styles={{\n        container: (provided) => ({\n          ...provided,\n          flex: 1,\n          alignSelf: 'stretch',\n          pointerEvents: focus ? undefined : 'none',\n        }),\n        /*...*/\n      }}\n    />\n  )\n}\n")),(0,l.kt)("p",null,'If we add 10 rows to our datasheet grid we see that the "Select..." placeholder and the caret look very repetitive.\nIt is recommended to only show placeholders on active cells to avoid this effect. We can use the ',(0,l.kt)("a",{parentName:"p",href:"../api-reference/cell-components#active"},"active")," prop to do so:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx",metastring:"{8-15}","{8-15}":!0},"import { CellProps } from 'react-datasheet-grid'\n\nconst SelectComponent = ({ active }: CellProps) => {\n  return (\n    <Select\n      styles={{\n        /*...*/\n        indicatorsContainer: (provided) => ({\n          ...provided,\n          opacity: active ? 1 : 0,\n        }),\n        placeholder: (provided) => ({\n          ...provided,\n          opacity: active ? 1 : 0,\n        }),\n      }}\n    />\n  )\n}\n")),(0,l.kt)("p",null,"Finally we can add some options just for fun:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"const SelectComponent = () => {\n  return (\n    <Select\n      /*...*/\n      options={[\n        { value: 'chocolate', label: 'Chocolate' },\n        { value: 'strawberry', label: 'Strawberry' },\n        { value: 'vanilla', label: 'Vanilla' },\n      ]}\n    />\n  )\n}\n")),(0,l.kt)(v,{mdxType:"Step3"}),(0,l.kt)("p",null,"The end result is a placeholder that is only visible when the cell is active, and a component you can\nonly interact with while the cell is focused."),(0,l.kt)("h2",{id:"fixing-the-menu"},"Fixing the menu"),(0,l.kt)("p",null,"It now takes 3 clicks to open the menu: click once to select the cell, click again to focus, and click\nagain to open the menu. We can fix that by using the ",(0,l.kt)("inlineCode",{parentName:"p"},"focus")," prop of our component to control the ",(0,l.kt)("inlineCode",{parentName:"p"},"menuIsOpen")," prop\nof the select."),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx",metastring:"{5}","{5}":!0},"const SelectComponent = ({ focus }: CellProps) => {\n  return (\n    <Select\n      /*...*/\n      menuIsOpen={focus}\n    />\n  )\n}\n")),(0,l.kt)("p",null,"We have another issue: if you try to open the select of the last row you have to scroll to\nsee the menu because it is contained within the scrollable area of the grid."),(0,l.kt)("p",null,"The solution is to use ",(0,l.kt)("a",{parentName:"p",href:"https://reactjs.org/docs/portals.html"},"portals"),". Thankfully react-select has this feature\nbuilt-in and we simply need to add a prop:"),(0,l.kt)("pre",null,(0,l.kt)("c
1ode",{parentName:"pre",className:"language-tsx",metastring:"{5}","{5}":!0},"const SelectComponent = () => {\n  return (\n    <Select\n      /*...*/\n      menuPortalTarget={document.body}\n    />\n  )\n}\n")),(0,l.kt)(b,{mdxType:"Step4"}),(0,l.kt)("p",null,"The menu is now displayed correctly and automatically opens when the cell is focused. You can use the return\nand escape keys to open and close the menu!"),(0,l.kt)("h2",{id:"fixing-the-search"},"Fixing the search"),(0,l.kt)("p",null,"When you start typing the menu open because the cell gains focus, but nothing else happens, you have to manually click\non the input to start searching."),(0,l.kt)("p",null,"We have the same issue when we close the menu by pressing Esc, the input is till in focus with a blinking cursor."),(0,l.kt)("p",null,"To fix this issue we simply need to manually focus and blur the input when we gain or loose focus.\nWe can use a ",(0,l.kt)("inlineCode",{parentName:"p"},"ref")," of the select element along with ",(0,l.kt)("inlineCode",{parentName:"p"},"useLayoutEffect")," to achieve that:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"import React, { useLayoutEffect, useRef } from 'react'\n\nconst SelectComponent = ({ focus }: CellProps) => {\n  const ref = useRef<Select>(null)\n\n  // This function will be called only when `focus` changes\n  useLayoutEffect(() => {\n    if (focus) {\n      ref.current?.focus()\n    } else {\n      ref.current?.blur()\n    }\n  }, [focus])\n\n  return (\n    <Select\n      ref={ref}\n      /*...*/\n    />\n  )\n}\n")),(0,l.kt)(S,{mdxType:"Step5"}),(0,l.kt)("h2",{id:"keeping-focus"},"Keeping focus"),(0,l.kt)("p",null,"Now that the menu renders in a portal, any click in the menu will be considered to be outside of the grid and react-datasheet-grid\nwill blur the cell, closing the menu. Luckily for us react-select prevent this from happening by blocking the click event\non the menu, as you can see it stays open when you select a choice."),(0,l.kt)("p",null,'But for the sake of example we are going to still "fix" this issue that might occur if you use another library or decide to\nimplement your own.'),(0,l.kt)("p",null,"We use the ",(0,l.kt)("a",{parentName:"p",href:"../api-reference/columns#keepfocus"},"keepFocus")," property of the column to keep focus even when we click outside\nof the grid. We now have to call ",(0,l.kt)("a",{parentName:"p",href:"../api-reference/cell-components#stopediting"},"stopEditing")," ourself when the user clicks\noutside of the select. Notice the ",(0,l.kt)("inlineCode",{parentName:"p"},"nextRow")," property that allows us to stay on the same cell instead of going to the next."),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx",metastring:"{7,22}","{7,22}":!0},"const SelectComponent = ({ stopEditing }: CellProps) => {\n  /*...*/\n\n  return (\n    <Select\n      /*...*/\n      onMenuClose={() => stopEditing({ nextRow: false })}\n    />\n  )\n}\n\nfunction App() {\n  const [ data, setData ] = useState(['chocolate', 'strawberry', null])\n\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={[\n        {\n          component: SelectComponent,\n          keepFocus: true,\n          title: 'Flavor',\n        },\n      ]}\n    />\n  )\n}\n")),(0,l.kt)(x,{mdxType:"Step6"}),(0,l.kt)("h2",{id:"fixing-the-up-and-down-keys"},"Fixing the up and down keys"),(0,l.kt)("p",null,"The last UX issue we face is that using the up and down keys to select a choice moves the active\ncell up or down, and as a result closes the select. To fix this we simply need to disable keys when the cell is focused\nusing the ",(0,l.kt)("a",{parentName:"p",href:"../api-reference/columns#disablekeys"},"disableKeys")," option of the column:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx",metastring:"{11}","{11}":!0},"function App() {\n  const [ data, setData ] = useState(['chocolate', 'strawberry', null])\n\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={[\n        {\n          component: SelectComponent,\n          disableKeys: true,\n          title: 'Flavor',\n        },\n      ]}\n    />\n  )\n}\n")),(0,l.kt)(D,{mdxType:"Step7"}),(0,l.kt)("p",null,"Now we can freely use the up and down arrows, but the ",(0,l.kt)("inlineCode",{parentName:"p"},"Enter")," key is also ignored when we try to select a value.\nWe will fix this in next section."),(0,l.kt)("h2",{id:"binding-data"},"Binding data"),(0,l.kt)("p",null,"Until this point the underlying data (our state) was not modified. It looks like the select is working just fine because it\nkeeps an inner state to show the value that has just been selected, but two points can give it away:"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},"The default values we specified in ",(0,l.kt)("inlineCode",{parentName:"li"},"useState")," are not shown: the two first rows should be Chocolate and Strawberry"),(0,l.kt)("li",{parentName:"ul"},"If you add 20 rows, scroll down and scroll back up the selected value is gone. This is because the rows that are not in view\nare deleted and created back when the user scrolls back to them, resetting the inner state of the select component")),(0,l.kt)("p",null,"Wee need to bind the right values to our select component:"),(0,l.kt)("pre",null,(0,l.kt)("c
1ode",{parentName:"pre",className:"language-tsx"},"// For now we just took the choices outside of the component because we\n// need to refer to it multiple times (this is not the final design, but it will do for now)\nconst choices = [\n  { value: 'chocolate', label: 'Chocolate' },\n  { value: 'strawberry', label: 'Strawberry' },\n  { value: 'vanilla', label: 'Vanilla' },\n]\n\nconst SelectComponent = ({ rowData }: CellProps) => {\n  /*...*/\n\n  return (\n    <Select\n      /*...*/\n      value={\n        // We cannot just pass the rowData as value\n        // We have to pass the entire choice object { label, value }\n        choices.find(({ value }) => value === rowData) ?? null\n      }\n      options={choices}\n    />\n  )\n}\n")),(0,l.kt)("p",null,"We also need to bind the ",(0,l.kt)("inlineCode",{parentName:"p"},"onChange")," callback of the select to update the value and close the select. We use the\n",(0,l.kt)("a",{parentName:"p",href:"../api-reference/cell-components#setrowdata"},"setRowData")," and ",(0,l.kt)("a",{parentName:"p",href:"../api-reference/cell-components#stopediting"},"stopEditing"),"\nto achieve that:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"const SelectComponent = ({ setRowData, stopEditing }: CellProps) => {\n  /*...*/\n\n  return (\n    <Select\n      /*...*/\n      onChange={({ value }) => {\n        setRowData(value)\n        // We don't just do `stopEditing()` because it is triggered too early by react-select\n        setTimeout(stopEditing, 0)\n      }}\n    />\n  )\n}\n")),(0,l.kt)(T,{mdxType:"Step8"}),(0,l.kt)("h2",{id:"handling-copy-pasting"},"Handling copy pasting"),(0,l.kt)("p",null,"Handling copy pasting is very straight forward, we just need to implement three functions:"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},(0,l.kt)("strong",{parentName:"li"},"deleteValue"),": will be called when cutting or deleting a cell. We want return null as a deleted value."),(0,l.kt)("li",{parentName:"ul"},(0,l.kt)("strong",{parentName:"li"},"copyValue"),": will be called when the cell is copied. Here we decided to return the label of the choice, not the underlying value."),(0,l.kt)("li",{parentName:"ul"},(0,l.kt)("strong",{parentName:"li"},"pasteValue"),": will be called to handle pasting. Because we chose to copy the label, we have to transform it back to a value on pasting.")),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx",metastring:"{12-16}","{12-16}":!0},"function App() {\n  const [ data, setData ] = useState(['chocolate', 'strawberry', null])\n\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={[\n        {\n          component: SelectComponent,\n          disableKeys: true,\n          deleteValue: () => null,\n          copyValue: ({ rowData }) =>\n            choices.find((choice) => choice.value === rowData)?.label,\n          pasteValue: ({ value }) =>\n            choices.find((choice) => choice.label === value)?.value ?? null,\n          title: 'Flavor',\n        },\n      ]}\n    />\n  )\n}\n")),(0,l.kt)(F,{mdxType:"Step9"}),(0,l.kt)("h2",{id:"making-the-column-generic"},"Making the column generic"),(0,l.kt)("p",null,"Now we need to make our column re-usable. To make a column re-usable we often expose a function that takes params and returns a column object:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"type SelectOptions = {\n  choices: Choice[]\n  // Let's add more options!\n  disabled?: boolean\n}\n\nconst selectColumn = (\n  // We receive the options as a parameter to create the column object\n  options: SelectOptions\n): Column<string | null, SelectOptions> => ({\n  component: SelectComponent,\n  // We pass the options to the cells using the `columnData`property\n  columnData: options,\n  // We set other column properties so we don't have to do it manually everytime we use the column\n  disableKeys: true,\n  keepFocus: true,\n  // We can also use the options to customise some properties\n  disabled: options.disabled,\n  deleteValue: () => null,\n  copyValue: ({ rowData }) =>\n    options.choices.find((choice) => choice.value === rowData)?.label,\n  pasteValue: ({ value }) =>\n    options.choices.find((choice) => choice.label === value)?.value ?? null,\n})\n")),(0,l.kt)("p",null,"We also have to update the ",(0,l.kt)("inlineCode",{parentName:"p"},"SelectComponent")," to get the options from ",(0,l.kt)("inlineCode",{parentName:"p"}
1,"columnData"),":"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"\nconst SelectComponent = React.memo(\n  ({ columnData, rowData }: CellProps<string | null, SelectOptions>) => {\n  /*...*/\n\n    return (\n      <Select\n        /*...*/\n        isDisabled={columnData.disabled}\n        value={\n          columnData.choices.find(({ value }) => value === rowData) ?? null\n        }\n        options={columnData.choices}\n      />\n    )\n  }\n)\n")),(0,l.kt)("p",null,"Now our column is truly re-usable:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"function App() {\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={[\n        selectColumn({ choices: [/*..*/], disabled: true })\n      ]}\n    />\n  )\n}\n")),(0,l.kt)("p",null,"We can also extend the column to add a title, a custom min-width, or override any property:"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"function App() {\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={[\n        {\n          ...selectColumn({ choices: [/*..*/], disabled: true }),\n          title: 'Flavor',\n          minWidth: 150,\n        }\n      ]}\n    />\n  )\n}\n")),(0,l.kt)("h2",{id:"final-result"},"Final result"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"import React, { useLayoutEffect, useRef, useState } from 'react'\nimport { DataSheetGrid, CellProps, Column } from 'react-datasheet-grid'\nimport Select, { GroupBase, SelectInstance } from 'react-select'\n\ntype Choice = {\n  label: string\n  value: string\n}\n\ntype SelectOptions = {\n  choices: Choice[]\n  disabled?: boolean\n}\n\nconst SelectComponent = React.memo(\n  ({\n    active,\n    rowData,\n    setRowData,\n    focus,\n    stopEditing,\n    columnData,\n  }: CellProps<string | null, SelectOptions>) => {\n    const ref = useRef<SelectInstance<Choice, false, GroupBase<Choice>>>(null)\n\n    useLayoutEffect(() => {\n      if (focus) {\n        ref.current?.focus()\n      } else {\n        ref.current?.blur()\n      }\n    }, [focus])\n\n    return (\n      <Select\n        ref={ref}\n        styles={{\n          container: (provided) => ({\n            ...provided,\n            flex: 1,\n            alignSelf: 'stretch',\n            pointerEvents: focus ? undefined : 'none',\n          }),\n          control: (provided) => ({\n            ...provided,\n            height: '100%',\n            border: 'none',\n            boxShadow: 'none',\n            background: 'none',\n          }),\n          indicatorSeparator: (provided) => ({\n            ...provided,\n            opacity: 0,\n          }),\n          indicatorsContainer: (provided) => ({\n            ...provided,\n            opacity: active ? 1 : 0,\n          }),\n          placeholder: (provided) => ({\n            ...provided,\n            opacity: active ? 1 : 0,\n          }),\n        }}\n        isDisabled={columnData.disabled}\n        value={\n          columnData.choices.find(({ value }) => value === rowData) ?? null\n        }\n        menuPortalTarget={document.body}\n        menuIsOpen={focus}\n        onChange={(choice) => {\n          if (choice === null) return;\n\n          setRowData(choice.value);\n          setTimeout(stopEditing, 0);\n          }}\n        onMenuClose={() => stopEditing({ nextRow: false })}\n        options={columnData.choices}\n      />\n    )\n  }\n)\n\nconst selectColumn = (\n  options: SelectOptions\n): Column<string | null, SelectOptions> => ({\n  component: SelectComponent,\n  columnData: options,\n  disableKeys: true,\n  keepFocus: true,\n  disabled: options.disabled,\n  deleteValue: () => null,\n  copyValue: ({ rowData }) =>\n    options.choices.find((choice) => choice.value === rowData)?.label ?? null,\n  pasteValue: ({ value }) =>\n    options.choices.find((choice) => choice.label === value)?.value ?? null,\n})\n\nfunction App() {\n  const [data, setData] = useState<Array<string | null>>([\n    'chocolate',\n    'strawberry',\n    null,\n  ])\n\n  return (\n    <div style={{ marginBottom: 20 }}>\n      <DataSheetGrid\n        value={data}\n        onChange={setData}\n        columns={[\n          {\n            ...selectColumn({\n              choices: [\n                { value: 'chocolate', label: 'Chocolate' },\n                { value: 'strawberry', label: 'Strawberry' },\n                { value: 'vanilla', label: 'Vanilla' },\n              ],\n            }),\n            title: 'Flavor',\n          },\n        ]}\n      />\n    </div>\n  )\n}\n")),(0,l.kt)(d,{mdxType:"FinalResult"}),(0,l.kt)("h2",{id:"using-with-multiple-columns"},"Using with multiple columns"),(0,l.kt)("p",null,"So far we have been using our select column on rows that are strings or null, that works well if we have only one column.\nOf course in a real world situation you probably want to have multiple columns and use objects instead. Fortunately\ntheir is no extra work needed to make it work, simply use ",(0,l.kt)("inlineCode",{parentName:"p"}
1,"keyColumn"),":"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-tsx"},"import {\n  DataSheetGrid,\n  Column,\n  keyColumn,\n  intColumn,\n} from 'react-datasheet-grid'\n\ntype Row = {\n  flavor: string | null\n  quantity: number | null\n}\n\nfunction App() {\n  const [data, setData] = useState<Row[]>([\n    { flavor: 'chocolate', quantity: 3 },\n    { flavor: 'strawberry', quantity: 5 },\n    { flavor: null, quantity: null },\n  ])\n\n  const columns: Column<Row>[] = [\n    {\n      ...keyColumn('flavor', selectColumn({ choices: [/*...*/] })),\n      title: 'Flavor',\n    },\n    {\n      ...keyColumn('quantity', intColumn),\n      title: 'Quantity',\n    },\n  ]\n\n  return (\n    <DataSheetGrid\n      value={data}\n      onChange={setData}\n      columns={columns}\n    />\n  )\n}\n")),(0,l.kt)(m,{mdxType:"MultipleColumns"}))}O.isMDXComponent=!0}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.