[coursera] AngularJS week 2-2
2016-08-28

Task Runners, Angular Scope, Forms and Form Validation-2

Angular Scope

스코프는 다음과 같이 정의된다.

스코프는 application model 과 관련된 object 다. This is at the core of Angular's two-way data binding view 와 controller 사이를 연결해주는 역할을 한다.

우선 controller 에 scope 를 만들어 속성을 부여한다. 그리고 view 는 controller 에 정의된 속성들을 bind 한다. 이런 방식을 통해 view 와 controller 의 sync 를 맞춰준다.

$rootScope

가장 상위의 scope 다. app 이 시작되면 만들어진다. 새로운 스코프는 ng-controller 와 같은 directives 를 통해 만들수 있다. scope 의 구조는 DOM 의 구조와 비슷하다. 그렇기 때문에 child scope 에서는 parent scope 의 속성에 접근이 가능하지만 그 반대는 불가하다.

app.js

// 기존 코드
angular.module('confusionApp', [])
    .controller('MenuController',function(){
        this.tab = 1;
        this.filtText = '';

        ...

	});

// scope 적용 코드
angular.module('confusionApp', [])
	// scope 정의
    .controller('MenuController', ['$scope',function($scope){
        $scope.tab = 1;
        $scope.filtText = '';

        ...

    )]};

기존의 코드를 보면 this 접근자를 통해서 속성들을 부여하고 있다. 여기서 스코프를 사용하면, this 가 아닌 scope 에 속성들을 부여함으로써 view 단에서 scope 를 통해 각 속성에 접근하게 된다.

<!-- 기존 코드 -->
<div class="container">
    <div class="row row-content" ng-controller="MenuController as menuCtrl">
        <div class="col-xs-12">
            <ul class="nav nav-tabs" role="tablist">
                <li role="presentation" ng-class="{active:menuCtrl.isSelected(1)}">
                    <a ng-click="menuCtrl.select(1)" aria-controls="all menu"
                     role="tab">The Menu</a>
                     ...
                </li>
            </ul>
            <div class="tab-content">
                <ul class="media-list tab-pane fade in active">
                    <li class="media" ng-repeat="dish in menuCtrl.dishes | filter:menuCtrl.filtText">
                    ...
	                </li>
	            </ul>
	        </div>
	    </div>
	</div>
</div>


<!-- scope 적용 코드 -->
<div class="container">
    <div class="row row-content" ng-controller="MenuController">
        <div class="col-xs-12">
            <ul class="nav nav-tabs" role="tablist">
                <li role="presentation" ng-class="{active:isSelected(1)}">
                    <a ng-click="select(1)" aria-controls="all menu"
                     role="tab">The Menu</a>
                     ...
                </li>
            </ul>
            <div class="tab-content">
                <ul class="media-list tab-pane fade in active">
                    <li class="media" ng-repeat="dish in dishes | filter:filtText">
                    ...
	                </li>
	            </ul>
	        </div>
	    </div>
	</div>
</div>

기존코드에서는 menuController 의 alias 인 menuCtrl 을 만들어 각 변수와 함수 앞에 붙여줬었다. 하지만 MenuController 에 스코프가 적용되면서 해당 컨트롤러가 속한 DOM 내부에서는 컨트롤러 이름없이 변수/함수명으로 직접 접근이 가능해진다. 스코프를 적용함으로써 훨씬 코드가 간단해졌다.

ng-show

ng-show directive 는 주어진 조건(true/false)에 따라서 해당 DOM 객체를 보여줄것인지(show) 말것인지(not show)를 결정해준다. 길게 설명할 필요없이 예제를 보자

<div class="col-xs-12">
    <button ng-click="toggleDetails()" class="btn btn-xs btn-primary pull-right" type="button">
        {{showDetails ? 'Hide Details':'Show Details'}}
    </button>
    <ul class="nav nav-tabs" role="tablist">
	    ...
	</ul>
	...
        <p ng-show="showDetails">{{dish.description}}</p>
    ...
</div>

$scope.showDetails 를 app.js 에 설정해놓고 기본값으로 false 를 지정해놓자. 그리고는 button 을 하나 만들어 showDetails 가 true 이면 Hide Details 문구의 버튼을 노출하고 false 면 Show Details 문구의 버튼을 노출한다. 리스트의 요소중에 description 부분에 ng-show directive 를 선언하고 showDetails 를 넣어주자. 그리고 버튼을 클릭하면(showDetails 가 toggle 된다. app.js 에 function 을 만들어둔다.) showDetails 가 true 가 됐다가 false 가 되면서 description 이 보여졌다가 가려졌다가 할 것이다.

Angular Forms and Form Validation

Forms

form 은 유저에게 웹 사이트의 정보를 제공하는 가장 널리 상요되는 방법이다. 이번 강의에서는 angularJS 에서 form 을 이용하는 방법과 form 의 validation 체크하는 방법에 대해서 알아보자.

form 에서는 two-way data binding 이 중요하게 사용되는데, 넘어가기 앞서 잠깐 짚고 넘어가야할게 있다. ng-model 의 개념이 약간 모호해서 한번 찾아봤다.

ng-model

html 의 input, select, textarea 와 같은 입력 요소에 값을 갱신하면 ng-model 해 설정된 변수의 값도 변함 반대로 변수의 값이 바뀌면 input 등의 화면도 동일하게 변경된다. 이를 two-way data binding 이라 한다. 참고로 form 필드에 설정된 ng-model 의 속성을 필드 내에서 부여할 수도 있다.

form 과 자바스크립트 object 간의 연결해주는 역할을 하는 것이 ng-model 이다. ng-model 에 설정된 변수가 변경되면 변경된 것을 html 에 그대로 반영해준다. 예를들어보자.

// javascript code
.controller('ContactController', ['$scope', function($scope){
    $scope.feedback = {mychannel:"", firstname:"",
                       lastname:"", agree:"", email:""};
}]);

// html code
<input type="text" class="form-control" id="firstname"
	name="firstname" placeholder="Enter First Name"
	ng-model="feedback.firstname" required>

위와 같이 angularJS 의 controller 에 feedback 이라는 object 를 만들어두고, html 에서 feedback.firstname 을 ng-model attribute 에 지정해두면, firstname 이 변함에 따라서 자동으로 feedback.firstname 의 값을 변경한다.

select 의 경우에는 어떤가 보자

// javascript code
var channels = [
  { value: 'tel', label: 'Tel.' },
  { value: 'Email', label: 'Email' },
]

// html code
;<select
  class="form-control"
  ng-model="feedback.mychannel"
  ng-options="channel.value as channel.label for channel in channels"
>
  <option value="">Tel. or Email?</option>
</select>

우선 select 의 item 들을 저장해둘 자바스크립트 array 변수 channels 를 만든다. 그리고 select 태그내에 ng-model, ng-options 를 위와 같이 설정해두자. ng-model 의 mychannel 에는 유저가 선택된 channel 의 value 가 들어갈 것이다. ng-options directive 에서 channels 라는 자바스크립트 array 를 for loop 로 분해하여 각각 channel 이라는 object 를 가져온다. 그리고 channel.label 이 option 의 text 로 들어가고, channel.value 가 option 태그의 value 로써 들어가게 된다. 즉, 만들어진 selectbox 의 item 하나를 선택하게 되면, 해당 item 의 value 가 feedback.mychannel 변수에 할당된다.

위의 방법을 이용하면 form 의 다른요소나, 웹페이지의 상태에 따라 select 의 항목을 동적으로 바꿀 수 있게 된다.

Form Validation

우선 HTML5 form validation 을 끄자 그리고 ng-submit directive 를 이용하여 form 이 submit 될 때 sendFeedback 함수를 호출하도록 하자.

<form class="form-horizontal" name="feedbackForm" ng-submit="sendFeedback()" novalidate>

validation 체크할때 field name 을 이용하여 다음의 필드 속성들을 체크할 수 있다.

property Description
$pristine true if form has not been changed (form 의 변경사항이 없다면,)
$dirty reverse of $pristine
$valid true if form field/whole form is valid (form 이 valid 하면,)
$invalid reverse of $valid

예를 들어보자.

  • feedbackForm.firstName.$pristine : firstName 필드의 변경사항이 없으면 true
  • feebackForm.$valid : feedbackForm 의 모든 항목이 valid 하면 true

에러가 있을경우(invalid 하면) bootstrap 의 class 를 이용하자.

.has-error, .has-warning, .has-success .help-block to display helpful messages below the field

app.js

...

	.controller('ContactController', ['$scope', function($scope){
	    $scope.feedback = {mychannel:"", firstname:"",
	                       lastname:"", agree:"", email:""};
	    var channels = [{value:"tel", label:"Tel."},
	                    {values:"Email", label:"Email"}];
	    $scope.channels = channels;
	    $scope.invalidChannelSelection = false;

	}])
	.controller('FeedbackController', ['$scope', function($scope){
	    $scope.sendFeedback = function() {
	        console.log($scope.feedback);

	        if ($scope.feedback.agree && ($scope.feedback.mychannel == "")) {
	            $scope.invalidChannelSelection = true;
	            console.log('incorrent');
	        } else {
	            $scope.invalidChannelSelection = false;
	            $scope.feedback = {
	                mychannel:"", firstname:"",
	                lastname:"", agree:false, email:""
	            };
	            $scope.feedback.mychannel = "";
	            $scope.feedbackForm.$setPristine();
	            console.log($scope.feedback);
	        }
	    };
	}]);

...

contactus.html

<!DOCTYPE html>
<html lang="en" ng-app="confusionApp">

...

<body>
    <div class="container" ng-controller="ContactController">
    	...
                <form class="form-horizontal" role="form" name="feedbackForm" ng-submit="sendFeedback()" novalidate>
                    <div class="form-group" ng-class="{ 'has-error' : feedbackForm.firstname.$error.required && !feedbackForm.firstname.$pristine }">
                        <label for="firstname" class="col-sm-2 control-label">First Name</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" id="firstname" name="firstname" placeholder="Enter First Name" ng-model="feedback.firstname" required>
                            <span ng-show="feedback.firstname.$error.required && !feedbackForm.firstname.$pristine" class="help-block">Your First name is required</span>
                        </div>
                    </div>
                    <div class="form-group" ng-class="{ 'has-error' : feedbackForm.lastname.$error.required && !feedbackForm.lastname.$pristine }">
                        <label for="lastname" class="col-sm-2 control-label">Last Name</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" id="lastname" name="lastname" placeholder="Enter Last Name" ng-model="feedback.lastname" required>
                            <span ng-show="feedback.lastname.$error.required && !feedbackForm.lastname.$pristine" class="help-block">Your Last name is required</span>
                        </div>
                    </div>
                    <div class="form-group">
                        <label for="telnum" class="col-sm-2 control-label">Contact Tel.</label>
                        <div class="col-xs-5 col-sm-4 col-md-3">
                            <div class="input-group">
                                <div class="input-group-addon">(</div>
                                    <input type="tel" class="form-control" id="areacode" name="areacode" placeholder="Area code" ng-model="feedback.tel.areacode">
                                <div class="input-group-addon">)</div>
                            </div>
                        </div>
                        <div class="col-xs-7 col-sm-6 col-md-7">
                                    <input type="tel" class="form-control" id="telnum" name="telnum" placeholder="Tel. number" ng-model="feedback.tel.number">
                        </div>
                    </div>
                    <div class="form-group" ng-class="{ 'has-error' : feedbackForm.emailid.$invalid && !feedbackForm.emailid.$pristine }">
                        <label for="emailid" class="col-sm-2 control-label">Email</label>
                        <div class="col-sm-10">
                            <input type="email" class="form-control" id="emailid" name="emailid" placeholder="Email" ng-model="feedback.email" required>
                            <span ng-show="feedbackForm.emailid.$invalid && !feedbackForm.emailid.$pristine" class="glyphicon glyphicon-remove form-control-feedback" aria-hidden="true"></span>
                            <span ng-show="feedbackForm.emailid.$invalid && !feedbackForm.emailid.$pristine" class="help-block">Enter a valid email address.</span>
                            <span ng-show="feedbackForm.emailid.$error.required && !feedbackForm.emailid.$pristine" class="help-block">Enter a valid email address.</span>
                        </div>
                    </div>
                    <div class="form-group" ng-class="{ 'has-error' : invalidChannelSelection }">
                       <div class="checkbox col-sm-5 col-sm-offset-2">
                            <label class="checkbox-inline">
                                <input type="checkbox" name="approve" value="" ng-model="feedback.agree">
                                <strong>May we contact you?</strong>
                            </label>
                        </div>
                        <div class="col-sm-3 col-sm-offset-1" ng-show="feedback.agree">
                            <select class="form-control" ng-model="feedback.mychannel" ng-options="channel.value as channel.label for channel in channels">
                                <option value="">Tel. or Email?</option>
                            </select>
                            <span ng-show="invalidChannelSelection" class="help-block">Select an option.</span>
                        </div>
                    </div>
                    <div class="form-group">
                        <label for="feedback" class="col-sm-2 control-label">Your Feedback</label>
                        <div class="col-sm-10">
                            <textarea class="form-control" id="feedback" name="feedback" rows="12" ng-model="feedback.comments"></textarea>
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="col-sm-offset-2 col-sm-10">
                            <button type="submit" class="btn btn-primary" ng-disabled="feedbackForm.$invalid">Send Feedback</button>
                        </div>
                    </div>
                </form>
            </div>
            <div class="col-xs-12 col-sm-3">
                <h3>Your Current Feedback:</h3>
                <p>{{feedback.firstname}} {{feedback.lastname | uppercase }}</p>
                <p>Contact Tel.: ({{feedback.tel.areacode}}){{feedback.tel.number}}</p>
                <p>Contact Email: {{feedback.email}}</p>
                <p ng-show="feedback.agree">Contact by:{{feedback.mychannel}}</p>
                <p>Comments: {{feedback.comments}}</p>
            </div>
       </div>
    </div>
</body></html>