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;56 try {7 if (!title) {8 logger.error(`${req.originalUrl} - ${req.ip} - title is missing `);9 throw new Error('title is missing');10 }1112 const data = {13 ...req.body,14 createdAt: new Date(Date.now()).toISOString(),15 updatedAt: new Date(Date.now()).toISOString(),16 }1718 // Insert data to collection19 const result = await notesCollection.insertOne(data);2021 const objResult = JSON.parse(result);2223 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully saved`);2425 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;56 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 collection12 await notesCollection.updateOne(13 { _id: ObjectId(req.params.id) },14 { $set: { title, note, updatedAt: new Date(Date.now()).toISOString() } }15 );1617 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully updated`);1819 res.status(200).json('Data successfully updated');20 } catch (error) {21 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);22 next(error);23 }24};2526...
Selanjutnya kita test menggunakan PostMan.
Menambah data.
Cek data yang berhasil disimpan / ditambahkan.
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';910const 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`;2021const DropdownMenu = () => {22 const [visible, setVisible] = useState(false);2324 const handleChange = (e) => {25 setVisible(!visible);26 };2728 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 Date38 </Item>39 <Item role="menuitem">40 Oldest Modified Date41 </Item>42 </Menu>43 </DropdownPanel>44 )}45 </DropdownInnerWrapper>46 );47};4849const NotesList = () => {5051 ...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};6263...
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();56 useEffect(() => {7 document.addEventListener("mousedown", handleClick);89 return () => {10 document.removeEventListener("mousedown", handleClick);11 };12 }, []);1314 const handleChange = (e) => {15 setVisible(!visible);16 };1718 const handleClick = (e) => {19 if (node.current.contains(e.target)) {20 return;21 }22 setVisible(false);23 };2425 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 Date35 </Item>36 <Item role="menuitem" onClick={(e) => handleClick(e)}>37 Oldest Modified Date38 </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 Date12 </Item>13 <Item role="menuitem" onClick={(e) => handleClick(e, 'oldest')}>14 Oldest Modified Date15 </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();89 useEffect(() => {10 document.addEventListener("mousedown", handleClick);1112 return () => {13 document.removeEventListener("mousedown", handleClick);14 };15 }, []);1617 const handleChange = (e) => {18 setVisible(!visible);19 };2021 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.