Login

Saat ini login masih dihardcode di component Navigation, seharusnya halaman login diakses melalui form login. Sebagai contoh, kita akan membuat form login di halaman home. Selain form login kita juga akan membutuhkan toast notification, dan fetch api.

Toast

buat file src/helper/toast.js

import { writable, derived } from "svelte/store"

const TIMEOUT = 2000

function createNotificationStore (timeout) {
    const _notifications = writable([])

    function send (message, type = "default", timeout) {
        _notifications.update(state => {
            return [...state, { id: id(), type, message, timeout }]
        })
    }

    let timers = []

    const notifications = derived(_notifications, ($_notifications, set) => {
        set($_notifications)
        if ($_notifications.length > 0) {
            const timer = setTimeout(() => {
                _notifications.update(state => {
                    state.shift()
                    return state
                })
            }, $_notifications[0].timeout)
            return () => {
                clearTimeout(timer)
            }
        }
    })
    const { subscribe } = notifications

    return {
        subscribe,
        send,
		    default: (msg, timeout = TIMEOUT) => send(msg, "default", timeout),
        danger: (msg, timeout = TIMEOUT) => send(msg, "danger", timeout),
        warning: (msg, timeout = TIMEOUT) => send(msg, "warning", timeout),
        info: (msg, timeout = TIMEOUT) => send(msg, "info", timeout),
        success: (msg, timeout = TIMEOUT) => send(msg, "success", timeout),
    }
}

function id() {
    return '_' + Math.random().toString(36).substr(2, 9);
};

export const notifications = createNotificationStore()

Buat file src/components/Toast.svelte

<script>
  import { flip } from "svelte/animate";
  import { fly } from "svelte/transition";
  import { notifications } from "../helper/toast.js";

  export let themes = {
      danger: "#E26D69",
      success: "#84C991",
      warning: "#f0ad4e",
      info: "#5bc0de",
      default: "#aaaaaa",
  };
</script>

<div class="notifications">
  {#each $notifications as notification (notification.id)}
      <div
          animate:flip
          class="toast"
          style="background: {themes[notification.type]};"
          transition:fly={{ y: 30 }}
      >
          <div class="content">{notification.message}</div>
          {#if notification.icon}<i class={notification.icon} />{/if}
      </div>
  {/each}
</div>

<style>
  .notifications {
    position: fixed;
    top: 10px;
    left: 0;
    right: 0;
    margin: 0 auto;
    padding: 0;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    pointer-events: none;
  }

  .toast {
    flex: 0 0 auto;
    margin-bottom: 10px;
    border-radius: 4px;
  }

  .content {
    padding: 16px 40px;
    display: block;
    color: white;
    font-weight: 500;
  }
</style>

Pasang component Toast di file src/App.svelte

<script>
	import { Router, Route } from "svelte-routing"
	import About from "./routes/About.svelte"
	import Home from "./routes/Home.svelte"
	import { PATH_URL } from "./helper/path"
	import UserList from "./routes/UserList.svelte"
	import ProtectedRoute from "./ProtectedRoute.svelte"
	import Toast from "./components/Toast.svelte"
</script>

<Router>
	<Route path={PATH_URL.BASE} component={Home}/>
	<Route path={PATH_URL.ABOUT} component={About}/>
	<ProtectedRoute path={PATH_URL.USER_LIST} component={UserList} />
</Router>
<Toast/>

Form Login

Buat komponen Input pada file src/components/Input.svelte

<script>
  export let label
  export let value
</script>

<div>
  <label for={label}>{label}</label>
  <input bind:value id={label} name={label} {...$$restProps} />
</div>

Buat komponen Button pada file src/components/Button.svelte

<script>
  import Loader from './Loader.svelte';
  export let bgColor = "#00FF00AA"
  export let bgHoverColor = "#00FF0088"
  export let size = "medium";
  export let className = "";
  export let isLoading = false;

  const getPaddingSize = (size) => {
    switch (size) {
      case "small":
        return "py-1 px-4"
      default:
        return "py-2 px-8"
    }
  }

  const getTextSize = (size) => {
    switch (size) {
      case "small":
        return "text-sm"
      default:
        return "text-lg"
    }
  }

  const getLoaderSize = (size) => {
    switch (size) {
      case "small":
        return "20"
      default:
        return "40"
    }
  }
  let padding = getPaddingSize(size);
  let text = getTextSize(size);
  let loaderSize = getLoaderSize(size);
  
</script>

<button
  disabled={isLoading}
  on:click
  class={`flex align-center justify-center text-white ${bgColor} border-0 ${padding} focus:outline-none hover:${bgHoverColor} rounded ${text} ${className}`}>
  {#if isLoading}
    <div class="relative">
      <div class="invisible"><slot /></div>
      <div class="loader-container">
        <Loader size={loaderSize}/>
      </div>
    </div>
  {:else}
    <slot />
  {/if}
</button>

<style>
  .loader-container {
    display: flex;
    justify-content: center;
    position: absolute;
    left: 0px;
    right: 0px;
    top: 0px;
    bottom: 0px;
    margin: auto;
    height: fit-content;
  }
</style>

Pada button dibuat suatu loader, ide-nya ketika button diklik, maka akan muncul loader selama sekian detik (sekitar 1,5 detik). Untuk buat file src/components/Loader/svelte

<script>
  export const durationUnitRegex = /[a-zA-Z]/;
  export const range = (size, startAt = 0) =>
  [...Array(size).keys()].map(i => i + startAt);
  export let color = "#ffffff";
  export let unit = "px";
  export let duration = "1.5s";
  export let size = "60";
  let durationUnit = duration.match(durationUnitRegex)[0];
  let durationNum = duration.replace(durationUnitRegex, "");
</script>

<style>
  .wrapper {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    width: var(--size);
    height: calc(var(--size) / 2.5);
    margin-top: calc(var(--size) / 5);
  }
  .circle {
    position: absolute;
    top: 0px;
    width: calc(var(--size) / 5);
    height: calc(var(--size) / 5);
    border-radius: 999px;
    background-color: var(--color);
    animation: motion var(--duration) cubic-bezier(0.895, 0.03, 0.685, 0.22)
      infinite;
  }
  @keyframes motion {
    0% {
      opacity: 1;
    }
    50% {
      opacity: 0;
    }
    100% {
      opacity: 1;
    }
  }
</style>

<div
  class="wrapper"
  style="--size: {size}{unit}; --color: {color}; --duration: {duration}">
  {#each range(3, 0) as version}
    <div
      class="circle"
      style="animation-delay: {version * (+durationNum / 10)}{durationUnit}; left: {version * (+size / 3 + +size / 15) + unit};" />
  {/each}
</div>

Update halaman src/routes/Home.svelte untuk menambahkan form login

<script>
  import { Images } from "../helper/images"
  import Button from "../components/Button.svelte"
  import Input from "../components/Input.svelte"
  import { notifications } from '../helper/toast'
  import { PATH_URL } from "../helper/path"
  import { token } from "../stores/token"
  import { navigate } from "svelte-routing"

  const state = {
    username: "",
    password: ""
  }
  
  let isLoading = false;
  let isToken = false;

  if (localStorage.getItem("token")) {
    isToken = true
  }

  const handleInput = (e) => {
    const { value, name } = e.target
    state[name] = value
  }

  const loginCall = async (username, password) => {
    
    if (username === "rijal.asep.nugroho@gmail.com" && password === "1234") {
      return {
        Token: "sudah-login"
      }
    }

    return {}
  }

  const login = async () => {
    try {
      isLoading = true
      const user = await loginCall(state.username, state.password)
      
      if (!user.Token) {
        throw({message: "please suplay valid username and password"})
      }
      
      localStorage.setItem("token", user.Token)
      token.set(localStorage.getItem('token'));
      isLoading = false;
      isToken = true

      navigate(PATH_URL.USER_LIST, { replace: false })
    } catch(e) {
      isLoading = false;
      notifications.danger(e.message)
    }
  };

  let widthDiv = 50;
  if (isToken) {
    widthDiv = 100;
  }
 
</script>

<div id="container" style="background-image:url({Images.img_erp});">
  <div id="welcome-div" style="flex:{widthDiv}%;">
    <h1>Selamat datang di svelte skeleton</h1>
    <a href="{PATH_URL.ABOUT}">About Us</a>
  </div>
  {#if !isToken}
  <div id="loginDiv" style="flex: {widthDiv}%;">
    <form on:submit|preventDefault={login}>
      <Input label="Username" name="username" on:input={handleInput} bind:value={state.username} placeholder="username" type="text" required/>
      <Input label="Password" name="password" on:input={handleInput} bind:value={state.password} placeholder="password" type="password" required/>
      <Button isLoading={isLoading}>Login</Button>
    </form>
  </div>
  {/if}
 </div>

<style>
  #container {
    width:100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  #welcome-div, #loginDiv {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-flow: wrap column;
  }

  a {
    color: wheat;
  }
</style>

Karena fungsi login sdh diambil alih di halaman Home, maka komponen Navigation kita update untuk menghapus kode tentang login.

<script>
  import { Link, navigate } from "svelte-routing"
  import { token } from "../stores/token"
  import { PATH_URL } from "../helper/path"
  
  let isToken = false

  if (localStorage.getItem("token")) {
    isToken = true
  }


  function logout () {
    localStorage.removeItem("token")
    token.set(token, localStorage.getItem("token"))
    isToken = false
    navigate(PATH_URL.BASE, { replace: true })
  }

</script>

<div>
  {#if isToken}
  <ul>
    <li><Link to={PATH_URL.USER_LIST}>List User</Link></li>
  </ul>
  <button on:click="{logout}">Logout</button>
  {:else}
  <ul>
    <li><Link to={PATH_URL.BASE}>Home</Link></li>
    <li><Link to={PATH_URL.ABOUT}>Tentang Kami</Link></li>
  </ul>
  <Link to="{PATH_URL.BASE}">Login</Link>
  {/if}

</div>

Call API

Saat ini, fungsi loginCall() pada halaman Home masih dihardcode. Pada aplikasi sesungguhnya fungsi ini memanggil api dari backend melalui Ajax.

Ubah file src/routes/Home.svelte

<script>
  import { Images } from "../helper/images"
  import Button from "../components/Button.svelte"
  import Input from "../components/Input.svelte"
  import { notifications } from '../helper/toast'
  import { PATH_URL } from "../helper/path"
  import { token } from "../stores/token"
  import { navigate } from "svelte-routing"

  const state = {
    username: "",
    password: ""
  }

  const user = {token: ""}
  
  let isLoading = false;
  let isToken = false;

  if (localStorage.getItem("token")) {
    isToken = true
  }

  const handleInput = (e) => {
    const { value, name } = e.target
    state[name] = value
  }

  function reqListener() {
    user.token = JSON.parse(this.responseText).token;
  }

  const loginCall = async (username, password) => {
    const url = "http://localhost:3000/login";
    const data = {
      email:username, 
      password:password
    }

    var xmlHttp = new XMLHttpRequest();
    xmlHttp.addEventListener("load", reqListener);
    xmlHttp.open("POST", url, false); // false for synchronous request
    xmlHttp.setRequestHeader("Content-type", "application/json");
    xmlHttp.send(JSON.stringify(data));
    
  }

  const login = async () => {
    try {
      isLoading = true
      await loginCall(state.username, state.password)
      
      if (!user.token) {
        throw({message: "please suplay valid username and password"})
      }
      
      localStorage.setItem("token", user.Token)
      token.set(localStorage.getItem('token'));
      isLoading = false;
      isToken = true

      navigate(PATH_URL.USER_LIST, { replace: false })
    } catch(e) {
      isLoading = false;
      notifications.danger(e.message)
    }
  };

  let widthDiv = 50;
  if (isToken) {
    widthDiv = 100;
  }
 
</script>

<div id="container" style="background-image:url({Images.img_erp});">
  <div id="welcome-div" style="flex:{widthDiv}%;">
    <h1>Selamat datang di svelte skeleton</h1>
    <a href="{PATH_URL.ABOUT}">About Us</a>
  </div>
  {#if !isToken}
  <div id="loginDiv" style="flex: {widthDiv}%;">
    <form on:submit|preventDefault={login}>
      <Input label="Username" name="username" on:input={handleInput} bind:value={state.username} placeholder="username" type="text" required/>
      <Input label="Password" name="password" on:input={handleInput} bind:value={state.password} placeholder="password" type="password" required/>
      <Button isLoading={isLoading}>Login</Button>
    </form>
  </div>
  {/if}
 </div>

<style>
  #container {
    width:100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  #welcome-div, #loginDiv {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-flow: wrap column;
  }

  a {
    color: wheat;
  }
</style>

Last updated