시그널 폼은 Angular 시그널을 사용해서 폼 상태를 관리하며, 데이터 모델과 UI를 동기화할 때도 Angular 시그널을 활용합니다.
이 문서에서는 시그널 폼을 어떻게 만드는지 핵심을 알아봅시다. 이렇게 구현합니다:
첫번째 폼 만들기
1. 폼 모델 생성하기: signal()
모든 폼은 폼 데이터 모델을 저장할 시그널을 만드는 것부터 시작합니다:
interface LoginData {
email: string;
password: string;
}
const loginModel = signal<LoginData>({
email: '',
password: '',
});
2. form()을 실행해서 폼 모델을 FieldTree로 변환하기
그리고 폼 모델을 form() 함수로 전달해서 이 폼 모델을 필드 트리 로 변환합니다.
필드 트리는 데이터 모델을 표현하는 객체이며, 객체이기 때문에 객체 프로퍼티처럼 접근할 수 있습니다.
최상위 폼 객체와 그 안에 중첩된 프로퍼티는 모두 FieldTree 노드입니다:
const loginForm = form(loginModel);
loginForm; // FieldTree입니다.
loginForm.email; // 이 값도 FieldTree입니다.
3. [formField] 디렉티브로 HTML 입력 필드를 바인딩합니다.
그다음에는 HTML 입력 엘리먼트에 [formField] 디렉티브를 양방향 바인딩해서 폼과 연결합니다:
<input type="email" [formField]="loginForm.email" />
<input type="password" [formField]="loginForm.password" />
그러면 사용자가 입력 엘리먼트의 값을 변경하면 폼에 있는 데이터도 자동으로 갱신됩니다.
참고: [formField] 디렉티브는 required, disabled, readonly와 같은 어트리뷰트 값도 동기화합니다.
4. FieldTree 시그널로 상태 읽기
FieldTree 노드를 함수처럼 실행하면 트리의 어느 부분에서든 상태에 접근할 수 있습니다.
반환된 상태 객체에는 값, 유효성 검사 상태, 상호작용 상태를 표현하는 반응형 시그널이 포함됩니다:
loginForm(); // 폼 전체의 상태를 반환합니다.
loginForm.email(); // 이메일 필드의 상태를 반환합니다.
현재 값을 읽으려면 value() 시그널을 읽으면 됩니다:
<!-- 사용자가 입력한 값을 화면에 렌더링합니다. -->
<p>Form value: {{ loginForm().value() | json }}</p>
<p>Email: {{ loginForm.email().value() }}</p>
// 현재값을 참조합니다.
const currentEmail = loginForm.email().value();
5. 값 변경하기: set()
어떤 노드에서든 value.set() 메서드를 사용해서 코드로 값을 변경할 수 있습니다.
이 메서드를 실행하면 FieldTree와 모델 시그널 값을 모두 변경합니다:
// 코드로 필드 값을 변경합니다.
loginForm.email().value.set('alice@wonderland.com');
그러면 필드 값과 모델 시그널 값이 모두 변경된 것을 확인할 수 있습니다:
// 모델 시그널도 함께 변경됩니다.
console.log(loginModel().email); // 'alice@wonderland.com'
전체 예제
활용 방법
[formField] 디렉티브는 표준 HTML 입력 필드라면 어디에라도 사용할 수 있습니다.
이렇게 사용하면 됩니다:
텍스트 입력
텍스트 입력은 다양한 type과 함께 사용할 수 있습니다:
<!-- 텍스트, 이메일 -->
<input type="text" [formField]="form.name" />
<input type="email" [formField]="form.email" />
숫자
숫자 입력 필드에 바인딩하면 문자열 타입과 숫자 타입을 자동으로 변환합니다:
<!-- 숫자 - 숫자 타입으로 자동 변환합니다. -->
<input type="number" [formField]="form.age" />
날짜, 시각
날짜 입력 필드는 YYYY-MM-DD 문자열 타입으로 값을 저장하며, 시각 입력 필드는 HH:mm 형식으로 값을 저장합니다:
<!-- 날짜, 시각 - ISO 문자열 형식으로 값을 저장합니다. -->
<input type="date" [formField]="form.eventDate" />
<input type="time" [formField]="form.eventTime" />
문자열 타입 날짜를 Date 객체로 변환하려면 Date() 생성자를 활용하면 됩니다:
const dateObject = new Date(form.eventDate().value());
여러 줄 텍스트
문자 입력 필드와 동일합니다:
<!-- Textarea -->
<textarea [formField]="form.message" rows="4"></textarea>
체크박스
체크박스는 불리언 값을 바인딩합니다:
<!-- 단일 체크박스 -->
<label>
<input type="checkbox" [formField]="form.agreeToTerms" />
I agree to the terms
</label>
여러 체크박스
체크박스가 여러 개라면 개별 필드마다 불리언 값을 연결합니다:
<label>
<input type="checkbox" [formField]="form.emailNotifications" />
Email notifications
</label>
<label>
<input type="checkbox" [formField]="form.smsNotifications" />
SMS notifications
</label>
라디오 버튼
라디오 버튼은 체크박스와 비슷하게 동작합니다.
라디오 버튼의 [formField] 값이 같다면, 시그널 폼은 이 라디오 버튼에 같은 name 어트리뷰트 값을 바인딩합니다:
<label>
<input type="radio" value="free" [formField]="form.plan" />
Free
</label>
<label>
<input type="radio" value="premium" [formField]="form.plan" />
Premium
</label>
이렇게 구현하면 사용자가 라디오 버튼을 클릭했을 때 폼의 field가 개별 라디오 버튼의 value 어트리뷰트값을 저장합니다.
그래서 "Premium" 버튼을 선택하면 form.plan().value() 값은 "premium"이 됩니다.
드롭다운 셀렉트
셀렉트 엘리먼트는 정적인 옵션과 동적인 옵션을 모두 사용할 수 있습니다:
<!-- 정적 옵션 -->
<select [formField]="form.country">
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
<!-- @for 를 사용하는 동적 옵션 -->
<select [formField]="form.productId">
<option value="">Select a product</option>
@for (product of products; track product.id) {
<option [value]="product.id">{{ product.name }}</option>
}
</select>
참고: 셀렉트 엘리먼트에 multiple 옵션을 지정하면 [formField] 디렉티브를 사용할 수 없습니다.
유효성 검사, 유효성 검사 상태
시그널 폼은 폼 필드에 사용할 수 있는 유효성 검사 함수를 기본으로 제공합니다.
폼 필드에 유효성 검사 함수를 적용하려면 form() 함수를 실행할 때 두번째 인자로 스키마 함수(schema function)를 전달하면 됩니다:
const loginForm = form(loginModel, (schemaPath) => {
debounce(schemaPath.email, 500);
required(schemaPath.email);
email(schemaPath.email);
});
스키마 함수는 유효성 검사 규칙을 지정하는 스키마 경로(schema path) 를 인자로 받습니다.
이런 함수를 자주 사용합니다:
required()- 필드에 값이 존재해야 합니다.email()- 이메일 형식인지 검사합니다.min()/max()- 입력된 숫자의 범위를 검사합니다.minLength()/maxLength()- 문자열이나 배열의 길이를 검사합니다.pattern()- 정규표현식 형식에 맞는지 검사합니다.
그리고 유효성 검사 함수의 두번째 인자로 옵션 객체를 전달하면서 오류 메시지를 커스터마이징할 수 있습니다:
required(schemaPath.email, {message: 'Email is required'});
email(schemaPath.email, {message: 'Please enter a valid email address'});
FieldTree의 각 노드는 유효성 검사 상태와 상호작용 상태를 반응형 시그널로 표현합니다.
FieldTree 상태 시그널
최상위 폼 객체를 포함한 트리의 모든 노드는 상태를 확인하는 동일한 시그널을 제공합니다.
모든 노드가 FieldTree이므로 어느 단계에서든 같은 API로 유효성 검사 상태와 상호작용 상태를 확인할 수 있습니다.
| 상태 | 설명 |
|---|---|
valid() |
노드가 모든 유효성 검사 규칙을 통과하면 true를 반환합니다. |
invalid() |
유효성 검사 오류가 있으면 true를 반환합니다. |
pending() |
비동기 유효성 검사가 진행 중이면 true를 반환합니다. |
touched() |
사용자가 해당 필드나 자식 필드에 접근했다가 빠져나가면 true를 반환합니다. |
dirty() |
사용자가 값을 변경했으면 true를 반환합니다. |
disabled() |
노드가 비활성화되면 true를 반환합니다. |
readonly() |
노드가 읽기 전용이면 true를 반환합니다. |
errors() |
kind, message 프로퍼티가 있는 유효성 검사 오류 배열을 반환합니다. |
전체 예제
다음 단계
시그널 폼이 동작하는 것을 더 자세하게 알아보려면 이런 내용을 확인해 보세요: