TaeHee Kim
Software Engineer / Bassist
using angular.js
ITWISE Consulting
Engineering center
선임 엔지니어 김태희
/*
* 어디는 이렇게 Server Side로 제목을 렌더링
* ex) EL Exp
*/
<h3>${title}</h3>
/*
* 어디는 이렇게 처리하고...
* ex) scripting
*/
<h3 class="title"></h3>
// scripting
$('.title').text('title text');
<c:forEach items="${articles}" var="article">
<article>
<div class="article-header">${article.header}</div>
<div class="article-content">${article.content}</div>
<!-- 엘리먼트 노출 여부를 Server Side Rendering에서 결정 -->
<c:if test="${article.commentCount > 0}">
<button class="comment-load-button">댓글 불러오기</button>
</c:if>
<!-- ajax 로딩 후 아래 영역에 html append -->
<div class="article-comments"></div>
</article>
</c:forEach>
'추가' 버튼 누르면 할 일이 추가
해야 할 일의 해당 항목을 클릭하면 완료된 일로 이동
해야 할 일의 삭제 버튼을 누르면 해당 일 삭제
할 일 갯수의 변화가 있을 때 마다 맨 아래 할 일 갯수 영역이 갱신되어야 한다.
현재는 localStorage 사용, 추후 RESTful API 연동 처리
<!DOCTYPE html>
<html>
<head>
<title>Sample Todo App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container">
<header class="todo-header">
<h3>Sample Todo App</h3>
</header>
<section class="panel">
<h3>할 일 추가하기</h3>
<form class="new-todo">
<input type="text" id="todo-title" placeholder="할 일을 입력하세요!">
<button class="btn btn-add pull-right">추가</button>
</form>
</section>
<section class="panel">
<h3>해야 할 일</h3>
<ul class="todos-inprogress">
<!-- 반복되는 부분 -->
<li class="panel todo">
<label><input type="checkbox">http 공부하기</label>
<button class="btn btn-remove pull-right">삭제</button>
</li>
<li class="panel todo">
<label><input type="checkbox">ionic 정리</label>
<button class="btn btn-remove pull-right">삭제</button>
</li>
</ul>
</section>
<section class="panel">
<h3>완료한 일<button class="btn btn-default inline pull-right todo-complete-clear-button">비우기</button></h3>
<ul class="todos todos-completed">
<!-- 반복되는 부분 -->
<li class="panel todo">발표자료 준비</li>
</ul>
</section>
<aside class="panel todo-info">
<strong class="todo-total-count">3</strong>개 중 <strong class="todo-complete-count">2</strong>개를 완료했습니다.
</aside>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="script.js"></script>
</body>
</html>
'use strict';
(function($){
var todos = [];
// localStorage에서 저장된 todos 로딩
if(window.localStorage && window.localStorage.todos !== undefined){
todos = JSON.parse(window.localStorage.todos);
console.log(localStorage.todos);
console.log(todos.length)
}
function sync(){
window.localStorage.todos = JSON.stringify(todos);
console.log(localStorage.todos);
}
function updateTodoInfo(){
var todoCompleteCount = 0;
for(var i = 0; i < todos.length; i++){
if(todos[i].isComplete){
todoCompleteCount = todoCompleteCount + 1;
}
}
// dom 접근해서 값 변경
$('.todo-complete-count').text(todoCompleteCount);
$('.todo-total-count').text(todos.length);
}
$(document).ready(function(){
// todos render
for(var i = 0; i < todos.length; i++){
if(todos[i].isComplete){
$('.todos-completed').append(
'<li class="panel todo" id="todo-' + todos[i].id + '">' +
todos[i].title +
'</li>'
);
}else{
$('.todos-inprogress').append(
'<li class="panel todo" id="todo-' + todos[i].id + '">' +
'<label>' +
'<input type="checkbox" class="todo-complete">' + todos[i].title +
'</label>' +
'<button class="btn btn-remove pull-right">삭제</button>' +
'</li>'
);
}
}
updateTodoInfo();
// add event binding
$('.new-todo').on('submit', function(event){
var $todoTitle = $('#todo-title');
var todoTitle = $todoTitle.val();
var todo = {
title: todoTitle,
id: new Date().getTime(),
isComplete: false
};
$('.todos-inprogress').append(
'<li class="panel todo" id="todo-' + todo.id + '">' +
'<label>' +
'<input type="checkbox" class="todo-complete">' + todo.title +
'</label>' +
'<button class="btn btn-remove pull-right">삭제</button>' +
'</li>'
);
$todoTitle.val('');
todos.push(todo);
sync();
updateTodoInfo();
event.preventDefault();
});
// todo complete event
$('.todos-inprogress').on('click', '.todo-complete', function(){
var $completeTodo = $(this).parents('.todo');
var todoId = $completeTodo.attr('id').replace('todo-', '');
var todo, i;
// 완료 처리
for(i = 0; i < todos.length; i++){
todo = todos[i];
if(todo.id === parseInt(todoId)){
todo.isComplete = true;
break;
}
}
$completeTodo.find('input[type=checkbox]').remove();
$completeTodo.find('.btn-remove').remove();
$('.todos-completed').append($completeTodo);
sync();
updateTodoInfo();
});
// remove event binding
$('.todos-inprogress').on('click', '.btn-remove', function(){
var $deleteTodo = $(this).parents('.todo');
var todoId = $deleteTodo.attr('id').replace('todo-', '');
$(this).parents('.todo').remove();
for(i = 0; i < todos.length; i++){
todo = todos[i];
if(todo.id === parseInt(todoId)){
todos.splice(i, 1);
break;
}
}
sync();
updateTodoInfo();
});
$('.todo').on('click', '.btn-remove', function(){
var $removeTargetTodo = $(this).parents('.todo');
todos.splice($removeTargetTodo.index(), 1);
$removeTargetTodo.remove();
sync();
updateTodoInfo();
});
// completed remove all event
$('.todo-complete-clear-button').on('click', function(){
var inprogressTodos = [], i;
for(i = 0; i < todos.length; i++){
if(!todos[i].isComplete){
inprogressTodos.push(todos[i]);
}
}
todos = inprogressTodos;
// html 비우기
$('.todos-completed').html('');
sync();
updateTodoInfo();
});
});
})(jQuery);
JAVASCRIPT에 HTML markup이 하드코딩 되어있음
'와 " 처리
markup을 고쳐야 하면 JAVASCRIPT 코드를 고쳐야 함
화면 조작에 따라 매번 DOM을 세세하게 조작해야 함
public class HelloWorldExample extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// Set the response message's MIME type.
response.setContentType("text/html;charset=UTF-8");
// Allocate a output writer to write the response message into the network socket.
PrintWriter out = response.getWriter();
// Write the response message, in an HTML document.
try {
out.println("<!DOCTYPE html>"); // HTML 5
out.println("<html><head>");
out.println("<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>");
String title = "helloworld.title";
out.println("<title>" + title + "</title></head>");
out.println("<body>");
out.println("<h1>" + title + "</h1>"); // Prints "Hello, world!"
out.println("<a href='" + request.getRequestURI() + "'><img src='images/return.gif'></a>");
out.println("</body></html>");
} finally {
out.close(); // Always close the output writer
}
}
}
<!-- templates -->
<!-- todo complete template -->
<script type="text/x-custom-template" id="todo-complete">
<li class="panel todo" id="todo-{id}">
✓ {title}
</li>
</script>
<!-- todo inprogress template -->
<script type="text/x-custom-template" id="todo-inprogress">
<li class="panel todo" id="todo-{id}">
<label>
<input type="checkbox" class="todo-complete">{title}
</label>
<button class="btn btn-remove pull-right">삭제</button>
</li>
</script>
<!-- todo info template -->
<script type="text/x-custom-template" id="todo-info">
<strong class="todo-total-count">{totalCount}</strong>개 중
<strong class="todo-complete-count">{completeCount}</strong>개를 완료했습니다.
</script>
var templateUtil = {
mapper: {},
init: function(){
// script element 중 type이 text/x-custom-template인 것을 template으로 등록
var that = this;
$('script').each(function(){
if($(this).attr('type') === 'text/x-custom-template'){
var id = $(this).attr('id');
that.mapper[id] = $(this).text();
}
});
},
render: function(id, data){
// 정규표현식을 이용해 {key} 형태의 string을 value로 치환
var template = this.mapper[id];
if(template !== undefined){
for(var key in data){
var binding = new RegExp('{' + key +'}', 'g');
template = template.replace(binding, data[key]);
}
return template;
}else{
throw new Error(id + ' template은 존재하지 않습니다.');
}
}
};
render 구간 분리
if(window.localStorage && window.localStorage.todos !== undefined){
todos = JSON.parse(window.localStorage.todos);
}
function sync(){
window.localStorage.todos = JSON.stringify(todos);
}
function render(){
var i;
var todoCompleteCount = 0;
// init html
$('.todos-completed').html('');
$('.todos-inprogress').html('');
// todos render
for(i = 0; i < todos.length; i++){
if(todos[i].isComplete){
todoCompleteCount = todoCompleteCount + 1;
$('.todos-completed').append(
templateUtil.render('todo-complete', todos[i])
);
}else{
$('.todos-inprogress').append(
templateUtil.render('todo-inprogress', todos[i])
);
}
}
$('.todo-info').html(templateUtil.render('todo-info', {
totalCount: todos.length,
completeCount: todoCompleteCount
}));
}
function syncAndRender(){
sync();
render();
}
$(document).ready(function(){
// template parsing
templateUtil.init();
render();
// add event binding
$('.new-todo').on('submit', function(event){
var $todoTitle = $('#todo-title');
var todoTitle = $todoTitle.val();
var todo = {
title: todoTitle,
id: new Date().getTime(),
isComplete: false
};
$todoTitle.val('');
todos.push(todo);
syncAndRender();
event.preventDefault();
});
// todo complete event
$('.todos-inprogress').on('click', '.todo-complete', function(){
var $completeTodo = $(this).parents('.todo');
var todoId = $completeTodo.attr('id').replace('todo-', '');
var todo, i;
// 완료 처리
for(i = 0; i < todos.length; i++){
todo = todos[i];
if(todo.id === parseInt(todoId)){
todo.isComplete = true;
break;
}
}
syncAndRender();
});
// remove event binding
$('.todos-inprogress').on('click', '.btn-remove', function(){
var $deleteTodo = $(this).parents('.todo');
var todoId = $deleteTodo.attr('id').replace('todo-', '');
var todo, i;
for(i = 0; i < todos.length; i++){
todo = todos[i];
if(todo.id === parseInt(todoId)){
todos.splice(i, 1);
break;
}
}
syncAndRender();
});
});
...and many more...
var todos = JSON.parse(window.localStorage.todos);
var localStorage = window.localStorage;
// 데이터를 추가해도
todos.push(todo);
localStorage.todos = JSON.stringify(todos);
// 데이터를 삭제해도
todos.splice(todos, 1);
localStorage.todos = JSON.stringify(todos);
// 데이터를 수정해도
todos[3].title = '변경';
// 새로고침하면 수정한 데이터 날라감!
...and many more...
<!DOCTYPE html>
<html data-ng-app="todoApp">
<head>
<title>Sample Todo App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container" data-ng-controller="todoController" data-ng-cloak>
<header class="todo-header">
<h3>Sample Todo App</h3>
</header>
<div class="panel">
<h3>할 일 추가하기</h3>
<form data-ng-submit="add($event)">
<input type="text" id="todo-title" placeholder="할 일을 입력하세요!"
data-ng-model="todoTitle"/>
<button class="btn btn-add pull-right">추가</button>
</form>
</div>
<div class="panel">
<h3>해야 할 일</h3>
<ul class="todos todos-inprogress">
<li class="panel todo"
data-ng-repeat="todo in todos"
data-ng-if="!todo.isComplete">
<label>
<input type="checkbox" data-ng-click="complete(todo)">{{todo.title}}
</label>
<button class="btn btn-remove pull-right"
data-ng-click="remove($index)">삭제</button>
</li>
</ul>
</div>
<div class="panel">
<h3>완료한 일<button class="btn" data-ng-click="removeCompleteAll()">비우기</button></h3>
<ul class="todos todos-completed">
<li class="panel todo"
data-ng-repeat="todo in todos"
data-ng-if="todo.isComplete">
{{todo.title}}
</li>
</ul>
</div>
<div class="panel todo-info">
<strong class="todo-total-count">{{todoTotalCount}}</strong>개 중
<strong class="todo-complete-count">{{todoCompleteCount}}</strong>개를 완료했습니다.
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.7/angular.js"></script>
<script src="script.js"></script>
</body>
</html>
'use strict';
var todoApp = angular.module('todoApp', []);
todoApp.controller('todoController', function($scope){
$scope.todos = [];
if(window.localStorage && window.localStorage.todos !== undefined){
$scope.todos = JSON.parse(window.localStorage.todos);
}
$scope.todoTotalCount = $scope.todos.length;
$scope.todoCompleteCount = 0;
// todos 데이터 변경을 감지하여 총 갯수 알아내고 localStorage에 갱신시키기
$scope.$watch('todos', function(){
$scope.todoTotalCount = $scope.todos.length;
var todoCompleteCount = 0;
var i;
for(i = 0; i < $scope.todos.length; i++){
if($scope.todos[i].isComplete){
todoCompleteCount = todoCompleteCount + 1;
}
}
$scope.todoCompleteCount = todoCompleteCount;
window.localStorage.todos = angular.toJson($scope.todos);
}, true);
$scope.todoTitle = '';
// todo functions
$scope.add = function(){
$scope.todos.push({
id: new Date().getTime(),
title: $scope.todoTitle
});
$scope.todoTitle = '';
};
$scope.complete = function(todo){
todo.isComplete = true;
};
$scope.remove = function($index){
$scope.todos.splice($index, 1);
};
$scope.removeCompleteAll = function(){
var inprogressTodos = [];
for(var i = 0; i < $scope.todos.length; i++){
if(!$scope.todos[i].isComplete){
inprogressTodos.push($scope.todos[i]);
}
}
$scope.todos = inprogressTodos;
};
});
with service & dependency injection
'use strict';
var todoApp = angular.module('todoApp', []);
// todoStore service 정의
todoApp.service('todoStore', function(){
return {
save: function(todos){
window.localStorage.todos = JSON.stringify(todos);
},
findAll: function(){
if(window.localStorage && window.localStorage.todos !== undefined){
return JSON.parse(window.localStorage.todos);
}else{
return [];
}
}
};
});
// 위에서 정의한 todoStore를 주입받는다.
// 이걸로 직접 localStorage에 접근하는 코드를 제거
todoApp.controller('todoController', function($scope, todoStore){
$scope.todos = todoStore.findAll();
$scope.todoTotalCount = $scope.todos.length;
$scope.todoCompleteCount = 0;
// todos 데이터 변경을 감지하여 총 갯수 알아내고 localStorage에 갱신시키기
$scope.$watch('todos', function(){
$scope.todoTotalCount = $scope.todos.length;
var todoCompleteCount = 0;
var i;
for(i = 0; i < $scope.todos.length; i++){
if($scope.todos[i].isComplete){
todoCompleteCount = todoCompleteCount + 1;
}
}
$scope.todoCompleteCount = todoCompleteCount;
todoStore.save(angular.toJson($scope.todos));
}, true);
$scope.todoTitle = '';
// todo functions
$scope.add = function(){
$scope.todos.push({
id: new Date().getTime(),
title: $scope.todoTitle
});
$scope.todoTitle = '';
};
$scope.complete = function(todo){
todo.isComplete = true;
};
$scope.remove = function($index){
$scope.todos.splice($index, 1);
};
$scope.removeCompleteAll = function(){
var inprogressTodos = [];
for(var i = 0; i < $scope.todos.length; i++){
if(!$scope.todos[i].isComplete){
inprogressTodos.push($scope.todos[i]);
}
}
$scope.todos = inprogressTodos;
};
});
By TaeHee Kim
client side rendering using angular