728x90

1. Mouse 이벤트

1단계 - 마우스 위치 좌표 표시

<script>
	let m = { x: 0, y: 0 };

	function handleMousemove(event) {
		m.x = event.clientX;
		m.y = event.clientY;
	}
</script>

<div on:mousemove={handleMousemove}>
	The mouse position is {m.x} x {m.y}
</div>

<style>
	div { width: 100%; height: 100%; }
</style>

2단계 - Event Handlers Inline

<script>
	let m = { x: 0, y: 0 };
</script>

<div on:mousemove={e => m = { x: e.clientX, y: e.clientY }}>
	The mouse position is {m.x} x {m.y}
</div>

<style>
	div { width: 100%; height: 100%; }
</style>

 

2. Click 이벤트

(한 번이라도 클릭한 경우, 더 이상 Alert가 표시되지 않음)

<script>
	function handleClick() {
		alert('no more alerts')
	}
</script>

<button on:click|once={handleClick}>
	Click me
</button>

1. preventDefault - calls event.preventDefault() before running the handler. Useful for client-side form handling, for example. (Handler를 실행하기 전에 preventDefault()를 진행. 클라이언트 측 양식 처리에 유용)
2. stopPropagation - calls event.stopPropagation(), preventing the event reaching the next element. (event가 다음 element에 도달하는 것을 방지)
3. passive - improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so) (터치/휠 이벤트에서 스크롤 성능 향상(Svelte가 안전한 곳에 자동으로 추가))

4. nonpassive - explicitly set passive: false (명시적으로 설정된 패시브: false)
5. capture - fires the handler during the capture phase instead of the bubbling phase (MDN docs) (버블링 단계 대신 캡처 단계에서 처리기를 실행)
6. once - remove the handler after the first time it runs (핸들러를 처음 실행한 후 제거)
7. self - only trigger handler if event.target is the element itself (event.target이 요소 자체인 경우에만 트리거 핸들러)
8. trusted - only trigger handler if event.isTrusted is true. I.e. if the event is triggered by a user action. (event.isTrusted가 true인 경우에만 트리거 핸들러. 즉, 사용자 작업에 의해 이벤트가 트리거된 경우.)

ex)  on:click|once|capture={...}

 

3. Dispatcher

1단계

App.svelte

<script>
	import Inner from './Inner.svelte';

	function handleMessage(event) {
		alert(event.detail.text);
	}
</script>

<Inner on:messageTurtle={handleMessage}/>

Inner.svelte

<script>
	import { createEventDispatcher } from 'svelte';

	const dispatch = createEventDispatcher();

	function sayHello() {
		dispatch('messageTurtle', {
			text: 'Hello!'
		});
	}
</script>

<button on:click={sayHello}>
	Click to say hello
</button>

 

2단계

App.svelte

<script>
	import Outer from './Outer.svelte';

	function handleMessage(event) {
		alert(event.detail.text);
	}
</script>

<Outer on:messageTurtle={handleMessage}/>

Outer.svelte

<script>
	import Inner from './Inner.svelte';
</script>

<Inner on:messageTurtle/>

Inner.svelte

<script>
	import { createEventDispatcher } from 'svelte';

	const dispatch = createEventDispatcher();

	function sayHello() {
		dispatch('messageTurtle', {
			text: 'Hello!'
		});
	}
</script>

<button on:click={sayHello}>
	Click to say hello
</button>

 

3. Event Forwarding

App.svelte

<script>
	import CustomButton from './CustomButton.svelte';

	function handleClick() {
		alert('Button Clicked');
	}
</script>

<CustomButton on:click={handleClick}/>

<button on:click={handleClick}> Click me </button>

CustomButton.svelte

<button on:click>
	Click me
</button>

<style>
	button {
		background: #E2E8F0;
		color: #64748B;
		border: unset;
		border-radius: 6px;
		padding: .75rem 1.5rem;
		cursor: pointer;
	}
	button:hover {
		background: #CBD5E1;
		color: #475569;
	}
	button:focus {
		background: #94A3B8;
		color: #F1F5F9;
	}
</style>

 

728x90

 

4. Input Text Binding

(Input box에 입력한 값을 자동으로 갱신하여 표시)

<script>
	let name = 'world';
</script>

<input bind:value={name}>

<h1>Hello {name}!</h1>

 

5. Input Number Binding

(input 타입 중 number, range에 대해 데이터 연결)

<script>
	let a = 1;
	let b = 2;
</script>

<label>
	<input type=number bind:value={a} min=0 max=10>
	<input type=range bind:value={a} min=0 max=10>
</label>

<label>
	<input type=number bind:value={b} min=0 max=10>
	<input type=range bind:value={b} min=0 max=10>
</label>

<p>{a} + {b} = {a + b}</p>

<style>
	label { display: flex }
	input, p { margin: 6px }
</style>

 

6. Input Checkbox Binding

<script>
	let yes = false;
</script>

<label>
	<input type=checkbox bind:checked={yes}>
	Yes! Send me regular email spam
</label>

{#if yes}
	<p>Thank you. We will bombard your inbox and sell your personal details.</p>
{:else}
	<p>You must opt-in to continue. If you're not paying, you're the product.</p>
{/if}

<button disabled={!yes}>
	Subscribe
</button>

 

7. Input Grouping

<script>
	let scoops = 1;
	let flavours = ['Mint choc chip'];

	let menu = [
		'Cookies and cream',
		'Mint choc chip',
		'Raspberry ripple'
	];

	function join(flavours) {
		if (flavours.length === 1) return flavours[0];
		return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`;
	}
</script>

<h2>Size</h2>

<label>
	<input type=radio bind:group={scoops} name="scoops" value={1}>
	One scoop
</label>

<label>
	<input type=radio bind:group={scoops} name="scoops" value={2}>
	Two scoops
</label>

<label>
	<input type=radio bind:group={scoops} name="scoops" value={3}>
	Three scoops
</label>

<h2>Flavours</h2>

{#each menu as flavour}
	<label>
		<input type=checkbox bind:group={flavours} name="flavours" value={flavour}>
		{flavour}
	</label>
{/each}

{#if flavours.length === 0}
	<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
	<p>Can't order more flavours than scoops!</p>
{:else}
	<p>
		You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
		of {join(flavours)}
	</p>
{/if}

 

8. Inpute Textarea Binding

<script>
	import { marked } from 'marked';
	let value = `Some words are *italic*, some are **bold**`;
</script>

{@html marked(value)}

<textarea bind:value></textarea>

<style>
	textarea { width: 100%; height: 200px; }
</style>

 

9. Input Select Binding

<script>
	let questions = [
		{ id: 1, text: `Where did you go to school?` },
		{ id: 2, text: `What is your mother's name?` },
		{ id: 3, text: `What is another personal fact that an attacker could easily find with Google?` }
	];

	let selected;

	let answer = '';

	function handleSubmit() {
		alert(`answered question ${selected.id} (${selected.text}) with "${answer}"`);
	}
</script>

<h2>Insecurity questions</h2>

<form on:submit|preventDefault={handleSubmit}>
	<select bind:value={selected} on:change="{() => answer = ''}">
		{#each questions as question}
			<option value={question}>
				{question.text}
			</option>
		{/each}
	</select>

	<input bind:value={answer}>

	<button disabled={!answer} type=submit>
		Submit
	</button>
</form>

<p>selected question {selected ? selected.id : '[waiting...]'}</p>

<style>
	input {
		display: block;
		width: 500px;
		max-width: 100%;
	}
</style>

 

10. Select Multiple

(Checkbox 방식을 Select에서도 적용)

<script>
	let scoops = 1;
	let flavours = ['Mint choc chip'];

	let menu = [
		'Cookies and cream',
		'Mint choc chip',
		'Raspberry ripple'
	];

	function join(flavours) {
		if (flavours.length === 1) return flavours[0];
		return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`;
	}
</script>

<h2>Size</h2>

<label>
	<input type=radio bind:group={scoops} value={1}>
	One scoop
</label>

<label>
	<input type=radio bind:group={scoops} value={2}>
	Two scoops
</label>

<label>
	<input type=radio bind:group={scoops} value={3}>
	Three scoops
</label>

<h2>Flavours</h2>

<select multiple bind:value={flavours}>
	{#each menu as flavour}
		<option value={flavour}>
			{flavour}
		</option>
	{/each}
</select>

{#if flavours.length === 0}
	<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
	<p>Can't order more flavours than scoops!</p>
{:else}
	<p>
		You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
		of {join(flavours)}
	</p>
{/if}

 

11. Contenteditable Binding

<script>
	let html = '<p>Write some text!</p>';
</script>

<div
	contenteditable="true"
	bind:innerHTML={html}
></div>

<pre>{html}</pre>

<style>
	[contenteditable] {
		padding: 0.5em;
		border: 1px solid #eee;
		border-radius: 4px;
	}
</style>

 

12. Each Block Binding

<script>
	let todos = [
		{ done: false, text: 'finish Svelte tutorial' },
		{ done: false, text: 'build an app' },
		{ done: false, text: 'world domination' }
	];

	function add() {
		todos = todos.concat({ done: false, text: '' });
	}

	function clear() {
		todos = todos.filter(t => !t.done);
	}

	$: remaining = todos.filter(t => !t.done).length;
</script>

<h1>Todos</h1>

{#each todos as todo}
	<div class:done={todo.done}>
		<input
			type=checkbox
			bind:checked={todo.done}
		>

		<input
			placeholder="What needs to be done?"
			bind:value={todo.text}
		>
	</div>
{/each}

<p>{remaining} remaining</p>

<button on:click={add}>
	Add new
</button>

<button on:click={clear}>
	Clear completed
</button>

<style>
	.done {
		opacity: 0.4;
	}
</style>

 

13. Media Elements

(영상에 대한 데이터를 연동하고 추가로 이벤트 진행-영상 상단에 상태값 표시)

<script>
	// These values are bound to properties of the video
	let time = 0;
	let duration;
	let paused = true;

	let showControls = true;
	let showControlsTimeout;

	// Used to track time of last mouse down event
	let lastMouseDown;

	function handleMove(e) {
		// Make the controls visible, but fade out after
		// 2.5 seconds of inactivity
		clearTimeout(showControlsTimeout);
		showControlsTimeout = setTimeout(() => showControls = false, 2500);
		showControls = true;

		if (!duration) return; // video not loaded yet
		if (e.type !== 'touchmove' && !(e.buttons & 1)) return; // mouse not down

		const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
		const { left, right } = this.getBoundingClientRect();
		time = duration * (clientX - left) / (right - left);
	}

	// we can't rely on the built-in click event, because it fires
	// after a drag — we have to listen for clicks ourselves
	function handleMousedown(e) {
		lastMouseDown = new Date();
	}

	function handleMouseup(e) {
		if (new Date() - lastMouseDown < 300) {
			if (paused) e.target.play();
			else e.target.pause();
		}
	}

	function format(seconds) {
		if (isNaN(seconds)) return '...';

		const minutes = Math.floor(seconds / 60);
		seconds = Math.floor(seconds % 60);
		if (seconds < 10) seconds = '0' + seconds;

		return `${minutes}:${seconds}`;
	}
</script>

<h1>Caminandes: Llamigos</h1>
<p>From <a href="https://studio.blender.org/films">Blender Studio</a>. CC-BY</p>

<div>
	<video
		poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
		src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
		on:mousemove={handleMove}
		on:touchmove|preventDefault={handleMove}
		on:mousedown={handleMousedown}
		on:mouseup={handleMouseup}
		bind:currentTime={time}
		bind:duration
		bind:paused>
		<track kind="captions">
	</video>

	<div class="controls" style="opacity: {duration && showControls ? 1 : 0}">
		<progress value="{(time / duration) || 0}"/>

		<div class="info">
			<span class="time">{format(time)}</span>
			<span>click anywhere to {paused ? 'play' : 'pause'} / drag to seek</span>
			<span class="time">{format(duration)}</span>
		</div>
	</div>
</div>

<style>
	div {
		position: relative;
	}

	.controls {
		position: absolute;
		top: 0;
		width: 100%;
		transition: opacity 1s;
	}

	.info {
		display: flex;
		width: 100%;
		justify-content: space-between;
	}

	span {
		padding: 0.2em 0.5em;
		color: white;
		text-shadow: 0 0 8px black;
		font-size: 1.4em;
		opacity: 0.7;
	}

	.time {
		width: 3em;
	}

	.time:last-child { text-align: right }

	progress {
		display: block;
		width: 100%;
		height: 10px;
		-webkit-appearance: none;
		appearance: none;
	}

	progress::-webkit-progress-bar {
		background-color: rgba(0,0,0,0.2);
	}

	progress::-webkit-progress-value {
		background-color: rgba(255,255,255,0.6);
	}

	video {
		width: 100%;
	}
</style>

<audio>와 <video>의 데이터 바인딩
1) 읽기 전용
duration - 총 재생 시간(초 단위)
buffered - {start, end}의 객체 배열
seekable - ditto
played - ditto
seeking - boolean 값
ended - boolean 값
2) 양방향
currentTime - 현재 지점의 시간(초 단위)
playbackRate - 재생 속도(보통 - 1)
paused - this one should be self-explanatory
volume - 0과 1 사이의 값 지정
muted - true가 음소거된 boolean 값

 

14. 치수(Dimensions)

(Range를 활용해 글자 크기 조정)

<script>
	let w;
	let h;
	let size = 42;
	let text = 'edit me';
</script>

<input type=range bind:value={size}>
<input bind:value={text}>

<p>size: {w}px x {h}px</p>

<div bind:clientWidth={w} bind:clientHeight={h}>
	<span style="font-size: {size}px">{text}</span>
</div>

<style>
	input { display: block; }
	div { display: inline-block; }
	span { word-break: break-all; }
</style>

연결되는 데이터 및 기능은 읽기 전용으로만 사용됨

 

15. Component Binding

App.svelte

<script>
	import Keypad from './Keypad.svelte';

	let pin;
	$: view = pin ? pin.replace(/\d(?!$)/g, '•') : 'enter your pin';

	function handleSubmit() {
		alert(`submitted ${pin}`);
	}
</script>

<h1 style="color: {pin ? '#333' : '#ccc'}">{view}</h1>

<Keypad bind:value={pin} on:submitTurtle={handleSubmit}/>

Keypad.svelte

<script>
	import { createEventDispatcher } from 'svelte';

	export let value = '';

	const dispatch = createEventDispatcher();

	const select = num => () => value += num;
	const clear  = () => value = '';
	const submitKeypad = () => dispatch('submitTurtle');
</script>

<div class="keypad">
	<button on:click={select(1)}>1</button>
	<button on:click={select(2)}>2</button>
	<button on:click={select(3)}>3</button>
	<button on:click={select(4)}>4</button>
	<button on:click={select(5)}>5</button>
	<button on:click={select(6)}>6</button>
	<button on:click={select(7)}>7</button>
	<button on:click={select(8)}>8</button>
	<button on:click={select(9)}>9</button>

	<button disabled={!value} on:click={clear}>clear</button>
	<button on:click={select(0)}>0</button>
	<button disabled={!value} on:click={submitKeypad}>submit</button>
</div>

<style>
	.keypad {
		display: grid;
		grid-template-columns: repeat(3, 5em);
		grid-template-rows: repeat(4, 3em);
		grid-gap: 0.5em
	}

	button {
		margin: 0
	}
</style>

 

16. Binding to component instances

App.svelte

<script>
	import InputField from './InputField.svelte';

	let field;
</script>

<InputField bind:this={field}/>

<button on:click={() => field.focus()}>Focus field</button>

InputField.svelte

<script>
	let input;

	export function focus() {
		input.focus();
	}
</script>

<input bind:this={input} />

DOM 요소에 바인딩할 수 있는 것처럼 구성 요소 인스턴스 자체에도 바인딩이 가능함

예를 들어, DOM Elements를 바인딩할 때와 동일한 방법으로 <InputField> 인스턴스를 변수 명명 필드에 바인딩할 수 있음

728x90

'Info > Svelte' 카테고리의 다른 글

Svelte.js 메모_Logic(로직)  (0) 2023.02.21
Svelte.js 메모_Basic  (0) 2023.02.20
Posted by 게으른거북
:
728x90

1. 조건문

if문

(loggedIn 값에 따른 버튼 표시)

<script>
	let user = { loggedIn: false };

	function toggle() {
		user.loggedIn = !user.loggedIn;
	}
</script>

{#if user.loggedIn}
	<button on:click={toggle}>
		Log out
	</button>
{/if}

{#if !user.loggedIn}
<button on:click={toggle}>
	Log in
</button>
{/if}

if...else문

<script>
	let user = { loggedIn: false };

	function toggle() {
		user.loggedIn = !user.loggedIn;
	}
</script>

{#if user.loggedIn}
	<button on:click={toggle}>
		Log out
	</button>
{:else}
	<button on:click={toggle}>
		Log in
	</button>
{/if}

if...else if...else문

(버튼 클릭 횟수에 따른 결과값 표시)

<script>
	let count = 0;
	
	function incrementCount() {
		count += 1;
	}
</script>

<button on:click={incrementCount}>
	Clicked {count}
</button>

{#if count > 10}
	<p>{count} is greater than 10</p>
{:else if 5 > count}
	<p>{count} is less than 5</p>
{:else}
	<p>{count} is between 5 and 10</p>
{/if}

 

2. each문

1단계

(배열 내 값을 나열하여 표시)

<script>
	let cats = [
		{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
		{ id: 'z_AbfPXTKms', name: 'Maru' },
		{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
	];
</script>

<h1>The Famous Cats of YouTube</h1>

<ul>
	{#each cats as { id, name }, i}
		<li><a target="_blank" href="https://www.youtube.com/watch?v={id}" rel="noreferrer">
			{i + 1}: {name}
		</a></li>
	{/each}
</ul>

2단계

(키 값 지정-버튼 클릭시, 첫번째 배열 제거)

App.svelte

<script>
	import Thing from './Thing.svelte';

	let things = [
		{ id: 1, name: 'apple' },
		{ id: 2, name: 'banana' },
		{ id: 3, name: 'carrot' },
		{ id: 4, name: 'doughnut' },
		{ id: 5, name: 'egg' },
	];

	function handleClick() {
		things = things.slice(1);
	}
</script>

<button on:click={handleClick}>
	Remove first thing
</button>

{#each things as thing (thing.id) }
	<Thing name={thing.name}/>
{/each}

Thing.svelte

<script>
	const emojis = {
        apple: "🍎",
        banana: "🍌",
        carrot: "🥕",
        doughnut: "🍩",
        egg: "🥚"
	}

	// the name is updated whenever the prop value changes...
	export let name;

	// ...but the "emoji" variable is fixed upon initialisation of the component
	const emoji = emojis[name];
</script>

<p>
	<span>The emoji for { name } is { emoji }</span>
</p>

<style>
	p {
		margin: 0.8em 0;
	}
	span {
		display: inline-block;
		padding: 0.2em 1em 0.3em;
		text-align: center;
		border-radius: 0.2em;
		background-color: #FFDFD3;
	}
</style>

 

3. await(대기)

(버튼 클릭에 따른 난수 표시)

<script>
	async function getRandomNumber() {
		const res = await fetch(`/tutorial/random-number`);
		const text = await res.text();

		if (res.ok) {
			return text;
		} else {
			throw new Error(text);
		}
	}
	
	let promise = getRandomNumber();

	function handleClick() {
		promise = getRandomNumber();
	}
</script>

<button on:click={handleClick}>
	generate random number
</button>

<!--
{#await promise then number}
	<p>the number is {number}</p>
{/await}
-->

{#await promise}
	<p>...waiting</p>
{:then number}
	<p>The number is {number}</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}
728x90

'Info > Svelte' 카테고리의 다른 글

Svelte.js 메모_Event/Binding  (0) 2023.02.21
Svelte.js 메모_Basic  (0) 2023.02.20
Posted by 게으른거북
:

Svelte.js 메모_Basic

Info/Svelte 2023. 2. 20. 18:57 |
728x90

Svelte 공식 페이지 내 튜토리얼(링크)

프로젝트 생성

npx degit sveltejs/template (프로젝트 이름)

 

1. 기초 단계 - Hello World 표시

<script>
	let name = 'world';
</script>

<h1>Hello {name}</h1>

 

2. 변수를 활용한 이미지 표시

1단계

<script>
	let src = '/tutorial/image.gif';
</script>

<img src = {src} alt="alt text">

2단계

<script>
	let src = '/tutorial/image.gif';
</script>

<img {src} alt="alt text">

 

3. 캡슐화를 통한 CSS 적용

(캡슐화로 인해 CSS가 별개로 적용)

App.svelte

<script>
	import Nested from './Nested.svelte';
</script>

<p>This is a paragraph.</p>
<Nested/>

<style>
	p {
		color: purple;
		font-family: 'Comic Sans MS', cursive;
		font-size: 2em;
	}
</style>

Nested.svelte

<p>This is another paragraph.</p>

결과화면

 

4. 버튼 카운트

(DOM 실시간 업데이트)

1단계

<script>
	let count = 0;

	function incrementCount() {
		count += 1
	}
</script>

<button on:click={incrementCount}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

 

2단계

<script>
	let count = 0;
	$: doubled = count * 2;

	function handleClick() {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

<p>{count} doubled is {doubled}</p>

결과화면

 

3단계

(카운트 상태에 따라 Alert 표시)

<script>
	let count = 0;
	
	$: if (count >= 10) {
		alert('count is dangerously high!');
		count = 9;
	}

	function handleClick() {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

 

5. 배열 push 기능 대체

( numbers.push(numbers.length + 1); => numbers = [...numbers, numbers.length + 1]; )

<script>
	let numbers = [1, 2, 3, 4];

	function addNumber() {
		numbers = [...numbers, numbers.length + 1];
	}

	$: sum = numbers.reduce((t, n) => t + n, 0);
</script>

<p>{numbers.join(' + ')} = {sum}</p>

<button on:click={addNumber}>
	Add a number
</button>

 

728x90

 

6. Props

1단계 - Export

App.svelte

<script>
	import Nested from './Nested.svelte';
</script>

<Nested answer={42} />

Nested.svelte

<script>
	export let answer;
</script>

<p>The answer is {answer}</p>

 

2단계 

App.svelte

<script>
	import Nested from './Nested.svelte';
</script>

<Nested answer={21}/>
<Nested/>

Nested.svelte

<script>
	export let answer = 'lazeturtle';
</script>

<p>The answer is {answer}</p>

결과화면

3단계 - Package

App.svelte

<script>
	import Info from './Info.svelte';

	const pkg = {
		name: 'svelte',
		version: 3,
		speed: 'blazing',
		website: 'https://svelte.dev'
	};
</script>

<Info {...pkg}/>

Info.svelte

<script>
	export let name;
	export let version;
	export let speed;
	export let website;
</script>

<p>
	The <code>{name}</code> package is {speed} fast.
	Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
	and <a href={website}>learn more here</a>
</p>

 

728x90

'Info > Svelte' 카테고리의 다른 글

Svelte.js 메모_Event/Binding  (0) 2023.02.21
Svelte.js 메모_Logic(로직)  (0) 2023.02.21
Posted by 게으른거북
:
728x90

참고 페이지 : #삽질 없는 개발은 없다

참고 페이지 : #Angular

 

Input Form에 대한 검증하는 기능을 추가하겠습니다.

총 3개의 Input이 있는데, 입력 값에 따라 Issue를 보여줄 수 있도록 합니다.

Web 실행화면

 

더보기

src/app/app.module.ts 수정

작업에 앞서서 먼저 Module를 추가해줍니다.

...
import { FormsModule, ReactiveFormsModule } from '@angular/forms';  // 코드 수정
...

const appRoutes : Routes = [
  ...
]

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    ReactiveFormsModule,  // 코드 추가
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
  ...
}

 

src/app/user/user.component.ts 수정

...
import { FormGroup, FormBuilder, Validators } from '@angular/forms';  // 코드 수정
...

@Component({
  ...
})
export class UserComponent implements OnInit {

  ...

  complexForm : FormGroup;	// 코드 추가

  constructor(
    public service: UserService,
    private toastr: ToastrService,
    private fbuilder : FormBuilder  // 코드 추가
  ) {
      this.service.getUsers()
      .subscribe((data) => {
        ...
      })

    this.service.selectedUser = {
      ...
    };
    
    // 코드 추가
    // 필드 당 여러개의 유효성 검사기 추가. 
    // 두개 이상 검사하려면 Validators.complose 함수로 검사 항목을 맵핑해야함
    this.complexForm = fbuilder.group({
      "name": [
        null, 
        Validators.compose([
          Validators.required,		// 입력 여부 체크
          Validators.minLength(2),	// 최소 글자수 2개
          Validators.maxLength(20),	// 최대 글자수 20개
          Validators.pattern('[ㄱ-ㅎ|가-힣|a-z|A-Z|0-9|\*]+$')	// 한글, 영문(대소문자), 숫자 포함 여부 체크
        ])
      ],
      "email": [
        null, 
        Validators.compose([
          Validators.required,		// 입력 여부 체크
          Validators.email			// 이메일 형식 체크
        ])
      ],
      "password": [
        null, 
        Validators.compose([
          Validators.required,		// 입력 여부 체크
          // 영문(대소문자), 숫자, 특수문자가 각각 1개 이상 포함하여야 하며, 6글자 이상 입력 여부 체크
          Validators.pattern('(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^*+=-]).{6,}$')
        ])
      ]
    })
  }

  ngOnInit(): void {
  }
  
  ...
}

 

Validators 함수 (링크)

class Validators {
  static min(min: number): ValidatorFn
  static max(max: number): ValidatorFn
  static required(control: AbstractControl): ValidationErrors | null
  static requiredTrue(control: AbstractControl): ValidationErrors | null
  static email(control: AbstractControl): ValidationErrors | null
  static minLength(minLength: number): ValidatorFn
  static maxLength(maxLength: number): ValidatorFn
  static pattern(pattern: string | RegExp): ValidatorFn
  static nullValidator(control: AbstractControl): ValidationErrors | null
  static compose(validators: ValidatorFn[]): ValidatorFn | null
  static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn | null
}

 

 

src/app/user/user.component.html 수정

<div class="container">
  <header>
    ...
  </header>

  <hr>

  <div class="row">
    <div class="col-md-12">
      <h3>Insert User Data</h3>
      <!-- 코드 수정 -->
      <form method="post" #form="ngForm" [formGroup]="complexForm"  (ngSubmit)="onSubmit(form)">
        <input type="hidden" name="id" [(ngModel)]="service.selectedUser.id">
        <div class="form-group">
          <label for="name">Name : </label>
          <!-- 코드 수정 -->
          <input type="text" name="name" id="name" class="form-control" placeholder="insert your name"
            [(ngModel)]="service.selectedUser.name" [formControl]="complexForm.controls['name']">
          <!-- 코드 추가 -->
          <div *ngIf="complexForm.controls['name'].touched && !complexForm.controls['name'].valid" class="alert alert-danger">

            <div *ngIf="complexForm.controls['name'].hasError('required')" >필수 입력사항입니다.</div>

            <div *ngIf="complexForm.controls['name'].hasError('pattern')" >특수문자 및 숫자를 입력하실 수 없습니다.</div>

            <div *ngIf="complexForm.controls['name'].hasError('minlength') || complexForm.controls['name'].hasError('maxlength')" >2~20자 이내로 입력해주세요.</div>

          </div>
        </div>
        <div class="form-group">
          <label for="email">Email : </label>
          <!-- 코드 수정 -->
          <input type="text" name="email" id="email" class="form-control" placeholder="insert your email"
            [(ngModel)]="service.selectedUser.email" [formControl]="complexForm.controls['email']">
          <!-- 코드 추가 -->
          <div *ngIf="complexForm.controls['email'].touched && !complexForm.controls['email'].valid" class="alert alert-danger">
            
            <div *ngIf="complexForm.controls['email'].hasError('required')" >필수 입력사항입니다.</div>

            <div *ngIf="complexForm.controls['email'].hasError('email')" >이메일 형식으로 입력해 주세요.</div>

          </div>

        </div>
        <div class="form-group">
          <label for="password">Password : </label>
          <!-- 코드 수정 -->
          <input type="text" name="password" id="password" class="form-control" placeholder="insert your password"
            [(ngModel)]="service.selectedUser.password" [formControl]="complexForm.controls['password']">
          <!-- 코드 추가 -->
          <div *ngIf="complexForm.controls['password'].touched && !complexForm.controls['password'].valid" class="alert alert-danger">
            
            <div *ngIf="complexForm.controls['password'].hasError('required')" >필수 입력사항입니다.</div>

            <div *ngIf="complexForm.controls['password'].hasError('pattern')" >영문(대소문자), 숫자, 특수문자(!@#$%^*+=-)를 조합하여 8자리 이상 입력해주세요.</div>

          </div>

        </div>

        <div class="form-row">
          <div class="d-grid gap-2 d-md-flex justify-content-md-end">
          	<!-- 코드 수정 -->
            <button class="btn btn-sm btn-block btn-primary col-lg-2" [disabled]="!complexForm.valid">
              submit &nbsp; <fa-icon icon="user-plus"></fa-icon>
            </button>

            <button class="btn btn-sm btn-block btn-secondary col-lg-2" (click)="clearForm()">
              clear &nbsp; <fa-icon icon="undo"></fa-icon>
            </button>
          </div>
        </div>
      </form>
    </div>
  </div>

  <div class="row">
    ...
  </div>


  <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
    ...
  </div>

  <br>
  <footer>
    ...
  </footer>
</div>

 

실행화면

Web 실행화면 - 초기화면

초기 실행화면에서는 입력값에 대해 Required Error가 발생되었있기에,

submit 버튼이 비활성화 상태로 되어있습니다.

 

Web 실행화면 - 에러화면

입력폼을 가볍게(?) 터치하고 넘어가면, Required Error로 인해,

문구가 표시되는 것을 볼 수 있습니다.

 

Web 실행화면 - 에러화면

각각의 입력폼에 대한 Error로 표시되는 문구가 다르게 표시되는 이유는,

user.component.html에서 <div> 태그 내 *ngIf를 다르게 지정했기 때문입니다.

 

Web 실행화면 - 성공 화면

입력에 대한 검증에 맞추어 잘 입력해주시면,

Issue 문구도 사라지고, submit 버튼도 활성화 되어집니다.

 

728x90
Posted by 게으른거북
:
728x90

참고페이지 : 구글링 검색을 통해 습득하여 적용

 

기존에 작업하던 app.component에서

새로 생성할 user.component로 코드를 이동하였습니다.

 

페이지를 이원화 하여, Router 연결 작업을 진행하였습니다.

 

더보기

Component 추가

프로젝트 폴더로 이동하여 Component 추가합니다.

cd workspace/angular/myApp
ng generate component user
cmd 결과 화면 - Component 생성

src/app/app.component.html 초기화

기존의 코드로 초기화 하여 새로 생성될 user의 Component와 충돌되지 않도록 합니다.

<div class="mat-app-background">
  <div class="mat-elevation-z4">
    
  </div>
  <!-- 라우터에 연결된 Component 표시해 줍니다. -->
  <router-outlet></router-outlet>
</div>

 

src/app/app.components.ts 초기화

기존의 코드로 초기화 하여 새로 생성될 user의 Component와 충돌되지 않도록 합니다.

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'myApp';
 
  constructor() {  }
  
  ngOnInit(): void {
  }
}

 

src/app/app.module.ts 수정

Router와 관련된 모듈을 Import 해 줍니다.

...

// Router 추가
import { Routes, RouterModule } from '@angular/router';

// URI를 위한 라우터값과 Component를 지정합니다.
const appRoutes : Routes = [
  { path: '', component: AppComponent }, 
  { path: 'user', component: UserComponent }	
]

@NgModule({
  declarations: [
    AppComponent,
    // user component를 생성할 경우 자동으로 생성되느 코드
    UserComponent
  ],
  imports: [
    ...
    
    // 코드 추가
    RouterModule.forRoot(appRoutes)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
  ...
}

 

src/app/user/user.component.ts 수정

app.component.ts에 있는 코드를 가져온 후 수정합니다.

import { Component, OnInit } from '@angular/core';

import { UserService } from './../user.service';
import { FormGroup } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {

  title = 'myApp';
   
  users: Array<any> = [];

  userId = null;  
  displayForm : boolean = false;  

  constructor(
    public service: UserService,
    private toastr: ToastrService
  ) {
    this.service.getUsers()
      .subscribe((data) => {
        this.users = data;
        console.log(this.users);
        this.onForm();
      })

    this.service.selectedUser = {
      "id": null,
      "name": '',
      "email": '',
      "password": ''
    };
  }

  ngOnInit(): void {
  }

  // toastr을 간편하게 쓰기 위한 함수
  public onToastr(service: string, title: string, message: string) {
    switch(service) {
      case "success": {
        this.toastr.success(message, title, {timeOut: 2000});
        break;
      }
      case "info": {
        this.toastr.info(message, title, {timeOut: 2000});
        break;
      }
      case "warning": {
        this.toastr.warning(message, title, {timeOut: 2000});
        break;
      }
      case "error": {
        this.toastr.error(message, title, {timeOut: 2000});
        break;
      }
      default: {
        console.log("Toast Error");
        break;
      }
    }
  }

  public onForm() {
    
    if(this.users.length > 0) {
      this.displayForm = true;
      return;
    }

    this.displayForm = false;
  }

  public onSubmit(form: FormGroup) {
 
    console.log(form.value)

    if( form.value.id == null) {
      console.log(form.value.name)

      this.service.postUser(form.value)
        .subscribe((resp) => {
          console.log(resp)

          if(resp["Status"] == 201) {
            this.clearForm();   

            this.service.getUsers()
              .subscribe((data) => {
                // 코드 추가
                this.onToastr("success", "Data Insert", "새로운 User가 등록되었습니다.");

                this.users = data
                this.onForm();

                this.service.nextUserId = (data[ data.length -1].id +1);
                console.log("ID disponivel : " + this.service.nextUserId);
              });
          }
        });
    } else {
      
      this.service.putUser(form.value)
        .subscribe((resp) => {
          console.log(resp);

          if(resp["Status"] == 200) {
            // 코드 추가
            this.onToastr("info", "Data Edit", "User 정보가 수정되었습니다.");

            this.onForm();
            this.clearForm();
            this.updateList(form.value);
          }
        });

        
    }
  }

  public onEdit(id: string) {

    this.service.getUser(id)
      .subscribe((data) => {
        this.service.selectedUser = data;
      });
  }

  public updateList(user: any) {
    for(var i = 0; i < this.users.length; i++) {
      if(user.id == this.users[i].id) {
        this.users[i] = user;
        return;
      }
    }
  }

  public deleteConfirm(id: string) {
    this.userId = id;
  }

  public cancelDelete() {
    this.userId = null;
    console.log("Cancel User Delete");
  }

  public onDelete() { 

    if(this.userId != null) {
      this.service.deleteUser(this.userId)
        .subscribe((resp) => {
          console.log(resp);
  
          if(resp["Status"] == 200) {
            // 코드 추가
            this.onToastr("error", "Data Delete", "User 정보가 삭제되었습니다.");

            this.service.nextUserId = (this.users[ this.users.length -1].id +1);
            console.log("ID disponivel : " + this.service.nextUserId);

            this.users = this.users.filter((user) => user.id != this.userId);

            this.cancelDelete();
            this.onForm();
          }
        });
    }
  }

  public clearForm() {
    this.service.selectedUser = {
      "id": null,
      "name": '',
      "email": '',
      "password": ''
    };
  }

}

 

src/app/user/user.component.html 수정

<div class="container">
  <header>
    <nav>
      <ul class="nav justify-content-end">
        <li class="nav-item">
          <a class="nav-link active" aria-current="page" href="#">Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">User</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">Contact</a>
        </li>
      </ul>
    </nav>
  </header>

  <hr>

  <div class="row">
    <div class="col-md-12">
      <h3>Insert User Data</h3>
      <form method="post" #form="ngForm" (ngSubmit)="onSubmit(form)">

        <input type="hidden" name="id" [(ngModel)]="service.selectedUser.id">
        <div class="form-group">
          <label for="name">Name : </label>
          <input type="text" name="name" id="name" class="form-control" placeholder="insert your name"
            [(ngModel)]="service.selectedUser.name" required>
        </div>
        <div class="form-group">
          <label for="email">Email : </label>
          <input type="text" name="email" id="email" class="form-control" placeholder="insert your email"
            [(ngModel)]="service.selectedUser.email" required>
        </div>
        <div class="form-group">
          <label for="password">Password : </label>
          <input type="text" name="password" id="password" class="form-control" placeholder="insert your password"
            [(ngModel)]="service.selectedUser.password" required>
        </div>

        <div class="form-row">
          <div class="d-grid gap-2 d-md-flex justify-content-md-end">
            <button class="btn btn-sm btn-block btn-primary col-lg-2" [disabled]="!form.valid">
              submit &nbsp; <fa-icon icon="user-plus"></fa-icon>
            </button>

            <button class="btn btn-sm btn-block btn-secondary col-lg-2" (click)="clearForm()">
              clear &nbsp; <fa-icon icon="undo"></fa-icon>
            </button>
          </div>
        </div>
      </form>
    </div>
  </div>

  <div class="row">
    <div clas="col-md-12" *ngIf="!displayForm">
      <p class="alert alert-warning text-center" >
        호출 가능한 User 데이터가 없습니다. <br>
        새로운 User 를 등록해주세요.
      </p>
    </div>

    <div clas="col-md-12" *ngIf="displayForm">

      <h3>User Data List</h3>

      <table class="table table-bordered table-hover text-center">
        <thead>
          <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Email</th>
            <th>Password</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let user of users">
            <td>{{ user.id }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.email }}</td>
            <td>{{ user.password }}</td>
            <td>
              <button type="button" class="btn btn-sm btn-info col-block col-lg-8" (click)="onEdit(user.id)">
                <fa-icon icon="edit"></fa-icon>
              </button>
            </td>
            <td>
              <button type="button" class="btn btn-sm btn-danger col-block col-lg-8" (click)="onDelete(user.id)" data-bs-toggle="modal" data-bs-target="#exampleModal" (click)="deleteConfirm(user.id)">
                <fa-icon icon="user-minus"></fa-icon>
              </button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>


  <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="exampleModalLabel">User Data Delete</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="cancelDelete()"></button>
        </div>
        <div class="modal-body">
          회원 <strong class="text-danger"> #{{ userId }} </strong> 번의  정보를 삭제 하시겠습니까?
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" (click)="cancelDelete()">
            Cancle
          </button>

          <button type="button" class="btn btn-danger" data-bs-dismiss="modal" (click)="onDelete()">
            Delete
          </button>
        </div>
      </div>
    </div>
  </div>

  <br>
  <footer>
    <p class="alert text-center"></p>
  </footer>
</div>

 

결과화면

Web 결과화면 - app.component.html

위 이미지와 같이 localhost:4200 으로 접속하면,
빈 화면이 표시됩니다.

 

Web 결과화면 - user/user.component.html

위에서 지정했었던 Router 값을 URL(localhost:4200/user)로 접속하면,

미리 작업해 두었던 화면이 표시됩니다.

 

<a> 태그를 활용하여 페이지 이동이 가능한 것입니다.

728x90
Posted by 게으른거북
: