js에서 투두리스트(Todo List)를 예제를 통해 포스팅한다.
지난번 포스팅에서 피드백 받은 내용에 대해 개선작업을 완료 했는데
해당 코드에 대해 다시 피드백 받은 내용은 다음과 같다.
1. todoCount는 this.num으로 교체해도 되지 않는지? (할 일을 제거할 땐 어떻게 할 건지)
2. id 낭비하지말고 data 속성을 쓰면 좋다. (data-id 또는 data-todo-id 이런식으로)
3. className = "todo"; 같은 경우는 classList를 써라. (classList.add('todo') 이런식으로)
4. 체크박스에 거는 이벤트는 엘리먼트 만들 때 바로바로 걸어라 그럼 좀 더 간결해진다.
5. innerHtml같은경우는 살짝 위험하다 나중에 디비 넣을 때 아무처리없이 다이렉트로 넣기는 좀 그렇다.
6. getList().slice().foreach 이부분이 나중에 로직 꼬일 위험감이 느껴진다.
다음 소스코드는 피드백을 반영하여 개선작업한 주요 소스코드이다.
example.html
<!DOCTYPE html>
<html>
<head>
<title>할 일 앱 만들기 예제</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="title">
<h1>나의 하루</h1>
<h2>10월 28일</h2>
</div>
<div class="todo-container">
</div>
<div class="add-todo">
<button>+</button>
<input type="text" placeholder="할 일 추가">
</div>
</body>
</html>
index.js
import './style.css';
import {Todo, TodoManager} from './models.js';
class TodoApp {
constructor(todos) {
this.todoManager = new TodoManager(todos);
this.todoContainerEl = document.querySelector(".todo-container");
this.titleEl = document.querySelector(".title h2");
this.plusBtnEl = document.querySelector(".add-todo button");
this.renderTodos(); // 할 일 데이터를 화면에 그림
this.bindEvents(); // 이벤트에 반응하는 리스너 함수를 등록하는 메소드 (메소드와 사용자입력에 따른)
}
renderTodos() { // 할 일 데이터를 화면에 그림
const todoCount = document.getElementsByClassName('todo').length;
this.todoManager.getList().slice(todoCount).forEach((todo, i) => {
const todoEl = this.createTodoEl(todo, todoCount + i);
this.todoContainerEl.appendChild(todoEl);
console.log(todoCount + i);
});
this.renderTitle();
}
createTodoEl(todo, id) { // '+'버튼을 눌렀을 때 생성 될 div 영역.
const todoEl = document.createElement("div");
todoEl.dataset.todoId = id;
todoEl.classList.add("todo");
todoEl.innerHTML =
`<input type="checkbox" ${todo.done ? "checked" : ""}>
<label>${todo.contents}</label>`;
todoEl.addEventListener('click', evt => {
if(evt.target.type == 'checkbox') {
const index = todoEl.dataset.todoId;
this.todoManager.getList()[index].toggle();
this.renderTitle();
}
})
return todoEl;
}
renderTitle() { // 현재날짜와 남은 할 일 갱신
const now = new Date();
const month = now.getMonth() + 1;
const date = now.getDate();
if (this.titleEl) {
this.titleEl.innerHTML =
`${month}월 ${date}일 <span class="left-count">
(${this.todoManager.leftTodoCount}개)</span>`;
}
}
bindEvents() {
this.plusBtnEl.addEventListener('click', evt => {
const textEl = document.querySelector('.add-todo input[type="text"]');
this.todoManager.addTodo(textEl.value);
textEl.value = '';
this.renderTodos();
});
}
}
const todoApp = new TodoApp([
{ contents: "공부하기", done: false },
{ contents: "놀기", done: true },
{ contents: "밥먹기", done: false }
]);
다음 포스팅에는 투두리스트에 삭제기능을 추가해볼 것이다.
js에서 투두리스트(Todo List)를 예제를 통해 포스팅한다.
지난번 포스팅에 이어서 화면부분에 대한 작업이다.
'할 일 추가'란에 텍스트를 입력 후, '+'버튼을 누르면 할 일을 추가할 수 있다.
각각의 할 일은 체크박스처럼 완료 여부를 지정할 수 있다.
현재 체크되 있는 완료 여부의 개수는 변경될 때 마다 갱신 된다.
아래는 '공부하기', '놀기', '밥먹기'라는 3개의 할 일이 추가된 상태에서
'산책하기'를 입력한 후, '+'버튼을 클릭해서 할 일을 추가한 모습이다.
example.html
<!DOCTYPE html>
<html>
<head>
<title>할 일 앱 만들기 예제</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="title">
<h1>나의 하루</h1>
<h2>10월 28일</h2>
</div>
<div class="todo-container">
</div>
<div class="add-todo">
<button>+</button>
<input type="text" placeholder="할 일 추가">
</div>
<script src="./models.js"></script>
<script src="./app.js"></script>
<script>
const todoApp = new TodoApp([ // A
{ contents: "공부하기", done: false },
{ contents: "놀기", done: true },
{ contents: "밥먹기", done: false }
]);
</script>
</body>
</html>
app.js
class TodoApp {
constructor(todos) { // B
this.todoManager = new TodoManager(todos);
this.todoContainerEl = document.querySelector(".todo-container");
this.titleEl = document.querySelector(".title h2");
this.plusBtnEl = document.querySelector(".add-todo button");
this.bindEvents();
}
renderTodos() { // C
this.todoContainerEl.innerHTML = '';
this.todoManager.getList().forEach((todo, i) => {
const todoEl = this.createTodoEl(todo, i);
this.todoContainerEl.appendChild(todoEl);
});
this.renderTitle();
}
createTodoEl(todo, id) { // D
const todoEl = document.createElement("div");
todoEl.id = "todo-" + id;
todoEl.className = "todo";
todoEl.innerHTML =
`<input type="checkbox" ${todo.done ? "checked" : ""}>
<label>${todo.contents}</label>`;
return todoEl;
}
renderTitle() { // E
const now = new Date();
const month = now.getMonth();
console.log('month = ', month);
const date = now.getDate();
if (this.titleEl) {
this.titleEl.innerHTML =
`${month}월 ${date}일 <span class="left-count">
(${this.todoManager.leftTodoCount}개)</span>`;
}
}
bindEvents() { // F
this.plusBtnEl.addEventListener('click', evt => {
const textEl = document.querySelector('.add-todo input[type="text"]');
this.todoManager.addTodo(textEl.value);
textEl.value = '';
this.renderTodos();
});
this.todoContainerEl.addEventListener('click', evt => {
if (evt.target.nodeName === 'INPUT' && evt.target.parentElement.className === 'todo') {
const clickedEl = evt.target.parentElement;
const index = clickedEl.id.replace('todo-', '');
this.todoManager.getList()[index].toggle();
this.renderTitle();
}
});
}
}
위에 주석친 부분을 문단단위로 정리한다.
// 할 일 데이터를 화면에 그림
[A] TodoApp()클래스를 통해 3개의 할 일과 완료여부를 생성자를 통해 인스턴스를 생성한다.
[B] models.js의 TodoManager()클래스를 사용해 생성자로 전달받은 할 일들과 완료여부를 리스트에 모두 push시킨다.
할 일들의 내용이 표시되는 영역을 찾아 변수 선언한다.
타이틀 영역을 찾아 변수 선언한다. (현재날짜와 완료여부 개수가 표시되는 곳)
'+'버튼을 찾아 변수 선언한다.
bindEvents()를 호출한다. (처리해야 할 이벤트를 모아놓은 메소드)
[C] renderTodos()를 정의한다. (할 일 데이터를 화면에 그려주는 메소드)
모든 데이터를 지우고 todoManager의 getList()를 통해 할 일과 완료여부가 담긴 리스트를 가져온다.
각각의 할일과 완료여부는 createTodoEl()에 전달한다. (해당 데이터로 div영역을 생성하는 메소드)
생성된 div영역은 todoContainer의 자식으로 달아준다.
renderTitle()메소드를 통해 현재날짜와 남은 할 일을 갱신한다.
[D] createTodoEl()메소드를 정의한다.
('+'버튼을 눌렀을 때 할 일 데이터를 통해 화면에 그려질 div영역을 생성한다)
[E] renderTitle()메소드를 정의한다.
(현재날짜와 남은 할 일을 갱신한다)
Date()객체를 사용해 현재 날짜를 가져온다.
todoManager의 leftTodoCount()메소드를 통해 남은 할 일의 개수를 표시해준다.
이 메소드는 할 일이 추가되거나 완료여부가 변경될 때 마다 호출 되어야 한다.
[F] bindEvents()메소드를 정의한다. (등록해야하는 이벤트리스너를 모아놓은 메소드)
plusBtnEl에 등록한 이벤트리스너는 할 일을 입력하고 '+'버튼을 클릭하면 발생된다.
입력한 내용은 todoManager를 통해 할 일의 내용과 완료여부를 추가한다.
input창에 입력한 내용은 공백으로 비워준다.
renderTodos()메소드를 통해 할 일들을 화면에 다시 그려준다.
todoContainerEl에 등록한 이벤트리스너는 완료 여부를 클릭했을 경우 발생된다.
(클릭한 타겟의 nodeName이 'input'이면서 부모요소의 class가 'todo'일 경우)
몇번째에 있는 할 일의 완료 여부를 클릭했는지 파악하기 위해 id값에 있는 todo- 뒤에 숫자를 추출한다.
추출한 숫자로 todoManager의 getList를 통해 인덱스로 접근 후 toggle()메소드를 수행한다. (t->f OR f->t)
renderTitle()메소드를 통해 현재날짜와 남은 할 일을 갱신한다.
이 방식은 3개의 할 일이 등록되어 있을 때, 4번째 할 일을 추가할 경우
현재 화면에 보이는 모든 할 일을 제거한 후 할 일들을 다시 하나하나 표시한다.
이러한 경우 화면깜박임이 발생될 수 있으며, 퍼포먼스적으로도 효율적이지 않다고 한다.
따라서, 할 일을 추가할 경우 기존에 있는 할 일들은 그대로 둔 상태에서
새로운 할 일만 추가되도록 변경해야 한다.
원래 포스팅했던 내용은 할 일을 추가할 때마다 다 지우고 처음부터 그리는 방식이다.
이 방식은 위에도 언급했지만 비효율적이다. 그렇기 때문에
할 일을 추가하면 기존 할 일은 그대로 두고 추가되는 것만 그리는 방식으로 수정했다.
아래와 같이 [B]부분과 [C]부분을 수정하면 된다.
원래는 할 일의 리스트를 getList()로 모두 받아온 뒤, forEach문을 처음부터 돌면서 그렸지만
getList()로 받아온 것을 slice()메소드를 통해 현재 할일의 개수를 start인덱스를 주어서
마지막 할 일의 다음부터 그리도록 변경하였다.
constructor(todos) { // B
this.num = 0;
this.todoManager = new TodoManager(todos);
this.todoContainerEl = document.querySelector(".todo-container");
this.titleEl = document.querySelector(".title h2");
this.plusBtnEl = document.querySelector(".add-todo button");
this.renderTodos();
this.bindEvents();
}
renderTodos() { // C
const todoCount = document.getElementsByClassName('todo').length;
this.todoManager.getList().slice(todoCount).forEach(todo => {
const todoEl = this.createTodoEl(todo, this.num);
this.todoContainerEl.appendChild(todoEl);
console.log(this.num++);
});
this.renderTitle();
}
js에서 투두리스트(Todo List)를 예제를 통해 포스팅한다.
투두리스트라는 것은 간단하게 오늘의 계획을 작성하고,
계획을 완료하면 체크를 해나가면서 관리하는
즉, 스케쥴을 작성하고 관리하는 일종의 체크리스트 같은 것이다.
이번 포스팅에서는 화면작업을 들어가기전 할 일 들을 입출력하는 작업이다.
이를 위한 models.js의 주요 내용은 다음과 같다.
[Todo class] 할 일에 다음과 같은 내용을 저장한다. (계획의 내용, 완료여부)
완료여부를 토글 할 수 있다. (호출 시 true -> false 또는 false -> true)
[TodoManager class] 할 일을 지속해서 추가할 수 있고
모든 할 일들을 가져올 수 있으며,
완료 되지 않은 할일의 개수를 가져올 수 있다.
example.html
<!DOCTYPE html>
<html>
<head>
<title>할일 앱 만들기 예제</title>
</head>
<body>
<script src="./src/models.js"></script>
<script>
const todos = new TodoManager(); // A
todos.addTodo('공부하기');
todos.addTodo('운동하기');
console.log(todos.getList());
console.log(todos.leftTodoCount);
setTimeout(() => {
todos.getList()[0].toggle()
console.log(todos.leftTodoCount);
console.log(todos.getList());
}, 3000);
</script>
</body>
</html>
models.js
class Todo { // B
constructor(contents, done) {
this.contents = contents;
this.done = done;
}
toggle() {
this.done = !this.done;
}
}
class TodoManager { // C
constructor(todos = []) {
this._todos = [];
todos.forEach(todo => {
this.addTodo(todo.contents, todo.done);
});
}
addTodo(contents, done = false) { // D
const newTodo = new Todo(contents, done);
this._todos.push(newTodo);
return newTodo;
}
getList() { // E
return this._todos;
}
get leftTodoCount() { // F
return this._todos.reduce((p, c) => {
if (c.done === false) {
return ++p;
} else {
return p;
}
}, 0);
}
}
위에 주석친 부분을 문단단위로 정리한다.
[A] TodoManager()클래스를 통해 인스턴스를 생성한다.
addTodo()메소드를 통해 2개의 할 일을 추가한다.
추가했던 할 일의 목록을 getList()메소드를 사용해 출력한다.
leftTodoCount메소드를 사용해 남아있는 할 일의 개수를 출력한다.
setTimeout()메소드를 통해 3초 후 아래의 명령을 수행한다.
getList()메소드를 통해 첫번째에 등록되 있는 할 일을 토글한다.
(완료여부가 true면 false, false면 true로 변경 됨)
leftTodoCount메소드를 사용해 남아있는 할 일의 개수를 출력한다.
할 일의 목록을 getList()메소드를 사용해 출력한다.
[B] Todo클래스를 정의한다.
생성자(constructor)는 할 일과 완료 여부를 설정할 수 있다.
toggle()메소드를 정의한다.
완료 된 상태에서 호출하면 완료되지 않은 상태가 된다.
반대로 완료되지 않은 상태에서 호출하면 완료 된 상태가 된다.
[C] TodoManager클래스를 정의한다.
생성자(constructor)는 할 일의 리스트를 전달받았을 경우
addTodo()메소드를 통해 각각의 할 일을 추가한다.
(멤버변수 _todos라는 리스트에 추가 된다)
[D] addTodo()메소드를 정의한다.
할 일과 완료 여부를 인자로 받는다.
Todo()클래스를 통해 할 일과 완료 여부를 설정한 인스턴스를 생성한다.
멤버변수인 _todos 리스트에 push()메소드를 통해 원소를 추가한다.
Todo()클래스를 통해 생성한 인스턴스를 반환한다.
[E] getList()메소드를 정의한다.
멤버변수인 _todos 리스트를 반환한다.
[F] leftTodoCount()메소드를 정의한다.
이 메소드는 남은 할일의 개수를 반환한다.
읽기만 가능한 메소드이기 때문에 앞에 get이 붙는다.
멤버변수인 _todos 리스트에 reduce()메소드를 통해
완료 여부가 false인 경우만 count를 진행한다.
즉, 미완료 작업의 개수만 더해서 반환한다.
처음에 2개의 할 일을 추가했으므로 2개의 할 일이 기록된 리스트가 출력된다.
그리고 남은 할 일의 개수를 출력했을 때 2가 출력된다.
그 후, 3초뒤 첫번째 할 일에 toggle()을 수행하고
남은 할 일의 개수를 출력하면 1이 출력된다.
그 이유는 남은 할 일의 개수는 미완료 작업의 개수를 반환하는데
첫번째 할 일에 toggle()을 수행함으로 써 완료 여부가
false에서 true로 변경되었기 때문에
남은 미완료 작업의 개수는 1개가 되기 때문이다.
한번 더 할 일 리스트를 출력해보면 첫번째 할 일이 true로 변경되어있다.
js에서 간단한 에디터를 만드는 방법을 예제를 통해 정리한다.
12개의 기능이 있는 간단한 에디터가 생성된다.
example.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css"> <!-- A -->
<link rel="stylesheet" href="style.css">
<title>간단한 텍스트 에디터 만들기 예제</title>
</head>
<body>
<div class="toolbar"> <!-- B -->
<a href="" data-command='h1'>H1</a>
<a href="" data-command='h2'>H2</a>
<a href="" data-command='h3'>H3</a>
<a href="" data-command='p' style="margin-right: 8px;">P</a>
<a href="" data-command='bold'>
<i class='fa fa-bold'></i>
</a>
<a href="" data-command='italic'>
<i class='fa fa-italic'></i>
</a>
<a href="" data-command='underline'>
<i class='fa fa-underline'></i>
</a>
<a href="" data-command='strikeThrough'style="margin-right: 8px;">
<i class='fa fa-strikethrough'></i>
</a>
<a href="" data-command='justifyLeft'>
<i class='fa fa-align-left'></i>
</a>
<a href="" data-command='justifyCenter'>
<i class='fa fa-align-center'></i>
</a>
<a href="" data-command='justifyRight'>
<i class='fa fa-align-right'></i>
</a>
<a href="" data-command='justifyFull' style="margin-right: 8px;">
<i class='fa fa-align-justify'></i>
</a>
</div>
<div class='editor' contenteditable="true"> <!-- contenteditable이 true면 편집가능 -->
<h1>심플 에디터</h1>
<p>간단한 에디터</p>
</div>
<script>
document.querySelectorAll('.toolbar a') // C
.forEach(aEl => aEl.addEventListener('click', function (e) {
e.preventDefault();
const command = aEl.dataset.command;
if (command == 'h1' || command == 'h2' || command == 'h3' || command == 'p') {
document.execCommand('formatBlock', false, command);
}
else {
document.execCommand(command);
}
}));
})
</script>
</body>
</html>
(style.css 생략)
위에 주석친 부분을 문단단위로 정리한다.
[A] 폰트 어썸 css를 적용한다. html에서 'fa fa-bold'처럼 css클래스 이름을 부여하면 적용할 수 있다.
[B] 상단에 있는 툴바를 작성한다. data-command를 사용해
data-command='h1'처럼 텍스트에 전달할 명령을 작성할 수 있다.
[C] 툴바 영역의 모든 버튼 선택하고 각각의 버튼에 클릭 이벤트리스너를 등록한다.
이벤트 발생 시 수행동작은 기본 행위를 방지한다.
그리고 data-command 속성값을 dataset 객체의 command 속성을 통해 가져온다.
가져온 command속성값은 클릭한 기능을 적용하기 위해 execComand()메소드에 전달한다.
[ execCommand(명령이름, 기본 사용자 UI를 보여주는 여부, 특정 명령에 필요한 값) ]
command값이 'h1', 'h2', 'h3', 'p'인 경우는 다음 공식 문서에 따라 'formatBlock' 명령을 전달해주어야한다.
https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands
'formatBlock', 'fontSize' 같은 다양한 명령은 위 링크에서 확인할 수 있다.
공부할 겸 카피기능과 텍스트크기를 지정하는 기능 2개를 더 추가해보았다.
이렇게 하려면 html과 script에 아래 코드를 추가해주면 된다.
html
<a href="" data-command='copy' style="margin-right: 8px;">
<i class='fa fa-copy'></i>
</a>
<select name="job" id='test'>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
script
document.getElementById('test').addEventListener('change', function (e) {
document.execCommand('fontSize', false, e.target.value);
}
js에서 멀티 슬라이드(Multi Slide)에 대한 내용을 예제를 통해 정리한다.
왼쪽 화살표와 오른쪽 화살표 버튼을 통해 아이템을 넘길 수가 있다.
넘어갈 때는 아이템 1개의 너비만큼 스크롤이 이동된다.
example.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>멀티 슬라이드쇼 만들기</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>국내 여행</h1>
<div class="slider">
<div class="slider-btn-wrapper slider-btn-wrapper-left"> <!-- 왼쪽 화살표 -->
<button id="left-btn" class="slider-btn">←</button>
</div>
<div class="item-wrapper">
<div class="item">
<img src="./images/seoul.jpg"/>
<div class="title">
<h2>서울</h2>
<p>3000원</p>
</div>
</div>
<div class="item">
<img src="./images/jeju.jpg"/>
<div class="title">
<h2>제주도</h2>
<p>4000원</p>
</div>
</div>
<div class="item">
<img src="images/suwon.jpg"/>
<div class="title">
<h2>수원</h2>
<p>3000원</p>
</div>
</div>
<div class="item">
<img src="./images/muju.jpg"/>
<div class="title">
<h2>무주</h2>
<p>5000원</p>
</div>
</div>
</div>
<div class="slider-btn-wrapper slider-btn-wrapper-right"> <!-- 오른쪽 화살표 -->
<button id="right-btn" class="slider-btn">→</button>
</div>
</div>
<script>
(function() { // A
const itemWrapperEl = document.querySelector('.item-wrapper'),
leftBtnEl = document.getElementById('left-btn'),
rightBtnEl = document.getElementById('right-btn');
function moveSlides(direction) { // B
const item = itemWrapperEl.querySelector('.item'),
itemMargin = parseFloat(getComputedStyle(item).marginRight);
itemWidth = itemMargin + item.offsetWidth + 2;
let itemCount = Math.round(itemWrapperEl.scrollLeft / itemWidth);
if (direction === 'left') {
itemCount = itemCount - 1;
} else {
itemCount = itemCount + 1;
}
itemWrapperEl.scrollLeft = itemWidth * itemCount;
}
leftBtnEl.addEventListener("click", e => moveSlides("left")); // C
rightBtnEl.addEventListener("click", e => moveSlides("right"));
})();
</script>
</body>
</html>
(style.css 생략)
위에 주석친 부분을 문단단위로 정리한다.
[A] 즉각 호출 패턴을 통해 정의와 동시에 실행한다.
아이템들을 감싸는 부모 요소를 찾아 변수 선언한다. (class가 item-wrapper인)
왼쪽, 오른쪽 버튼을 찾아 변수 선언한다.
[B] moveSlides()메소드를 정의한다. (버튼을 클릭했을 때 슬라이드를 이동시킬 거리를 구하고 위치값을 지정한다)
왼쪽 버튼을 클릭했는지, 오른쪽 버튼을 클릭했는 지를 인자로 받는다.
아이템을 선택하고 getComputedStyle()메소드와 marginRight값을 통해 오른쪽 마진값을 구한다.
(getComputedStyle()메소드는 해당 인자의 스타일 반환한다)
(margin은 px을 포함한 문자열로 반환되기 때문에 parseFloat()메소드를 통해 소수점으로 변환한다)
offsetWidth값을 통해 아이템의 너비값을 구한다.
위에서 구한 마진값과 임의의 숫자값 2를 더하여
최종적으로 아이템의 너비값을 구한다.
scrollLeft값은 왼쪽으로 스크롤바가 얼만큼 움직였는지 구할 수 있다.
이 값을 아이템 너비값으로 나누면 몇개의 아이템을 넘겼는지 알 수 있다.
나눈 값은 소수점이므로, Math.round()메소드를 통해 반올림한다.
왼쪽을 클릭한 경우, -1 (현재 스크롤의 위치가 아이템 3개를 넘어간 경우 왼쪽이면 2개를 넘어간 위치로 지정하기 위해)
오른쪽을 클릭한 경우, +1 (현재 스크롤의 위치가 아이템 3개를 넘어간 경우 오른쪽이면 4개를 넘어간 위치로 지정하기 위해)
그 후, 최종적으로 scrollLeft값을 통해 이동할 스크롤의 위치값을 지정한다.
[C] 왼쪽 버튼 클릭 시 moveSlides()메소드에 'left'를
오른쪽 버튼 클릭 시 moveSlides()메소드에 'right'를 전달한다.
js에서 탭 메뉴(Tab Menu)에 대한 내용을 예제를 통해 정리한다.
'체코'탭을 누르면 url뒤에 #czech가
'독일'탭을 누르면 url뒤에 #germany가 붙는다.
이처럼 탭이 바뀔 때마다 브라우저의 url이 변경되는
해쉬 URL값을 통한 탭 메뉴 예제이다.
example.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>탭메뉴 만들기 예제</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>여행지 설명</h1>
<div class="tabs"> <!-- A -->
<ul>
<li><a href="#czech">체코</a></li>
<li><a href="#germany">독일</a></li>
<li><a href="#british">영국</a></li>
</ul>
<div class="tab_content">
<div id="czech">
<h3>체코</h3>
<p>체코는 아름다운 동유럽의 나라입니다.</p>
</div>
<div id="germany">
<h3>독일</h3>
<p>독일은 맥주가 유명한 유럽의 나라입니다.</p>
</div>
<div id="british">
<h3>영국</h3>
<p>영국은 유럽의 서북쪽에 위치한 섬나라입니다.</p>
</div>
</div>
</div>
<script>
function createTabs(selector) { // B
const el = document.querySelector(selector);
const liEls = el.querySelectorAll('ul li');
const tabContentEl = el.querySelector('.tab_content');
const firstTabEl = liEls.item(0).firstElementChild
function activate(target) { // C
const hash = target.hash;
const anchors = target.closest('ul')
.querySelectorAll('li a');
Array.from(anchors).forEach(v => v.className = '');
Array.from(tabContentEl.children).forEach(v => v.style.display = 'none');
tabContentEl.querySelector(hash).style.display = '';
target.className = 'active';
} // 끝
const handleHash = () => { // D
if (location.hash) {
const selector = `a[href="${location.hash}"]`;
activate(document.querySelector(selector));
} else {
activate(firstTabEl);
}
}
window.addEventListener('hashchange', handleHash); // E
handleHash();
}
createTabs('.tabs'); // F
</script>
</body>
</html>
(style.css 생략)
위에 주석친 부분을 문단단위로 정리한다.
[A] a태그에 해시값(#)이 붙는 특징이 있다.
[B] 탭을 만드는 메소드를 정의한다. 탭을 만들어야 할 요소를 인자로 받는다.
전달받은 요소를 selector로 찾아서 변수 선언한다.
ul밑에 있는 모든 li를 liEls로 변수 선언한다.
class가 tab_content인 요소를 selector로 찾아서 변수 선언한다.
모든 li중 첫번째 li의 child요소를 첫번째탭 요소로 지정하기 위한 변수 선언한다.
[C] 특정탭을 활성화하기 위한 activate()메소드를 정의한다. 특정탭의 <a>요소를 인자로 받는다.
전달받은 <a>요소의 hash 속성값을 상수로 정의한다.
(hasg 속성값은 예를들면 #czech #germany #british ... 이다)
closest()메소드를 통해 부모 요소들 중 가장 가까운 <ul> 요소를 선택하고
해당 ul요소의 li요소에 있는 모든 a요소를 변수에 할당한다.
기존에 활성화된 탭을 제거하기 위해 모든 a요소들의 클래스 명을 제거한다.
전체 탭의 상세 내용을 담고 있는 모든 요소(class가 tab_content인)들을 화면에서 보이지 않게 처리한다.
hash를 selector로 찾은 후, display를 초기화해서 화면에 보여지게 한다.
[D] 해쉬가 변경될 때 처리하는 콜백 메소드를 정의한다.
현재 해쉬값이 있으면 해당 해쉬값을 href 속성으로 가지는 탭버튼을 선택하고, activate()메소드를 통해 해당 탭버튼 활성화한다.
현재 해쉬값이 없으면 첫번째 탭을 활성화 시킨다.
[E] 'hashchange'이벤트 리스너를 등록한다. (브라우저의 URL 해쉬값이 변경될 때마다 'hashchange'이벤트가 발생한다.)
handleHash()콜백 메소드를 통해 createTabs()메소드를 호출할 당시의 브라우저의 URL 해쉬값에 대해 처리한다.
[F] createTabs()메소드의 호출을 통해 탭메뉴를 생성한다.(class가 tabs인 요소를 createTabs()메소드에 전달)