- Angular
- TypeScript
- 技術 💻
Angular Signal Forms入門|v22安定版のAPI早見表と完成コード
Angular Signal Forms とは、フォームの値をシグナルで持ち、検証ルールを関数で宣言する Angular の新しいフォーム API で、Angular 22.0 で安定版になりました。 form(モデルのシグナル, スキーマ) でフォームを作り、テンプレートでは [formField] で入力欄とつなぐだけです。値・エラー・送信中などの状態はすべてシグナルなので、OnPush が既定になった v22 と自然になじみます。
この記事では、Angular 22.2.0 の型定義と公式ドキュメントで API を確認し、ログインフォームと会員登録フォーム(必須・メール形式・パスワード一致・カスタム検証・非同期の重複チェック・配列フィールド)をコピペで動く完成コード で紹介します。Playwright(Chromium)で入力 → エラー表示 → 送信まで操作した結果 も載せ、同じ会員登録フォームを Reactive Forms で書いた版との対応表、v21 実験版からの変更点 もまとめました。
Signal Forms API早見表(v22.2時点)
| やりたいこと | API | 書き方の例 |
|---|---|---|
| フォームを作る | form(model, schema?, options?) |
f = form(signal({ email: '' }), p => { ... }) |
| 入力欄とつなぐ | FormField ディレクティブ |
<input [formField]="f.email" /> |
<form> の送信を任せる |
FormRoot ディレクティブ |
<form [formRoot]="f"> |
| 必須 | required(path, opts?) |
required(p.email, { message: '必須です' }) |
| メール形式 | email(path, opts?) |
email(p.email) |
| 文字数・数値 | minLength / maxLength / min / max |
minLength(p.password, 8) |
| 日付の範囲 | minDate / maxDate(v22 追加) |
maxDate(p.birthDate, new Date()) |
| 正規表現 | pattern(path, regex) |
pattern(p.password, /[0-9]/) |
| カスタム検証 | validate(path, fn) |
validate(p.confirm, ({ value, valueOf }) => ...) |
| 非同期検証 | validateAsync(path, { params, factory, onSuccess, onError }) |
重複チェックなど |
| HTTP で非同期検証 | validateHttp(path, { request, onSuccess, onError }) |
request: ({ value }) => '/api/check?x=' + value() |
| 条件つきルール | 各ルールの when オプション / applyWhen |
required(p.x, { when: ({ valueOf }) => valueOf(p.y) }) |
| 配列の各要素にルール | applyEach(path, schema) |
applyEach(p.skills, item => required(item)) |
| 無効化・読み取り専用・非表示 | disabled / readonly / hidden |
disabled(p.name, { when: ... }) |
| 反映タイミングを遅らせる | debounce(path, ms | 'blur') |
debounce(p.password, 'blur') |
| 送信処理 | options.submission / submit(f, action) |
{ submission: { action: async f => ... } } |
| 値を読む・書く | f.email().value() / .value.set() |
モデルのシグナルも同時に変わる |
| 状態を読む | valid() invalid() pending() touched() dirty() submitting() |
すべてシグナル |
| エラーを読む | errors() / errorSummary() / getError(kind)(v22 追加) |
f.email().getError('required') |
| 非同期検証をやり直す | reloadValidation()(v22 追加) |
サーバー側の状態が変わったとき |
| 初期状態に戻す | reset(value?) |
f().reset({ email: '' }) |
import 元は @angular/forms/signals です(従来の Reactive Forms は @angular/forms)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular Signal Forms入門|v22安定版のAPI早見表と完成コード」
検証環境
Angular 22.2.0 / TypeScript 6.0.3 / Node.js 22.22.3 / Vitest 5.0.2 / Playwright 1.56.1(Chromium) で確認しました。型定義(@angular/forms/types/signals.d.ts)では、上の表の API に @publicApi 22.0 が付いています。v21 の型定義では同じ API が @experimental 21.0.0 などでした。
v21(実験版)から v22(安定版)への変更点
v21.0.0・v21.2.24・v22.2.0 の @angular/forms の型定義を比べ、公式ブログ・リリース解説と照らし合わせた結果です。
| 項目 | v21.0(実験版) | v21.2(実験版) | v22(安定版) |
|---|---|---|---|
| 状態 | @experimental |
@experimental |
@publicApi 22.0(安定版) |
| 入力欄とつなぐディレクティブ | Field / [field] |
FormField / [formField] |
FormField / [formField] |
<form> 用ディレクティブ |
なし | FormRoot / [formRoot] |
FormRoot / [formRoot] |
form() の送信オプション |
なし(submit() を呼ぶ) |
submission |
submission(action / onInvalid) |
| カスタムエラーの作り方 | customError() |
オブジェクト { kind, message } |
オブジェクト { kind, message } |
| 日付の検証 | なし | なし | minDate() / maxDate() |
| 特定のエラーを取り出す | なし | なし | getError(kind) |
| 非同期検証の再実行 | なし | なし | reloadValidation() |
| 非同期検証のデバウンス | なし | なし | validateAsync / validateHttp の debounce |
when オプション |
一部のみ | 一部のみ | すべての検証・disabled / readonly / hidden に統一 |
markAsTouched() |
そのフィールドのみ | そのフィールドのみ | 子孫も含む({ skipDescendants: true } で従来どおり) |
| カスタムコントロールの touched | touched を model で双方向 |
同左 | touched 入力+touch() 出力 |
hidden() |
when 必須 |
同左 | 22.2 で when なしで常に非表示 にできる |
v21.0 の記事やサンプルを見るときは、[field] が [formField] に、customError() がただのオブジェクトに変わっている点に注意 すると、そのまま v22 に読み替えられます。
ログインフォーム(完成コード)
メールとパスワードの必須・形式チェック、送信中表示、サーバーエラーの表示、エラー時のフォーカス移動まで入れた完成形です。
// src/app/forms/login-form.ts
import { Component, signal } from '@angular/core';
import { FormField, FormRoot, email, form, minLength, required } from '@angular/forms/signals';
interface LoginModel {
email: string;
password: string;
}
// 疑似API:demo@example.com / password123 だけ成功
function loginApi(v: LoginModel): Promise<boolean> {
return new Promise((r) => setTimeout(() => r(v.email === 'demo@example.com' && v.password === 'password123'), 300));
}
@Component({
selector: 'app-login-form',
imports: [FormField, FormRoot],
template: `
<form id="login" [formRoot]="loginForm">
<label>メール <input id="login-email" type="email" [formField]="loginForm.email" /></label>
@if (loginForm.email().touched() && loginForm.email().invalid()) {
@for (e of loginForm.email().errors(); track e.kind) {
<p class="err err-email">{{ e.message }}</p>
}
}
<label>パスワード <input id="login-password" type="password" [formField]="loginForm.password" /></label>
@if (loginForm.password().touched() && loginForm.password().invalid()) {
@for (e of loginForm.password().errors(); track e.kind) {
<p class="err err-password">{{ e.message }}</p>
}
}
@for (e of loginForm().errors(); track e.kind) {
<p class="err err-form">{{ e.message }}</p>
}
<button id="login-submit" type="submit" [disabled]="loginForm().submitting()">
{{ loginForm().submitting() ? '送信中…' : 'ログイン' }}
</button>
<p id="login-result">{{ result() }}</p>
</form>
`,
})
export class LoginForm {
protected readonly result = signal('');
// 1. モデル(フォームの値そのもの)はただのシグナル
private readonly model = signal<LoginModel>({ email: '', password: '' });
// 2. form(モデル, スキーマ, オプション)
protected readonly loginForm = form(
this.model,
(p) => {
required(p.email, { message: 'メールを入力してください' });
email(p.email, { message: 'メールの形式で入力してください' });
required(p.password, { message: 'パスワードを入力してください' });
minLength(p.password, 8, { message: 'パスワードは8文字以上です' });
},
{
submission: {
// 検証がすべて通ったときだけ呼ばれる
action: async (f) => {
const ok = await loginApi(f().value());
if (ok) {
this.result.set(`ようこそ ${f.email().value()} さん`);
return;
}
// 返したエラーはフォーム(ルート)のエラーになる
return { kind: 'loginFailed', message: 'メールまたはパスワードが違います' };
},
onInvalid: (f) => {
this.result.set('入力内容を確認してください');
f().errorSummary()[0]?.fieldTree().focusBoundControl();
},
},
},
);
}
実行結果(Playwright)
| 操作 | 画面に出たエラー・結果 |
|---|---|
| 何も入れずに「ログイン」 | メールを入力してください / パスワードを入力してください、結果欄 入力内容を確認してください、フォーカスはメール欄へ |
demo / short で送信 |
メールの形式で入力してください / パスワードは8文字以上です |
| 正しい形式・誤ったパスワードで送信(直後) | ボタンが 送信中… になり disabled |
| 〃(0.5秒後) | メールまたはパスワードが違います |
| パスワードを入れ直す | フォームのエラーが消える |
demo@example.com / password123 で送信 |
エラーなし、ようこそ demo@example.com さん |
DOM も確認したところ、[formRoot] を付けた <form> には novalidate が、required() を付けた入力欄には required 属性が自動で付き、入力欄の name 属性は ng.form0.email になっていました。
[formRoot] は送信時のページ遷移を止めて submission.action を呼び、検証エラーがあれば代わりに onInvalid を呼びます。 送信すると全フィールドが touched になるので、「touched のときだけエラー表示」にすると入力前は静かで、送信時に一斉に出る自然な動きになります。action から返したエラーはフォーム全体のエラーになり、fieldTree: f.email を付ければ特定の項目のエラーにできます。
会員登録フォーム(完成コード)
必須・メール形式・非同期の重複チェック・パスワードの強さ(カスタム検証)・パスワード一致(項目をまたぐ検証)・配列フィールド・同意チェックを入れた完成形です。
// src/app/forms/register-form.ts
import { Component, Signal, resource, signal } from '@angular/core';
import {
FormField, FormRoot, applyEach, debounce, email, form, maxLength,
minLength, pattern, required, validate, validateAsync,
} from '@angular/forms/signals';
interface RegisterModel {
name: string;
email: string;
password: string;
confirm: string;
skills: string[];
agree: boolean;
}
// 疑似API:登録済みメールかどうかを 500ms 後に返す
const TAKEN = ['taken@example.com', 'admin@example.com'];
function isEmailTaken(mail: string): Promise<boolean> {
return new Promise((r) => setTimeout(() => r(TAKEN.includes(mail.toLowerCase())), 500));
}
@Component({
selector: 'app-register-form',
imports: [FormField, FormRoot],
template: `
<form id="register" [formRoot]="f">
<label>お名前 <input id="reg-name" [formField]="f.name" /></label>
@if (f.name().touched()) { @for (e of f.name().errors(); track e.kind) { <p class="err err-name">{{ e.message }}</p> } }
<label>メール <input id="reg-email" type="email" [formField]="f.email" /></label>
@if (f.email().pending()) { <p class="pending">確認中…</p> }
@if (f.email().touched()) { @for (e of f.email().errors(); track e.kind) { <p class="err err-email">{{ e.message }}</p> } }
<label>パスワード <input id="reg-password" type="password" [formField]="f.password" /></label>
@if (f.password().touched()) { @for (e of f.password().errors(); track e.kind) { <p class="err err-password">{{ e.message }}</p> } }
<label>確認用 <input id="reg-confirm" type="password" [formField]="f.confirm" /></label>
@if (f.confirm().touched()) { @for (e of f.confirm().errors(); track e.kind) { <p class="err err-confirm">{{ e.message }}</p> } }
<fieldset>
<legend>得意なこと(1〜3件)</legend>
@for (s of f.skills; track $index) {
<div>
<input class="reg-skill" [formField]="s" />
<button type="button" class="skill-remove" (click)="removeSkill($index)">削除</button>
@if (s().touched()) { @for (e of s().errors(); track e.kind) { <span class="err err-skill">{{ e.message }}</span> } }
</div>
}
<button type="button" id="skill-add" (click)="addSkill()">追加</button>
@for (e of f.skills().errors(); track e.kind) { <p class="err err-skills">{{ e.message }}</p> }
</fieldset>
<label><input id="reg-agree" type="checkbox" [formField]="f.agree" /> 利用規約に同意する</label>
@if (f.agree().touched()) { @for (e of f.agree().errors(); track e.kind) { <p class="err err-agree">{{ e.message }}</p> } }
<button id="reg-submit" type="submit">登録</button>
<p id="reg-status">valid={{ f().valid() }} pending={{ f().pending() }}</p>
<pre id="reg-result">{{ result() }}</pre>
</form>
`,
})
export class RegisterForm {
protected readonly result = signal('');
private readonly model = signal<RegisterModel>({
name: '', email: '', password: '', confirm: '', skills: [''], agree: false,
});
protected readonly f = form(
this.model,
(p) => {
required(p.name, { message: 'お名前を入力してください' });
maxLength(p.name, 20, { message: 'お名前は20文字以内です' });
required(p.email, { message: 'メールを入力してください' });
email(p.email, { message: 'メールの形式で入力してください' });
// 非同期検証:同期検証がすべて通ってから実行される
validateAsync(p.email, {
params: ({ value }) => value(),
factory: (mail: Signal<string | undefined>) =>
resource({
params: () => mail(),
loader: ({ params }) => isEmailTaken(params),
}),
debounce: 300,
onSuccess: (taken) => (taken ? { kind: 'emailTaken', message: 'このメールは登録済みです' } : null),
onError: () => ({ kind: 'checkFailed', message: '確認できませんでした。時間をおいて再度お試しください' }),
});
required(p.password, { message: 'パスワードを入力してください' });
minLength(p.password, 8, { message: '8文字以上で入力してください' });
pattern(p.password, /[0-9]/, { message: '数字を1文字以上含めてください' });
debounce(p.password, 'blur'); // 入力中ではなく、フォーカスが外れたときにモデルへ反映
// カスタム検証(項目をまたぐ):確認用がパスワードと一致するか
validate(p.confirm, ({ value, valueOf }) =>
value() === valueOf(p.password) ? null : { kind: 'mismatch', message: 'パスワードが一致しません' },
);
// 配列:各要素に必須、配列全体に件数制限
applyEach(p.skills, (item) => {
required(item, { message: '入力するか削除してください' });
});
validate(p.skills, ({ value }) =>
value().length > 3 ? { kind: 'tooMany', message: '3件までです' } : null,
);
required(p.agree, { message: '同意が必要です' });
},
{
submission: {
action: async (tree) => {
this.result.set(JSON.stringify(tree().value(), null, 1));
},
onInvalid: (tree) => {
this.result.set('エラー: ' + tree().errorSummary().map((e) => e.kind).join(', '));
},
},
},
);
addSkill() {
this.model.update((m) => ({ ...m, skills: [...m.skills, ''] }));
}
removeSkill(i: number) {
this.model.update((m) => ({ ...m, skills: m.skills.filter((_, idx) => idx !== i) }));
}
}
実行結果(Playwright)
| 操作 | 画面の結果 |
|---|---|
| 空のまま「登録」 | お名前を入力してください / メールを入力してください / パスワードを入力してください / 入力するか削除してください / 同意が必要です、結果欄 エラー: required, required, required, required, required |
メールに taken@example.com を入力(0.1秒後) |
確認中… を表示、valid=false pending=true |
| 〃(1秒後) | このメールは登録済みです、valid=false pending=false |
メールを new@example.com に変更 |
メールのエラーが消える |
パスワード abcdefgh → フォーカスを外す |
数字を1文字以上含めてください |
パスワード abcdefg1、確認用 abcdefg2 |
パスワードはエラーなし、確認用に パスワードが一致しません |
確認用を abcdefg1 に |
エラーが消える |
| 「追加」を3回(4件に) | 3件までです |
| 1件削除し、3件すべて入力 | 配列のエラーが消える |
| 同意にチェック | valid=true pending=false |
| 「登録」 | 下の JSON を表示 |
{
"name": "あおい",
"email": "new@example.com",
"password": "abcdefg1",
"confirm": "abcdefg1",
"skills": [
"設計",
"テスト",
"レビュー"
],
"agree": true
}
コードの読みどころ
1. 非同期検証は「同期検証が通ってから」動く
validateAsync は required と email が通ってから実行されるので、空欄や形式エラーの間は問い合わせが発生しません。実行中は pending() が true、valid() も invalid() も false です。既定では実行中の検証は送信を妨げないため、完了まで待たせたいときは submission に ignoreValidators: 'none' を指定します。
HTTP で問い合わせるだけなら、request(URLを返す関数)・onSuccess・onError を渡す validateHttp でより短く書けます(この記事では疑似 API を使った validateAsync 版のみ実行しています)。
2. 項目をまたぐ検証は valueOf で他のフィールドを読む
valueOf(p.password) はシグナルとして追跡されるので、パスワード側を変えても確認用のエラーが自動で再計算 され、エラーも確認用フィールドに直接付きます。
3. 配列は「モデルを更新する」だけで入力欄が増減する
@for (s of f.skills; track $index) で各要素を回し、追加・削除は モデルのシグナルを update するだけ です。
4. debounce(p.password, 'blur') で反映タイミングを変える
入力のたびではなく、フォーカスが外れたときにモデルへ反映します。Reactive Forms の updateOn: 'blur' にあたります。
Reactive Forms で同じフォームを書いた場合
比較のため、同じ会員登録フォームを Reactive Forms で書き、同じ環境で動かしました。
// src/app/forms/reactive-register.ts(検証とフォーム定義の部分)
const TAKEN = ['taken@example.com', 'admin@example.com'];
const emailTaken: AsyncValidatorFn = async (c: AbstractControl): Promise<ValidationErrors | null> => {
await new Promise((r) => setTimeout(r, 500));
return TAKEN.includes(String(c.value).toLowerCase()) ? { emailTaken: true } : null;
};
const matchPassword: ValidatorFn = (g: AbstractControl) =>
g.get('password')?.value === g.get('confirm')?.value ? null : { mismatch: true };
const maxItems = (n: number): ValidatorFn => (a: AbstractControl) =>
(a as FormArray).length > n ? { tooMany: true } : null;
// コンポーネント内
private readonly fb = inject(FormBuilder).nonNullable;
protected readonly f = this.fb.group(
{
name: ['', [Validators.required, Validators.maxLength(20)]],
email: ['', { validators: [Validators.required, Validators.email], asyncValidators: [emailTaken] }],
password: ['', { validators: [Validators.required, Validators.minLength(8), Validators.pattern(/[0-9]/)], updateOn: 'blur' }],
confirm: [''],
skills: this.fb.array([this.fb.control('', Validators.required)], maxItems(3)),
agree: [false, Validators.requiredTrue],
},
{ validators: matchPassword },
);
テンプレートでは [formGroup] と formControlName、formArrayName を使い、エラーは f.controls.email.hasError('emailTaken') のように種類ごとに書きました。
実行結果: 空で送信すると お名前を入力してください と invalid: INVALID、taken@example.com では 確認中… の後に このメールは登録済みです が出ました。Reactive Forms はメッセージを hasError('...') ごとにテンプレートへ書き、Signal Forms はルールに持たせて errors() を回すだけ という違いがあります。
Reactive Forms → Signal Forms 移行対応表
| Reactive Forms | Signal Forms | メモ |
|---|---|---|
ReactiveFormsModule |
FormField, FormRoot |
import 元は @angular/forms/signals |
fb.group({ ... }) |
form(signal({ ... }), schema) |
値の型はモデルから推論 |
[formGroup]="f" + (ngSubmit) |
<form [formRoot]="f"> + submission |
novalidate も自動 |
formControlName="email" |
[formField]="f.email" |
文字列ではなく型つきの参照 |
Validators.required |
required(p.email, { message }) |
メッセージをルールに持たせる |
Validators.email / minLength / pattern |
email() / minLength() / pattern() |
ほぼ同名 |
Validators.requiredTrue |
required(p.agree) |
チェックボックスの false は「空」扱い |
グループの ValidatorFn(パスワード一致) |
validate(p.confirm, ({ valueOf }) => ...) |
エラーが確認用フィールドに付く |
AsyncValidatorFn |
validateAsync / validateHttp |
resource ベース、debounce あり |
FormArray + push / removeAt |
配列モデル + update / applyEach |
@for (s of f.skills; ...) |
updateOn: 'blur' |
debounce(p.x, 'blur') |
|
control.value / valueChanges |
f.x().value()(シグナル) |
computed / effect でそのまま使える |
control.errors / hasError('x') |
f.x().errors() / getError('x') |
|
pending / touched / dirty |
pending() / touched() / dirty() |
すべてシグナル |
markAllAsTouched() |
送信時に自動 / markAsTouched() |
v22 は子孫も含む |
disable() / enable() |
disabled(p.x, { when }) |
状態から宣言的に決まる |
reset() |
f().reset(value?) |
既存画面を一度に書き換える必要はありません。 @angular/forms/signals/compat には、Reactive Forms の FormControl をモデルに含めたまま Signal Forms を作れる compatForm() や、Signal Forms を Reactive Forms 側で扱う SignalFormControl などの互換 API が用意されている(いずれも @publicApi 22.0)ので、新しく作るフォームから Signal Forms にしていくと無理なく移行できます。
Signal Forms をテストする
スキーマの動きはコンポーネントなしでテストできます。form() は注入コンテキストが必要なので TestBed.runInInjectionContext の中で作ります。
// src/app/forms/register-form.spec.ts
import { TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { form, required, validate } from '@angular/forms/signals';
describe('Signal Forms のスキーマ', () => {
it('パスワード一致の検証', () => {
TestBed.runInInjectionContext(() => {
const model = signal({ password: 'abc12345', confirm: 'x' });
const f = form(model, (p) => {
required(p.password);
validate(p.confirm, ({ value, valueOf }) =>
value() === valueOf(p.password) ? null : { kind: 'mismatch', message: '一致しません' },
);
});
expect(f.confirm().errors().map((e) => e.kind)).toEqual(['mismatch']);
f.confirm().value.set('abc12345');
expect(f.confirm().valid()).toBe(true);
expect(model().confirm).toBe('abc12345'); // フィールドへの書き込みはモデルに反映
});
});
});
ng test --watch=false(Vitest 5.0.2)で成功しました。フィールドに値を書くとモデルのシグナルも変わる ことがテストでも確認できます。
つまずきやすいポイント
| 症状 | 原因 | 直し方 |
|---|---|---|
[formField] がテンプレートでエラーになる |
コンポーネントの imports に FormField がない |
imports: [FormField] を追加 |
古い記事のとおり [field] と書いて動かない |
v21.0 の書き方 | [formField] に置き換える |
| 画面を開いた瞬間からエラーが出る | 検証は入力前から実行されている | touched() のときだけ表示する |
| 送信してもページがリロードされる | <form> に [formRoot] がない |
[formRoot]="f" を付ける(または (submit) で submit(f, ...) を呼ぶ) |
| 非同期検証が動かない | 同期検証がまだ通っていない | 仕様どおり。required などを満たすと実行される |
非同期検証の途中で valid() も invalid() も false |
検証中(pending() が true) |
pending() で「確認中…」を出す |
| 配列の要素を追加しても入力欄が増えない | 配列を push して同じ参照を set した | update(m => ({ ...m, skills: [...m.skills, ''] })) のように新しい配列を作る |
よくある質問
Q. Signal Forms は本番で使えますか?
A. はい。Angular 22.0 で安定版(@publicApi 22.0)になりました。 v21 では実験版だったため、v21 時代のサンプルは [field] などの書き方が異なる場合があります。WebMCP 連携(experimentalWebMcpTool)だけは引き続き実験的な機能です。
Q. エラーメッセージはどこに書くのがよいですか?
A. ルールの message オプションに書く のがおすすめです。テンプレートは errors() を回すだけになり、表示の書き方がどのフィールドでも同じになります。種類ごとに出し分けたい場合は getError('required') を使います。
Q. 送信処理でサーバーから返ったエラーを特定の項目に出すには?
A. submission.action から { kind: 'taken', message: '...', fieldTree: f.email } のように fieldTree を付けたエラーを返す と、その項目のエラーとして表示されます。
まとめ
- Signal Forms は Angular 22.0 で安定版。
form()と[formField]、[formRoot]が基本の3点です。 - 検証ルールは
required/email/validate/validateAsyncなどの関数で宣言し、メッセージもルールに持たせる と、テンプレートがすっきりします。 - 項目をまたぐ検証は
valueOf、配列はapplyEachとモデルのupdateで書けます。 - 非同期検証は同期検証が通ってから動き、実行中は
pending()が true になります。 - v21 実験版からは
[field]→[formField]、customError()→ オブジェクトなどが変わっています。 v22 ではgetError・minDate・reloadValidationも加わりました。 - Reactive Forms は引き続き使えるので、新しいフォームから Signal Forms にしていく のが無理のない進め方です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular Signal Forms入門|v22安定版のAPI早見表と完成コード」
株式会社RJCでは、新しい API を実際に動かしながら、フォームのような身近な画面づくりの工夫をチームで共有しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。