Utilizando o ModalRoute e fazendo upload de imagens

ReactJS 23 de Ago de 2018

Esse post é a decima parte da série de posts “Clone AirBnB com AdonisJS, React Native e ReactJS” onde iremos construir do zero uma aplicação web com ReactJS e também uma aplicação mobile com React Native com dados servidos através de uma API REST feita com NodeJS utilizando o framework AdonisJS.

Lá vamos nós novamente, hoje vamos aprender a adicionar novos imóveis no mapa. Lembrando que até aqui já temos a área de SignIn e SignOut, bem como o mapa com a lista de imóveis. Agora sem mais lenga-lenga, boooora para o código.

Começando

Para adicionar um novo imóvel, será preciso adicionar um botão junto ao do logout e quando ele for clicado exibir um Map Marker e as opções de Adicionar e Cancelar. Neste caso, o Map Marker ficará fixo, e o usuário movimentará o mapa até achar a posição (latitude e longitude) para adicionar seu imóvel.

A principio será criado um PointReference para que o Map Marker seja fixado, então no arquivo, App/styles.js:

// Importe o polished
import { darken } from "polished";

// ...
export const PointReference = styled.div`
  position: absolute;
  top: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  i {
    color: #fc6963;
    pointer-events: all;
    font-size: 50px;
    margin-top: 112px;
    margin-left: 12px;
    -webkit-text-fill-color: #fc6963;
    -webkit-text-stroke-width: 2px;
    -webkit-text-stroke-color: ${() => darken(0.05, "#fc6963")};
  }
  div {
    margin-top: 100px;
    button {
      border: none;
      font-size: 15px;
      height: 46px;
      margin: 0 10px;
      background-color: #fc6963;
      color: #ffffff;
      padding: 0 20px;
      border-radius: 3px;
      pointer-events: all;
      text-align: center;
      &.cancel {
        background: #ffffff;
        color: #333333;
      }
    }
  }
`;

Esse PointReference cria uma layer que ficará sobre o Mapbox, aqui foi utilizado o FontAwesome para ser o Map Marker.

Agora é preciso importar o PointReference e fazer algumas alterações, para exibir ele somente quando o Button para adicionar imóvel for clicado, e o interessante é que nesse momento haja a listagem de imóveis, deixando assim o mapa mais limpo. No App/index.js:

// ...
// Adicionar o PointReference
import { Container, ButtonContainer, PointReference } from "./styles";

class Map extends Component {
  state = {
    viewport: // ...
    properties: [],
    // Novo state
    addActivate: false
  };

  renderActions() {
    return (
      <ButtonContainer>
        <Button
          color="#fc6963"
          onClick={() => this.setState({ addActivate: true })}
        >
          <i className="fa fa-plus" />
        </Button>
        <Button color="#222" onClick={this.handleLogout}>
          <i className="fa fa-times" />
        </Button>
      </ButtonContainer>
    );
  }
  // ...
}

Ainda falta exibir o render do PointReference e também tirar a lista de imóveis do mapa.

class Map extends Component {
  // ...
  // Novo render
  renderButtonAdd() {
    return (
      this.state.addActivate && (
        <PointReference>
          <i className="fa fa-map-marker" />
          <div>
            <button onClick={this.handleAddProperty} type="button">
              Adicionar
            </button>
            <button
              onClick={() => this.setState({ addActivate: false })}
              className="cancel"
            >
              Cancelar
            </button>
          </div>
        </PointReference>
      )
    );
  }
  
  render() {
    const { containerWidth: width, containerHeight: height, match } = this.props;
    const { properties, addActivate } = this.state;
    return (
      <Fragment>
        <MapGL
          //...
        >
          {!addActivate && <Properties match={match} properties={properties} />}
        </MapGL>
        {this.renderActions()}
        {this.renderButtonAdd() /* Esse aqui será adicionado */}
        // ...
      </Fragment>
    );
  }
}

Repare que só aparecerá as Properties quando o addActivate for false, e para não poluir o render foi criada um novo método que adiciona o PointReference caso o addActivate seja true.

E para finalizar essa parte, quando o usuário clicar em adicionar deve ser redirecionado para uma nova página com a latitude e longitudepresente para adicionar as demais informações sobre o imóvel.

class Map extends Component {
  // ...
  handleAddProperty = () => {
    const { match, history } = this.props;
    const { latitude, longitude } = this.state.viewport;
    history.push(
      `${match.url}/properties/add?latitude=${latitude}&longitude=${longitude}`
    );
    
    this.setState({ addActivate: false });
  };
  // ...
}

A página que o usuário será redirecionado ainda não foi criada (já já será), ela será uma página Modal igual existe em aplicativos como Instagram e Facebook, onde você não precisa carregar uma nova página quando clica no link, mas com a url você consegue chegar na mesma.

Adicionar imóvel

Agora o bicho vai PEGAR!!

Agora vamos começar a trabalhar em cima de uma nova página para adicionar um imóvel, essa página será uma página Modal e ela ficará dentro da página App. A ideia de abrir uma página modal é ter uma url exclusiva para ela mas que é “fácil” voltar para a página principal.

Dependências

Para trabalhar com o Modal utilize o react-router-modal, e além de instalar ele adicione também o seguinte:

npm install react-router-modal react-dropzone stringquery classnames

  • react-router-modal: Trabalhar com o modal junto com  o react-router-dom.
  • react-dropzone: Permite jogar arquivos selecionados numa determinada área do browser
  • stringquery: Ajudará a trabalhar com a query de uma url.
  • classnames: Adiciona classes CSS através de objetos.

Agora que você instalou as dependências, é hora de usa-las.

No routes.js, você deve configurar o seguinte:

import React, { Fragment } from "react";
// ...
import { ModalContainer } from "react-router-modal";
import "react-router-modal/css/react-router-modal.css";

// ...

const Routes = () => (
  <BrowserRouter>
    <Fragment>
      <Switch>
        // ...
      </Switch>
      <ModalContainer />
    </Fragment>
  </BrowserRouter>
);

// ...

Isso serviu para por o ModalContainer nas rotas, ele será importante para utilização do Modal.

Agora é preciso criar uma nova página que adicionará o imóvel, ela conterá apenas um formulário e através da url receberá a latitude e a longitude. Então monte aquela estrutura marota de páginas:

src/pages/
      |--- AddProperty/
              |--- index.js
              |--- styles.js

Depois da estrutura montada, já comece estilizando com o seguinte código no styles.js:

import styled from "styled-components";
import Dropzone from "react-dropzone";

export const File = styled(Dropzone)`
  border: 2px dashed #ff3333;
  width: 100%;
  max-width: 660px;
  font-size: 16px;
  color: #777777;
  text-align: center;
  display: grid;
  grid-template-columns: 100px 100px 100px;
  grid-gap: 5px;
  background-color: #fff;
  color: #444;
  &.without-files {
    display: flex;
  }
  img {
    width: 100px;
  }
  p {
    margin-top: 15px;
    border: none !important;
  }
`;

Aqui você percebe o StyledComponents (s2) trabalhando de forma mágica, mais uma vez. Dessa vez não foi utilizado um elemento do styled e sim um outro componente (Dropzone), isso foi possível porque o Dropzone é um elemento React, mas foi só chama-lo como parâmetro do styled que foi sucesso!!!

Ainda no styles.js será necessário estilizar o form, mas devido ao código está muito extenso copie desse link aqui. Só não vai me esquecer de copiar rsrs.

Agora é hora da página, então no index.js comece importando alguns componentes:

import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import querySearch from "stringquery";
import classNames from "classnames";
import PropTypes from "prop-types";

import { Form, File } from "./styles";

import api from "../../services/api";

Na class além dos states será validado no componenteDidMount se na query da url os parâmetros latitude e longitude estão presentes, caso não esteja, ele enviar o usuário para a página do App. Então ainda no index.js:

class AddProperty extends Component {
  static propTypes = {
    location: PropTypes.shape({
      search: PropTypes.string
    }).isRequired,
    history: PropTypes.shape({
      push: PropTypes.func
    }).isRequired
  };

  state = {
    title: "",
    address: "",
    price: "",
    error: "",
    files: []
  };

  componentDidMount() {
    const params = querySearch(this.props.location.search);
    if (
      !params.hasOwnProperty("latitude") ||
      !params.hasOwnProperty("longitude")
    ) {
      alert("É necessário definir a latitude e longitude para um imóvel.");
      this.props.history.push("/app");
    }

    this.setState({ ...params });
  }
  // ainda tem mais

Lá no styles o componente Dropzone foi importado, lembra? Ele é o componente em que separa uma área na página e dentro dela o usuário pode adicionar arquivos arrastando de pastas externas ao Browser e jogando dentro dela ou mesmo clicar para abrir o gerenciador de arquivos.

Os próximos métodos trabalham com o Dropzone, um para colocar os arquivos no state e o outro para renderizar esses arquivos.

handleDrop = files => this.setState({ files });

renderFiles() {
  const { files } = this.state;
  return !files.length ? (
    <p>Jogue as imagens ou clique aqui para adiciona-las</p>
  ) : (
    files.map(file => <img key={file.name} src={file.preview} />)
  );
}

Agora será trabalhado com o evento do handlerSubmit que é disparado no onSubmit, para ficar mais claro foi dividido em duas partes:

handleSubmit = async e => {
    e.preventDefault();

    try {
      const { title, address, price, latitude, longitude, files } = this.state;

      if (!title || !address || !price || !latitude || !longitude) {
        this.setState({ error: "Preencha todos os campos" });
        return;
      }

      const { data: { id } } = await api.post("/properties", {
        title,
        address,
        price,
        latitude,
        longitude
      });

      // tem mais

As considerações sobre a primeira parte são as seguintes, será utilizado métodos assíncronos e por isso o uso do async/await e já foi removido o envio padrão do formulário para uma action com o e.preventDefault().
Dentro try/catch é trabalhado com as requisições e para elas foram extraídas do state as variáveis importantes e tão logo verifica se estão presentes para dar continuidade a função.

É importante notar que o já foi extraído o id, através da desestruturação, da resposta da requisição da API para criar o imóvel.

A segunda parte desse método, não tem segredo, bora disseca-la:

// Continuação do handleSubmit
  if (!files.length) this.props.history.push("/app");
  
      const data = new FormData();
      files.map((file, index) =>
        data.append(`image[${index}]`, file, file.name)
      );

      const config = {
        headers: {
          "content-type": "multipart/form-data"
        }
      };

      await api.post(
        `/properties/${id}/images`,
        data,
        config
      );

      this.props.history.push("/app");
    } catch (err) {
      this.setState({ error: "Ocorreu algum erro ao adicionar o imóvel" });
    }
  };

Ainda dentro do try/catch nessa segunda parte temos o seguinte, uma verificação se existe algum arquivo (files) adicionado ao state, não existindo o usuário será enviado para a página App.
Todavia, existindo arquivos, foi utilizado o FormData para estrutura-los de maneira que um formulário deve está. Para essa requisição, por estar trabalhando com um formulário de arquivos, o cabeçalho deve conter o content-type como multipart/form-data. Feita a requisição à API, para adicionar os arquivos ao id definido anteriormente o usuário é enviado para a página de App.

Por fim, caso exista algum problema nas requisições um erro no formulário é apresentado ao usuário.

Ainda dentro do index.js, o método handleCancel deve ser adicionado:

// ...

handleCancel = e => {
  e.preventDefault();

  this.props.history.push("/app");
};

Esse método apenas mandará o usuário para a pagina App.

E para finalizar a página AddProperty, você deve adicionar o render:

// ...
  render() {
    return (
      <Form onSubmit={this.handleSubmit}>
        <h1>Adicionar imóvel</h1>
        <hr />
        {this.state.error && <p>{this.state.error}</p>}
        <input
          type="text"
          placeholder="Título"
          onChange={e => this.setState({ title: e.target.value })}
        />
        <input
          type="text"
          placeholder="Endereço"
          onChange={e => this.setState({ address: e.target.value })}
        />
        <input
          type="decimal"
          placeholder="Preço"
          onChange={e => this.setState({ price: e.target.value })}
        />
        <File
          multiple
          onDrop={this.handleDrop}
          className={classNames({ "without-files": !this.state.files.length })}
        >
          {this.renderFiles()}
        </File>
        <div className="actions">
          <button type="submit">Adicionar</button>
          <button onClick={this.handleCancel} className="cancel">
            Cancelar
          </button>
        </div>
      </Form>
    );
  }
}

export default withRouter(AddProperty);

Aqui é simplesmente um formulário utilizando os componentes previamente estilizados. Talvez você se pergunte sobre esse trecho de código: classNames({ "without-files": !this.state.files.length }), nele a função classNames serve para trabalhar com classes do CSS a partir de um objeto. O classNames funciona assim, caso o valor da chame seja true ele entende que aquela classe deve ser adicionado ao className.

Chamando a página

Agora você está a um passo de concluir esse Post. Falta ainda chamar essa página que acabamos de criar em algum lugar.
Ela será chamada dentro da página App, já que é um Modal da página é lá que o AddProperty deve ficar. Então no App/index.js comece importando algumas coisas:

// ...
import { ModalRoute } from "react-router-modal";

// ... 

import AddProperty from "../AddProperty";

O ModalRoute fará o papel do Route definindo o path e o component e como o nome sugere será para o Modal.

Agora dentro do render do componente App, adicione:


// ...
{this.renderActions()}
{this.renderButtonAdd()}
<ModalRoute
  path={`${match.url}/properties/add`}
  parentPath={match.url}
  component={AddProperty}
/>
// ...

No ModalRoute o path é a url que executará o component que é a página AddProperty, já o parentPath identifica qual é a página que chamou o Model e quando ele for fechado, por exemplo, retorna para a página do parentPath.

Finished it

É meu caro essa é a pior parte da brincadeira, mas fica tranquilo que ainda tem mais!!!
Hoje você aprendeu a utilizar RouteModal e já consegue adicionar o imóvel pelo APP. Irado, não?

Espero que você tenha gostado desse Post, caso tenha alguma dúvida deixe aí nos comentários!! E pode compartilhar ele como se não tivesse amanhã!!!!

O código está aqui.

Aquele abraço bem apertado e até mais.

Marcadores