Search by

    Terakhir diperbaharui: Dec 28, 2020

    Sorting

    Penambahan fitur sorting(penyortiran) bertujuan agar user dapat mengatur bagaimana note ditampilkan berdasarkan waktu terakhir note diupdate.

    Persiapan

    Clone repository aplikasi dinotes-api dan dinotes-client disini.

    Step by step

    API

    Saat ini note disimpan di dalam database dalam bentuk dokumen BSON yang terdiri dari field title dan field note, tidak ada field yang berfungsi untuk menyimpan data waktu seperti tanggal dan jam.

    Dengan bentuk dokumen yang sekarang sorting tidak mungkin bisa dilakukan.

    Oleh karena itu kita perlu tambahkan field untuk menyimpan value berupa waktu yang menunjukan kapan update dilakukan.

    Pada bagian API, update handler addNotes dengan menambahkan object properties dengan nama updatedAt.

    handler.js

    1...
    2exports.addNote = async (req, res, next) => {
    3 const { notesCollection } = req.app.locals;
    4 const { title } = req.body;
    5
    6 try {
    7 if (!title) {
    8 logger.error(`${req.originalUrl} - ${req.ip} - title is missing `);
    9 throw new Error('title is missing');
    10 }
    11
    12 const data = {
    13 ...req.body,
    14 createdAt: new Date(Date.now()).toISOString(),
    15 updatedAt: new Date(Date.now()).toISOString(),
    16 }
    17
    18 // Insert data to collection
    19 const result = await notesCollection.insertOne(data);
    20
    21 const objResult = JSON.parse(result);
    22
    23 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully saved`);
    24
    25 res.status(200).json({ message: 'Data successfully saved', _id: objResult.insertedId });
    26 } catch (error) {
    27 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);
    28 next(error);
    29 }
    30};
    31...

    Karena MongoDB menyimpan data waktu atau tanggal dalam format ISO, data tersebut harus dikonversi ke object Date dengan method toISOString().

    Kita lakukan hal yang sama pada handler updateNotes.

    1...
    2exports.updateNote = async (req, res, next) => {
    3 const { notesCollection } = req.app.locals;
    4 const { title, note } = req.body;
    5
    6 try {
    7 if (!title) {
    8 logger.error(`${req.originalUrl} - ${req.ip} - title is missing `);
    9 throw new Error('title is missing');
    10 }
    11 // update data collection
    12 await notesCollection.updateOne(
    13 { _id: ObjectId(req.params.id) },
    14 { $set: { title, note, updatedAt: new Date(Date.now()).toISOString() } }
    15 );
    16
    17 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully updated`);
    18
    19 res.status(200).json('Data successfully updated');
    20 } catch (error) {
    21 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);
    22 next(error);
    23 }
    24};
    25
    26...

    Selanjutnya kita test menggunakan PostMan.

    Menambah data.

    testing postman post

    Cek data yang berhasil disimpan / ditambahkan.

    testing postman get

    Sekarang selain field _id, title dan note, ada field baru bernama createdAt untuk menyimpan informasi kapan note dibuat dan updatedAt untuk menyimpan informasi kapan note terakhir diupdate.

    Client

    Di sisi client yang akan kita lakukan:

    • Membuat menu Dropdown untuk kebutuhan sorting
    • Membuat logika untuk sort data dengan menambah reducer di dalam slice

    Membuat Menu Dropdown

    Buat component baru bernama DropdownMenu di dalam component NotesList:

    src/components/NotesList.js

    1import React, { useEffect, useState, useRef } from 'react';
    2import tw from 'twin.macro';
    3import { Link } from 'react-router-dom';
    4import { useSelector, useDispatch } from 'react-redux';
    5import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
    6import { faSortAmountUp } from '@fortawesome/free-solid-svg-icons';
    7import { fetchNotes, getFilteredNotes } from '../features/notes/notesSlice';
    8import Container from './ui/Container';
    9
    10const NotesListContainer = tw.div`grid grid-cols-1 md:grid-cols-3 gap-4 my-8`;
    11const Card = tw.div`text-left p-4 border rounded-md`;
    12const Title = tw.h4`text-lg font-semibold text-purple-900`;
    13const SearchBar = tw.input`m-4 p-2 text-left border rounded focus:outline-none focus:ring focus:border-blue-300`;
    14const Toolbar = tw.div`flex flex-row w-full justify-end`;
    15const DropdownInnerWrapper = tw.div`relative inline-block text-left`;
    16const SortIcon = tw.button`text-base text-right my-4 p-2 border rounded-md`;
    17const DropdownPanel = tw.div`origin-top-right absolute right-0 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5`;
    18const Menu = tw.ul`py-1`;
    19const Item = tw.li`block px-4 py-2 w-full text-sm text-left text-gray-700 hover:bg-gray-100 hover:text-gray-900`;
    20
    21const DropdownMenu = () => {
    22 const [visible, setVisible] = useState(false);
    23
    24 const handleChange = (e) => {
    25 setVisible(!visible);
    26 };
    27
    28 return (
    29 <DropdownInnerWrapper ref={node}>
    30 <SortIcon onClick={handleChange}>
    31 <FontAwesomeIcon icon={faSortAmountUp} size="lg" />
    32 </SortIcon>
    33 {visible && (
    34 <DropdownPanel>
    35 <Menu role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
    36 <Item role="menuitem">
    37 Newest Modified Date
    38 </Item>
    39 <Item role="menuitem">
    40 Oldest Modified Date
    41 </Item>
    42 </Menu>
    43 </DropdownPanel>
    44 )}
    45 </DropdownInnerWrapper>
    46 );
    47};
    48
    49const NotesList = () => {
    50
    51 ...
    52 return (
    53 <Container>
    54 <Toolbar>
    55 <SearchBar placeholder="Search Notes..." onChange={handleChange} />
    56 <DropdownMenu />
    57 </Toolbar>
    58 <NotesListContainer>{content}</NotesListContainer>
    59 </Container>
    60 );
    61};
    62
    63...

    Dengan code di atas item Dropdown akan muncul ketika menu di klik dan akan hilang saat menu kembali di klik, sedangkan pada umumnya item Dropdown akan hilang ketika kita klik di sembarang tempat.

    Untuk itu kita perlu sedikit modifikasi code component DropdownMenu dengan menambahkan event listener 'mousedown' yang ditaruh di dalam hook useEffect.

    1...
    2const DropdownMenu = () => {
    3 const [visible, setVisible] = useState(false);
    4 const node = useRef();
    5
    6 useEffect(() => {
    7 document.addEventListener("mousedown", handleClick);
    8
    9 return () => {
    10 document.removeEventListener("mousedown", handleClick);
    11 };
    12 }, []);
    13
    14 const handleChange = (e) => {
    15 setVisible(!visible);
    16 };
    17
    18 const handleClick = (e) => {
    19 if (node.current.contains(e.target)) {
    20 return;
    21 }
    22 setVisible(false);
    23 };
    24
    25 return (
    26 <DropdownInnerWrapper ref={node}>
    27 <SortIcon onClick={handleChange}>
    28 <FontAwesomeIcon icon={faSortAmountUp} size="lg" />
    29 </SortIcon>
    30 {visible && (
    31 <DropdownPanel>
    32 <Menu role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
    33 <Item role="menuitem" onClick={(e) => handleClick(e)}>
    34 Newest Modified Date
    35 </Item>
    36 <Item role="menuitem" onClick={(e) => handleClick(e)}>
    37 Oldest Modified Date
    38 </Item>
    39 </Menu>
    40 </DropdownPanel>
    41 )}
    42 </DropdownInnerWrapper>
    43 );
    44};
    45...

    Dengan modifikasi di atas, setiap kali user klik sembarang tempat maka item dari menu dropdown akan hilang/disembunyikan.

    Hasilnya:

    Membuat logika sorting

    Langkah terakhir adalah membuat logika sorting untuk menu dropdown.

    Update handler pada bagian item menu dropdown.

    1...
    2 return (
    3 <DropDownInnerWrapper ref={node}>
    4 <SortIcon onClick={handleChange}>
    5 <FontAwesomeIcon icon={faSortAmountUp} size="lg" />
    6 </SortIcon>
    7 {visible && (
    8 <DropdownPanel>
    9 <Menu role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
    10 <Item role="menuitem" onClick={(e) => handleClick(e, 'newest')}>
    11 Newest Modified Date
    12 </Item>
    13 <Item role="menuitem" onClick={(e) => handleClick(e, 'oldest')}>
    14 Oldest Modified Date
    15 </Item>
    16 </Menu>
    17 </DropdownPanel>
    18 )}
    19 </DropDownInnerWrapper>
    20 );
    21...

    Jika item 'Newest Modified Date' di klik maka string 'newest' akan dikirimkan handler ke reducer via dispatch sebagai penentu untuk mengubah urutan dari note berdasarkan waktu update paling baru.

    Sedangkan untuk item 'Oldest Modified Date' maka string 'oldest' yang dikirimkan ke reducer sebagai penentu untuk mengubah urutan dari note berdasarkan waktu update paling lama.

    Perlu diingat dengan menggunakan redux maka semua modifikasi state terjadi di store dan yang bisa mengubah state adalah reducer.

    Tambahkan function sorting di dalam reducer.

    src/features/notes/notesSlice.js

    1...
    2const notesSlice = createSlice({
    3 name: 'notes',
    4 initialState,
    5 reducers: {
    6 statusReset(state, action) {
    7 state.status = 'idle';
    8 },
    9 updateSort(state, action) {
    10 if (action.payload === 'oldest') {
    11 state.data = state.data.sort(
    12 (a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
    13 );
    14 } else if(action.payload === 'newest'){
    15 state.data = state.data.sort(
    16 (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
    17 );
    18 }
    19 }
    20 },
    21...
    22export const { statusReset, updateSort } = notesSlice.actions;

    Pada code di atas, jika payload dari action adalah 'oldest' maka reducer updateSort akan mengeksekusi function yang akan mengurutkan note dari yang paling lama ke yang paling baru diupdate dan begitu juga sebaliknya.

    Selanjutnya import reducer dan tambahkan function dispatch pada front end.

    src/components/NotesList.js

    1...
    2import { useSelector, useDispatch } from 'react-redux';
    3...
    4const DropDownMenu = () => {
    5 const [visible, setVisible] = useState(false);
    6 const node = useRef();
    7 const dispatch = useDispatch();
    8
    9 useEffect(() => {
    10 document.addEventListener("mousedown", handleClick);
    11
    12 return () => {
    13 document.removeEventListener("mousedown", handleClick);
    14 };
    15 }, []);
    16
    17 const handleChange = (e) => {
    18 setVisible(!visible);
    19 };
    20
    21 const handleClick = (e, option) => {
    22 if (node.current.contains(e.target)) {
    23 dispatch(updateSort(option));
    24 return;
    25 }
    26 setVisible(false);
    27 };
    28...

    Hasil Akhir:

    Final code bisa dilihat di repository DinoTes pada branch search-sorting.