시작
- Todo Code 에 대하여 설명한다.
- https://github.com/MadfishDT/svelte-basic/tree/master/todo
- [GitHub - MadfishDT/svelte-basic: svelte type script example
svelte type script example. Contribute to MadfishDT/svelte-basic development by creating an account on GitHub.
github.com](https://github.com/MadfishDT/svelte-basic/tree/master/todo)
- 코드의 기본 구성은 Typescript + Svelte로 구성되어 있으며, Code 생성은 https://madfishdev.tistory.com/31?category=855358 와 동일한 방식으로 생성 하였다.
구성
- 가장 대중적인 UI Framwork인 Material UI를 기반으로 전체적인 UI를 구성한다.
- 기본 화면의 구성은 Todo 전체 리스트, Todo 생성 화면으로 간단하게 구성하여, Todo 생성 화면에서 생성한 Todo가 Todo 전체 리스트에 보이는 방식이다.
- 이를 위해서 TodoList, NewDialog라는 2개의 Component를 만들었다.
- Todo를 표현하기 위한 Interface를 정의 하였다.
- 보통 Todo는 제목, 설명, 생성일일자 정도로 구성되어 있다, 추가로 favorite(먼저볼거), selected(현재 선택된 아이템), done(완료 표기), end(완료일자) 정보를 추가 하였다.
- Material Card UI를 사용하여, Card를 여러개를 한화면에 세로로 뿌리는 형태를 사용한다.
- 단순이 카드만 뿌리기 때문에 svelte each 문을 사용하여, todo item들을 TodoCard component를 이용하여 뿌리는 작업만 구현된다.
export interface iTodo {
id?: number;
desc?: string;
name: string;
date: Date;
end?: Date;
favorite?: boolean;
selected: boolean;
done?: boolean;
}
- Store 구성
- Todo 정보 리스트
- Todo 추가 함수
- Favorite Todo지정 함수
- Checkd Todo(완료한 Todo) 지정 함수
- Todo 신규 작성 Dialog Open/Close 상태 값
- Todo 정보 리스트
import { writable, get } from "svelte/store";
import type { Todos, iTodo } from "./interface";
export const datas = writable<Todos>([]);
export const selected_items = writable<Todos>([]);
export const openNewTodoDialog = writable<boolean>(false);
let todos: Todos = [];
datas.subscribe((value) => {
todos = value;
});
export const addTodo = (item: iTodo) => {
const newDatas = [...get(datas)];
item.id = newDatas.length;
newDatas.push(item);
datas.set(newDatas);
};
export const favorite = (item: iTodo) => {
const current = get(datas);
const index = current.findIndex((i) => i.id == item.id);
if (index >= 0) current[index].favorite = true;
datas.set([...current]);
};
export const checked = (item: iTodo) => {
const current = get(datas);
const index = current.findIndex((i) => i.id == item.id);
if (index >= 0) current[index].selected = true;
datas.set([...current]);
};
export const openDialog = () => {
const current = get(openNewTodoDialog);
if (!current) openNewTodoDialog.set(true);
};
export const closeDialog = () => {
openNewTodoDialog.set(false);
};
- APP Layout 구성
- 보토의 mobile app과 비슷한 구성으로 상단 헤더에 버튼과 제목
- 메인 화면에는 카드 형식의 리스트
- 팝업 형식으로 구성
- svelte:head의 경우 smui, front, icon등을 상용하기 위한 css 를 code실행전 포함 시키기 위해서 사용하였습니다.
<script lang="ts">
import { onMount } from "svelte";
import Router from "svelte-spa-router";
import routes from "./router";
import { SmapleData } from "./interface";
import TopAppBar, { Row, Section, Title } from "@smui/top-app-bar";
import IconButton from "@smui/icon-button";
import NewTodoDialog from "./NewDialog.svelte"
import { datas, openDialog } from "./store";
export let prominent = false;
export let dense = false;
export let secondaryColor = false;
const loadData = () => {
datas.set(SmapleData);
};
onMount(() => {
loadData();
});
</script>
<svelte:head>
<!-- Fonts -->
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,600,700"
/>
<!-- Material Typography -->
<link
rel="stylesheet"
href="https://unpkg.com/@material/typography@13.0.0/dist/mdc.typography.css"
/>
<!-- SMUI -->
<link
rel="stylesheet"
href="https://unpkg.com/svelte-material-ui/bare.css"
/>
</svelte:head>
<main>
<div class="top-app-bar-container flexor">
<NewTodoDialog />
<TopAppBar
variant="static"
{prominent}
{dense}
color={secondaryColor ? "secondary" : "primary"}
>
<Row>
<Section>
<Title>Mad Todos</Title>
</Section>
<Section align="end" toolbar>
<IconButton class="material-icons" aria-label="add_todo" on:click={() => openDialog()}
>add</IconButton
>
<IconButton
class="material-icons"
aria-label="Bookmark this page">delete</IconButton
>
</Section>
</Row>
</TopAppBar>
<div class="content-container">
<Router {routes} />
</div>
</div>
</main>
<style>
main {
text-align: center;
padding: 1em;
max-width: 480px;
height: 100%;
margin: 0 auto;
}
.top-app-bar-container {
max-width: 480px;
width: 100%;
height: 80%;
border: 1px solid
var(--mdc-theme-text-hint-on-background, rgba(0, 0, 0, 0.1));
background-color: var(--mdc-theme-background, #fff);
overflow-y: hidden;
overflow-x: hidden;
display: inline-block;
}
.content-container {
max-width: 480px;
width: 100%;
height:100%;
border: 1px solid
var(--mdc-theme-text-hint-on-background, rgba(0, 0, 0, 0.1));
background-color: var(--mdc-theme-background, #fff);
overflow-y: auto;
overflow-x: hidden;
display: inline-block;
}
.top-app-buttons {
margin-bottom: 0px;
}
.flexy {
display: flex;
flex-wrap: wrap;
}
.flexor {
display: inline-flex;
flex-direction: column;
}
.flexor-content {
flex-basis: 0;
height: 0;
flex-grow: 1;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
}
@media (min-width: 480px) {
main {
max-width: none;
}
}
</style>
- TodoList Component 구성
- TodoList는 TodoCard 여러개로 구성 됩니다. 아래 코드에서 볼 수 있는 것과 같이 단순히 카드 리스트를 For문으로 뿌리고 있습니다.
- 단 이때 Store에 저장된 datas -> todo정보들을 info라는 곳에 bind해서 넣어주고 있습니다.
- 단순히 readonly라서 bind는 반드시 해줄 필요가 없습니다.
<script lang="ts">
import TodoCard from "./TodoCard.svelte";
import { datas } from "./store";
import type { Todos } from "./interface";
let todos: Todos = [];
datas.subscribe((value) => {
todos = [...value].reverse();
});
</script>
<main>
{#each todos as item}
<TodoCard bind:info={item} />
{/each}
</main>
<style>
main {
text-align: center;
padding: 1em;
max-width: 100%;
}
</style>
- TodoCard
- Todo Card는 아래와 같습니다. SMUI를 이용하여 기본 카드에 버튼과 Text로 이루어져 있습니다.
- 카드에 뿌릴 정보는 위에 정의된 iTodo라는 Interface 타입의 객체인 info 속성에서 가져와 뿌리고 있습니다.
- info는 TodoList에서 TodoCard에 할당하는 속성입니다.
<script lang="ts">
import Card, {
PrimaryAction,
Content,
Actions,
ActionIcons,
} from "@smui/card";
import IconButton from "@smui/icon-button";
import { Icon } from "@smui/common";
import type { iTodo } from "./interface";
import { favorite } from "./store";
import moment from "moment";
export let info: iTodo | undefined = undefined;
export let favoriteToggle = info ? info.favorite : false;
export let checkedToggle = info ? info.selected : false;
</script>
<main>
<div class="card-display">
<div class="card-container">
<Card>
<div class="flexor" style="margin-left: 10px; margin-top:5px;">
<span style="font-size:10px;">
{#if info && info.date}
{moment(info.date).format("YYYY-MM-DD HH:mm:ss")}
{/if}
</span>
</div>
<Actions style="padding-top:0px;">
<div style="text-align: left;">
<h2
class="mdc-typography--headline6"
style="margin: 0;"
>
{#if info && info.name}
{info.name}
{/if}
</h2>
</div>
<ActionIcons>
<IconButton
aria-label="Add to favorites"
title="Add to favorites"
on:click={() => {
favoriteToggle = !favoriteToggle;
if (favoriteToggle) favorite(info);
}}
>
{#if favoriteToggle}
<Icon class="material-icons">favorite</Icon>
{:else}
<Icon class="material-icons"
>favorite_border</Icon
>
{/if}
</IconButton>
<IconButton
aria-label="Add to favorites"
title="Add to favorites"
on:click={() => (checkedToggle = !checkedToggle)}
>
{#if checkedToggle}
<Icon class="material-icons">check_box</Icon>
{:else}
<Icon class="material-icons"
>check_box_outline_blank</Icon
>
{/if}
</IconButton>
</ActionIcons>
</Actions>
<PrimaryAction>
<Content class="mdc-typography--body2">
<div class="flexor">
{#if info && info.desc}
{info.desc}
{/if}
</div>
</Content>
</PrimaryAction>
</Card>
</div>
</div>
</main>
<style>
main {
text-align: center;
padding: 1em;
max-width: 100%;
}
.flexor {
text-align: center;
display: flex;
flex-wrap: wrap;
}
</style>
- 다이얼 로그
- 제목과 할일 설명을 작성하고 완료 버튼을 누를 수 있는 단순한 다이얼로그
- 적용이나 Enter Key 입력시, store의 addTodo 함수를 호출하여 신규 Todo를 TodoList에 추가 한다.
<script lang="ts">
import Textfield from "@smui/textfield";
import CharacterCounter from "@smui/textfield/character-counter";
import Icon from "@smui/textfield/icon";
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import { openNewTodoDialog, closeDialog, addTodo } from "./store";
import type { iTodo } from "./interface";
import { debounce } from "lodash";
let open = false;
let name = "";
let desc = "";
const onAddTodo = () => {
if (name && name.length > 0) {
const todo: iTodo = {
id: 0,
desc: desc,
name: name,
date: new Date(Date.now()),
selected: false,
};
name = "";
addTodo(todo);
return true;
}
return false;
};
const onEnterEvent = debounce((e: CustomEvent) => {
const ke = e as unknown as KeyboardEvent;
if (ke.code == "Enter") {
if (onAddTodo()) closeDialog();
}
});
const onClose = () => {
name = "";
desc = "";
closeDialog();
};
openNewTodoDialog.subscribe((value: boolean) => {
open = value;
});
</script>
<Dialog
bind:open
aria-labelledby="buttons-title"
aria-describedby="buttons-content"
autoStackButtons={false}
on:SMUIDialog:closed={(e) => ($openNewTodoDialog = false)}
>
<Title id="buttons-title">새로운 할일</Title>
<Content id="buttons-content" style={"width:400px; padding: 14px"}>
<Textfield
variant="outlined"
bind:value={name}
label="할일 추가"
style={"width:100%"}
on:keydown={onEnterEvent}
>
<Icon class="material-icons" slot="leadingIcon">work</Icon>
</Textfield>
<div style={"width:100%;height:200px;margin-top:13px"}>
<Textfield
style={"width:100%;height:100%"}
textarea
variant="outlined"
input$maxlength={100}
bind:value={desc}
on:keydown={onEnterEvent}
label="설명"
>
<CharacterCounter slot="internalCounter"
>0 / 100</CharacterCounter
>
</Textfield>
</div>
</Content>
<Actions>
<div class="flexy">
<div>
<Button on:click={onAddTodo}>
<Label>적용</Label>
</Button>
</div>
<div>
<Button on:click={closeDialog}>
<Label>닫기</Label>
</Button>
</div>
</div>
</Actions>
</Dialog>
<style>
.flexy {
display: flex;
flex-wrap: wrap;
}
</style>
'SPA' 카테고리의 다른 글
Svelte 3 -> 4 migration (0) | 2023.10.01 |
---|---|
Svelte Material Todos (1) (0) | 2022.08.23 |
Svelte Material UI 적용하기 (0) | 2022.05.30 |
Svelte Store (0) | 2021.10.31 |
Svelte 글자 바꾸기2 (0) | 2021.03.24 |