Aguarde...

2 de novembro de 2021

Como criar uma zona de depósito de arquivos no React e TypeScript

Como criar uma zona de depósito de arquivos no React e TypeScript

Existem várias soluções para dropzones de arquivos. Alguns são simples, outros complicados. Este tutorial o ajudará a criar sua própria zona de depósito de arquivos simples. Você aprenderá como lidar com vários eventos de arrastar e soltar, como processar arquivos soltos e como criar uma API simples para o componente de zona de soltar reutilizável.

Uma breve introdução

Neste tutorial, criaremos uma zona de lançamento de arquivos simples do zero, sem nenhuma dependência especial. Vamos criar este aplicativo usando o create- react -app , com o template TypeScript ( --template typescriptflag). Isso nos dará quase todos os recursos de que precisamos.

Junto com as dependências padrão do React e do TypeScript, também adicionaremos a biblioteca de nomes de classe . Usaremos esta biblioteca para acrescentar classes à zona de lançamento de arquivos quando ela estiver ativa. Isso significa quando alguém arrasta um arquivo sobre ele. Esta classe aplicará alguns estilos CSS para destacar a zona de lançamento.

Usar o create-react-appmodelo irá gerar algumas coisas que podemos remover. Isso inclui o logotipo e o conteúdo de App.tsx. No entanto, você pode deixar o conteúdo do componente App como está por enquanto. Vamos substituí-lo mais tarde pela zona de lançamento de arquivos e a lista de arquivos. Agora, vamos dar uma olhada na zona de lançamento.

Criando componente Dropzone

A ideia de um componente de dropzone de arquivo personalizado pode parecer complicada. No entanto, isso não é necessariamente verdade. A lógica para a zona de soltar exigirá que tratemos de poucos eventos de arrastar e soltar, algum gerenciamento de estado simples para o estado ativo e processamento de arquivos soltos. É basicamente isso.

Para gerenciamento de estado, usaremos o gancho React useState . Em seguida, também usaremos o gancho useEffect para anexar ouvintes de eventos e observar o estado da zona de lançamento. Por último, também memorizaremos cada componente usando o memo HOC. Vamos começar a construir.

Começando

A primeira coisa que precisamos é definir o componente da zona de lançamento de arquivos. Isso também inclui a definição de alguma interface para sua propsAPI ou componente API. O componente dropzone aceitará seis manipuladores de eventos. Quatro desses manipuladores será invocado em eventos tais como dragenterdragleavedragoverdrop.

Esses manipuladores permitirão que qualquer pessoa que use esse componente dropzone execute algum código quando esses eventos forem disparados. O quinto e o sexto manipulador serão sintéticos. Um será chamado quando o estado da zona de lançamento ativa for alterado. Isso significa quando alguém está arrastando um arquivo sobre ele e quando o arrasto termina.

Sempre que isso acontecer, o manipulador para isso será chamado, ele passará o valor booleano especificando o estado ativo / não ativo atual. O sexto evento será invocado quando os arquivos forem soltos na zona de soltar. Este manipulador passará os arquivos descartados na zona de depósito para que possam ser processados ​​em outro lugar no aplicativo.

A própria zona de lançamento será um <div>elemento com ref. Usaremos isso refpara anexar ouvintes de eventos à zona de recebimento quando o componente for montado e para removê-los quando for desmontado. Para tornar esta dropzone mais utilizável, iremos configurá-la de forma que renderize as crianças passadas pelos adereços.

Isso significa que poderemos usar esta zona de lançamento como um wrapper para outro conteúdo, sem remover o próprio conteúdo.

import React from 'react'

// Define interface for component props/api:
export interface DropZoneProps {
  onDragStateChange?: (isDragActive: boolean) => void
  onDrag?: () => void
  onDragIn?: () => void
  onDragOut?: () => void
  onDrop?: () => void
  onFilesDrop?: (files: File[]) => void
}

export const DropZone = React.memo(
  (props: React.PropsWithChildren<DropZoneProps>) => {
    const {
      onDragStateChange,
      onFilesDrop,
      onDrag,
      onDragIn,
      onDragOut,
      onDrop,
    } = props

    // Create state to keep track when dropzone is active/non-active:
    const [isDragActive, setIsDragActive] = React.useState(false)
    // Prepare ref for dropzone element:
    const dropZoneRef = React.useRef<null | HTMLDivElement>(null)

    // Render <div> with ref and children:
    return <div ref={dropZoneRef}>{props.children}</div>
  }
)

DropZone.displayName = 'DropZone'

Evento DragEnter

O primeiro evento com o qual trataremos é o dragenterevento. Este evento será acionado quando um arquivo entrar na zona de soltar, alguém pega um arquivo e o coloca sobre a zona de soltar. Usaremos este evento para fazer duas coisas. Primeiro, invocaremos qualquer método opcional transmitido por onDragIn()meio de props.

Em segundo lugar, verificaremos se alguém está realmente arrastando um arquivo para a zona de soltar. Nesse caso, definiremos o estado ativo da zona de soltar como true. Também evitaremos qualquer evento padrão e propagação. É tudo o que precisamos para este evento.

// Create handler for dragenter event:
const handleDragIn = React.useCallback(
  (event) => {
    // Prevent default events:
    event.preventDefault()
    event.stopPropagation()
    // Invoke any optional method passed as "onDragIn()":
    onDragIn?.()

    // Check if there are files dragging over the dropzone:
    if (event.dataTransfer.items && event.dataTransfer.items.length > 0) {
      // If so, set active state to "true":
      setIsDragActive(true)
    }
  },
  [onDragIn]
)

Evento DragLeave

Gerenciar o dragleaveevento também será muito fácil. Este evento será disparado quando algum arquivo sair da zona de lançamento, quando não estiver mais pairando sobre ele. Para lidar com esse evento, precisamos fazer algumas coisas. Primeiro, iremos evitar novamente quaisquer eventos padrão e propagação.

A segunda coisa a fazer é invocar qualquer método opcional passado onDragOut()por adereços. Depois disso, também precisaremos definir o estado ativo para false.

// Create handler for dragleave event:
const handleDragOut = React.useCallback(
  (event) => {
    // Prevent default events:
    event.preventDefault()
    event.stopPropagation()
    // Invoke any optional method passed as "onDragOut()":
    onDragOut?.()

    // Set active state to "false":
    setIsDragActive(false)
  },
  [onDragOut]
)

Arrastar evento

O manipulador de dragovereventos nos ajudará a garantir que o estado ativo da zona de soltar seja truequando algo estiver sendo arrastado sobre ele. No entanto, não definiremos simplesmente o estado ativo como true. Em vez disso, primeiro verificaremos se o valor do estado atual é falsee só então o alteraremos para true.

Isso nos ajudará a evitar algumas mudanças de estado desnecessárias. Também usaremos esse evento para invocar qualquer método transmitido onDrag()pelos adereços.

// Create handler for dragover event:
const handleDrag = React.useCallback(
  (event) => {
    // Prevent default events:
    event.preventDefault()
    event.stopPropagation()
    // Invoke any optional method passed as "onDrag()":
    onDrag?.()

    // Set active state to "true" if it is not active:
    if (!isDragActive) {
      setIsDragActive(true)
    }
  },
  [isDragActive, onDrag]
)

Largar o evento

dropevento é o evento mais importante que precisamos cuidar. Seu manipulador também será o mais longo. Este manipulador fará algumas coisas. Primeiro, ele impedirá qualquer comportamento padrão e interromperá a propagação. Em seguida, ele definirá o estado ativo da zona de soltar como false.

Isso faz sentido porque quando algo é solto na área, o evento de arrastar termina. Dropzone deve registrar isso. Quando o evento drop é disparado, também podemos invocar qualquer método opcional passado por onDrop()meio de props. A parte mais importante são os arquivos descartados.

Antes de cuidarmos deles, primeiro verificaremos se há algum arquivo. Podemos fazer isso verificando o event.dataTransfer.filesobjeto e suas lengthpropriedades. Se houver alguns arquivos, invocaremos qualquer método transmitido onFilesDrop()pelos adereços.

Isso nos permitirá processar esses arquivos como quisermos fora da zona de armazenamento. Quando despachamos esses arquivos, podemos limpar os dataTransferdados para preparar a zona de lançamento para outro uso. Há uma coisa importante sobre os arquivos. Obteremos esses arquivos na forma de FileListnão um array.

Podemos facilmente converter isso FileListem um array usando o loop for . Este loop irá percorrer os arquivos no dataTransferobjeto e colocar cada um em um array vazio. Podemos então passar esse array como um argumento para qualquer método onFilesDrop()para obter os arquivos onde eles são necessários.

// Create handler for drop event:
const handleDrop = React.useCallback(
  (event) => {
    event.preventDefault()
    event.stopPropagation()
    // Prevent default events:

    // Set active state to false:
    setIsDragActive(false)
    // Invoke any optional method passed as "onDrop()":
    onDrop?.()

    // If there are any files dropped:
    if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
      // Convert these files to an array:
      const filesToUpload = []

      for (let i = 0; i < event.dataTransfer.files.length; i++) {
        filesToUpload.push(event.dataTransfer.files.item(i))
      }

      // Invoke any optional method passed as "onFilesDrop()", passing array of files as an argument:
      onFilesDrop?.(filesToUpload)

      // Clear transfer data to prepare dropzone for another use:
      event.dataTransfer.clearData()
    }
  },
  [onDrop, onFilesDrop]
)

Efeitos

Os manipuladores estão prontos e prontos. Antes de prosseguirmos, precisamos configurar dois useEffectganchos. Um gancho será para observar o estado ativo. Quando esse estado muda, queremos invocar qualquer método transmitido por onDragStateChange()meio de props, passando o valor do estado atual como um argumento.

O segundo efeito anexará todos os manipuladores que acabamos de criar ao <div>elemento dropzone quando ele for montado. Depois disso, a zona de lançamento estará pronta para uso. Também usaremos esse efeito para remover todos os ouvintes de eventos quando a zona de soltar for desmontada. Faremos isso por meio do método de limpeza.

// Obser active state and emit changes:
React.useEffect(() => {
  onDragStateChange?.(isDragActive)
}, [isDragActive])

// Attach listeners to dropzone on mount:
React.useEffect(() => {
  const tempZoneRef = dropZoneRef?.current
  if (tempZoneRef) {
    tempZoneRef.addEventListener('dragenter', handleDragIn)
    tempZoneRef.addEventListener('dragleave', handleDragOut)
    tempZoneRef.addEventListener('dragover', handleDrag)
    tempZoneRef.addEventListener('drop', handleDrop)
  }

  // Remove listeners from dropzone on unmount:
  return () => {
    tempZoneRef?.removeEventListener('dragenter', handleDragIn)
    tempZoneRef?.removeEventListener('dragleave', handleDragOut)
    tempZoneRef?.removeEventListener('dragover', handleDrag)
    tempZoneRef?.removeEventListener('drop', handleDrop)
  }
}, [])

Juntar as peças

Essas são todas as partes de que precisamos para o componente File dropzone. Quando colocarmos todas essas partes juntas, poderemos usar esse componente em qualquer lugar no aplicativo React.

import React from 'react'

// Define interface for component props/api:
export interface DropZoneProps {
  onDragStateChange?: (isDragActive: boolean) => void
  onDrag?: () => void
  onDragIn?: () => void
  onDragOut?: () => void
  onDrop?: () => void
  onFilesDrop?: (files: File[]) => void
}

export const DropZone = React.memo(
  (props: React.PropsWithChildren<DropZoneProps>) => {
    const {
      onDragStateChange,
      onFilesDrop,
      onDrag,
      onDragIn,
      onDragOut,
      onDrop,
    } = props

    // Create state to keep track when dropzone is active/non-active:
    const [isDragActive, setIsDragActive] = React.useState(false)
    // Prepare ref for dropzone element:
    const dropZoneRef = React.useRef<null | HTMLDivElement>(null)

    // Create helper method to map file list to array of files:
    const mapFileListToArray = (files: FileList) => {
      const array = []

      for (let i = 0; i < files.length; i++) {
        array.push(files.item(i))
      }

      return array
    }

    // Create handler for dragenter event:
    const handleDragIn = React.useCallback(
      (event) => {
        event.preventDefault()
        event.stopPropagation()
        onDragIn?.()

        if (event.dataTransfer.items && event.dataTransfer.items.length > 0) {
          setIsDragActive(true)
        }
      },
      [onDragIn]
    )

    // Create handler for dragleave event:
    const handleDragOut = React.useCallback(
      (event) => {
        event.preventDefault()
        event.stopPropagation()
        onDragOut?.()

        setIsDragActive(false)
      },
      [onDragOut]
    )

    // Create handler for dragover event:
    const handleDrag = React.useCallback(
      (event) => {
        event.preventDefault()
        event.stopPropagation()

        onDrag?.()
        if (!isDragActive) {
          setIsDragActive(true)
        }
      },
      [isDragActive, onDrag]
    )

    // Create handler for drop event:
    const handleDrop = React.useCallback(
      (event) => {
        event.preventDefault()
        event.stopPropagation()

        setIsDragActive(false)
        onDrop?.()

        if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
          const files = mapFileListToArray(event.dataTransfer.files)

          onFilesDrop?.(files)
          event.dataTransfer.clearData()
        }
      },
      [onDrop, onFilesDrop]
    )

    // Obser active state and emit changes:
    React.useEffect(() => {
      onDragStateChange?.(isDragActive)
    }, [isDragActive])

    // Attach listeners to dropzone on mount:
    React.useEffect(() => {
      const tempZoneRef = dropZoneRef?.current
      if (tempZoneRef) {
        tempZoneRef.addEventListener('dragenter', handleDragIn)
        tempZoneRef.addEventListener('dragleave', handleDragOut)
        tempZoneRef.addEventListener('dragover', handleDrag)
        tempZoneRef.addEventListener('drop', handleDrop)
      }

      // Remove listeners from dropzone on unmount:
      return () => {
        tempZoneRef?.removeEventListener('dragenter', handleDragIn)
        tempZoneRef?.removeEventListener('dragleave', handleDragOut)
        tempZoneRef?.removeEventListener('dragover', handleDrag)
        tempZoneRef?.removeEventListener('drop', handleDrop)
      }
    }, [])

    // Render <div> with ref and children:
    return <div ref={dropZoneRef}>{props.children}</div>
  }
)

DropZone.displayName = 'DropZone'

Adicionando componente de lista de arquivos simples

Um bom complemento para a zona de soltar pode ser a lista de arquivos mostrando todos os arquivos soltos na zona de soltar. Isso pode tornar a IU mais amigável, pois os usuários agora saberão quais arquivos foram registrados pelo aplicativo. Esta lista não precisa ser complicada. Ele pode mostrar apenas o nome do arquivo e seu tamanho.

Este componente da lista de arquivos será simples. Ele aceitará uma variedade de arquivos props. Em seguida, ele mapeará essa matriz e gerará <li>com nome e tamanho de arquivo para cada arquivo. Todos os itens da lista serão agrupados com o <ul>elemento.

import React from 'react'

export interface FileListProps {
  files: File[]
}

export const FileList = React.memo(
  (props: React.PropsWithChildren<FileListProps>) => (
    <ul>
      {props.files.map((file: File) => (
        <li key={`${file.name}_${file.lastModified}`}>
          <span>{file.name}</span>{' '}
          <span>({Math.round(file.size / 1000)}kb)</span>
        </li>
      ))}
    </ul>
  )
)

FileList.displayName = 'FileList'

Criar o componente App e fazê-lo funcionar

A zona de lançamento de arquivos e a lista de arquivos estão prontas. Isso significa que agora podemos ir para o App.tsxe substituir o conteúdo padrão. Dentro do Appcomponente, precisaremos criar dois estados. Um será para manter o controle do estado ativo da zona de soltar. Usaremos isso para destacar a zona de soltar quando o arrasto estiver acontecendo.

O segundo estado será para todos os arquivos colocados na zona de depósito. Também precisaremos de dois manipuladores. Um será para o onDragStateChange()método do dropzone . Usaremos esse manipulador para atualizar o estado ativo local. O segundo manipulador será para dropzones onFilesDrop().

Usaremos esse manipulador para obter todos os arquivos colocados na zona de depósito fora dela, para o filesestado local . Iremos anexar esses dois manipuladores ao Dropzonecomponente. Para a zona de depósito e lista de arquivos, iremos colocá-los na seção de renderização do Appcomponente.

import React from 'react'
import classNames from 'classnames'

// Import dropzone and file list components:
import { DropZone } from './Dropzone'
import { FileList } from './Filelist'

export const App = React.memo(() => {
  // Create "active" state for dropzone:
  const [isDropActive, setIsDropActive] = React.useState(false)
  // Create state for dropped files:
  const [files, setFiles] = React.useState<File[]>([])

  // Create handler for dropzone's onDragStateChange:
  const onDragStateChange = React.useCallback((dragActive: boolean) => {
    setIsDropActive(dragActive)
  }, [])

  // Create handler for dropzone's onFilesDrop:
  const onFilesDrop = React.useCallback((files: File[]) => {
    setFiles(files)
  }, [])

  return (
    <div
      className={classNames('dropZoneWrapper', {
        'dropZoneActive': isDropActive,
      })}
    >
      {/* Render the dropzone */}
      <DropZone onDragStateChange={onDragStateChange} onFilesDrop={onFilesDrop}>
        <h2>Drop your files here</h2>

        {files.length === 0 ? (
          <h3>No files to upload</h3>
        ) : (
          <h3>Files to upload: {files.length}</h3>
        )}

        {/* Render the file list */}
        <FileList files={files} />
      </DropZone>
    </div>
  )
})

App.displayName = 'App'

Conclusão: Como criar uma zona de depósito de arquivos no React e TypeScript

Aí está! Você acabou de criar um componente de dropzone de arquivo personalizado. Uma vez que é um componente autônomo, você pode levá-lo e usá-lo em qualquer lugar que desejar e precisar. Espero que tenha gostado deste tutorial. Eu também espero que este tutorial tenha ajudado você a aprender algo novo e útil.

Postado em Blog
Escreva um comentário