Angular Reactive Forms 如何切换语言并保留表单数据?
结论
建议按语言代码为每种语言建立独立的 FormGroup。切换语言时只切换当前显示的子表单,不删除或重新创建控件。这样,各语言已填写的内容都会保留;首次切换到还没有数据的语言时,字段会显示为空。
Language 和 Internal Key 可以放在外层表单中,标题、描述等语言相关字段则放在按语言代码分组的子表单里。下面的示例沿用问题中的 TranslationType 和 TranslationLanguage,并将 Title 显示为 Category Name,将 FullDescription 显示为 Category Description。
创建每种语言的子表单
下面的示例会在初始化时为每种语言创建一组控件,并从已有的 Translation[] 载入内容。为简化示例,代码使用 FormGroup<any>。如果项目启用了严格类型表单,可以为各组控件补充具体类型。
import { FormControl, FormGroup } from '@angular/forms';
enum TranslationType {
Title = 0,
ShortDescription = 1,
FullDescription = 2,
Meta = 3
}
enum TranslationLanguage {
RO = 0,
EN = 1,
DE = 2,
FR = 3
}
interface Translation {
id: string;
value: string;
translationType: TranslationType;
language: TranslationLanguage;
}
const languages = [
TranslationLanguage.RO,
TranslationLanguage.EN,
TranslationLanguage.DE,
TranslationLanguage.FR
];
const translationTypes = [
TranslationType.Title,
TranslationType.ShortDescription,
TranslationType.FullDescription,
TranslationType.Meta
];
export class CategoryEditorComponent {
readonly languages = languages;
readonly form: FormGroup<any>;
selectedLanguage = TranslationLanguage.EN;
constructor() {
const existingTranslations: Translation[] = [];
const languageGroups: Record<string, FormGroup<any>> = {};
for (const language of this.languages) {
const fields: Record<string, FormControl<string>> = {};
for (const type of translationTypes) {
const translation = existingTranslations.find(
item => item.language === language && item.translationType === type
);
fields[String(type)] = new FormControl(translation?.value ?? '', {
nonNullable: true
});
}
languageGroups[String(language)] = new FormGroup(fields);
}
this.form = new FormGroup({
internalKey: new FormControl('', { nonNullable: true }),
translationsByLanguage: new FormGroup(languageGroups)
});
}
get currentTranslationForm(): FormGroup<any> {
return this.form.get([
'translationsByLanguage',
String(this.selectedLanguage)
]) as FormGroup<any>;
}
onLanguageChange(value: string | number): void {
this.selectedLanguage = Number(value) as TranslationLanguage;
}
}
实际组件中的 existingTranslations 应从正在编辑的分类中获取。没有对应翻译记录的字段会以空字符串作为初始值。将字段名设为 TranslationType 的枚举值后,提交时就可以直接映射回 Translation[]。
切换语言并显示子表单
语言选择器会更新当前语言,模板则绑定到对应的子表单:
<select
[value]="selectedLanguage"
(change)="onLanguageChange($any($event.target).value)"
>
<option *ngFor="let language of languages" [value]="language">
{{ language }}
</option>
</select>
<form [formGroup]="form">
<label>
Internal Key
<input formControlName="internalKey" />
</label>
<section [formGroup]="currentTranslationForm">
<label>
Category Name
<input [formControlName]="TranslationType.Title" />
</label>
<label>
Category Description
<textarea [formControlName]="TranslationType.FullDescription"></textarea>
</label>
</section>
</form>
要在模板中使用 TranslationType.Title 和 TranslationType.FullDescription,需要将枚举暴露为组件属性:
readonly TranslationType = TranslationType;
切换语言时,变化的只是 currentTranslationForm 指向的子表单。各语言的控件仍保留在父表单中,所以用户填写 EN 后切换到 FR,再切回 EN,之前输入的内容还在。若切换时删除或重新创建控件,则需要另行保存并恢复它们的值。
提交时生成 Translation[]
保存时,可以遍历语言和字段,将有内容的字段转换为翻译记录。若需要保留现有记录的 id,可以按语言和类型查找并复用。新记录的 ID 则应按项目现有的生成或持久化方式处理。
function collectTranslations(
translationsByLanguage: Record<string, Record<string, string>>,
existingTranslations: Translation[]
): Translation[] {
const result: Translation[] = [];
for (const language of languages) {
const values = translationsByLanguage[String(language)];
for (const type of translationTypes) {
const value = values[String(type)]?.trim() ?? '';
if (!value) continue;
const existing = existingTranslations.find(
item => item.language === language && item.translationType === type
);
if (!existing) {
// 新记录的 ID 按项目现有规则生成或由服务端创建。
continue;
}
result.push({ ...existing, value });
}
}
return result;
}
这段提交代码只更新已有翻译,并为新翻译预留了 ID 创建位置。实际保存逻辑还需补上项目使用的 ID 处理方式,并确认空字符串表示“删除翻译”还是“不保存该字段”。此外,问题中的 ObjectType 没有作为字段出现在 Translation 接口中;如果翻译记录还要关联产品或分类,保存方式应由实际数据模型和接口约定决定。
另一种表单组织方式
如果语言较多,或者用户需要同时编辑多种语言,也可以直接将所有语言以分栏或标签页的形式展示。数据模型仍然可以使用按语言分组的子表单,区别在于界面会同时显示多组字段。选择下拉框还是标签页,主要取决于交互设计。
备注:内容仅供参考。
