برنامه نویسی

مؤلفه های زاویه ای: فرم های ترکیب محور بر وراثت

مؤلفه های زاویه ای: فرم های ترکیب محور بر وراثت
در برنامه نویسی شی گرا (OOP) ، ترکیب (HAS-A) یک مفهوم کلیدی است ، در کنار وراثت (IS-A)بشر

در زاویه ای ، ما اغلب هنگام تزریق خدمات به مؤلفه هایی که درخواست های API ، حافظه پنهان و غیره را اداره می کنیم ، به طور طبیعی از ترکیب استفاده می کنیم.

با این حال ، وراثت – گسترش a BaseComponent – همچنین در زاویه ای رایج است. یکی از اصلی ترین نقاط قوت وراثت ، رفتار مشترک است.

در بسیاری از موارد خوب کار می کند ، اما گاهی اوقات یک مشکل ظریف ایجاد می کند:

هنگامی که مؤلفه پایه شروع به نگه داشتن منطق مربوط به فرم می کند ، هر مؤلفه کودک (حتی آنهایی که فقط داده ها را نمایش می دهند ، ایجاد نمی کنند یا به روز می کنند) را مجبور می کند تا منطق را که ممکن است به آنها احتیاج نداشته باشد ، به ارث ببرد. این می تواند به اجزای محکم و محکم منجر شود.

ساده و حداقل مؤلفه پایه یک روش OOP معتبر است.

به عنوان مثال ، الف BaseComponent ممکن است اینگونه به نظر برسد:

// src/app/shared/component/base.component.ts

@Component({
  selector: '',
  template: '',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BaseComponent {

  private errorMsg: WritableSignal<string> = signal('');
  private statusMsg: WritableSignal<string> = signal('');
  private ready: WritableSignal<boolean> = signal(false);

  protected componentReady(): void {
    this.ready.set(true);
  }

  protected resetErrorMessage(): void {
    this.errorMsg.set('');
  }

  protected resetStatusMessage(): void {
    this.statusMsg.set('');
  }

  protected updateStatusMessage(message: string = ''): void {
    this.statusMsg.set(message);
  }

  protected updateErrorMessage(message: string = ''): void {
    this.errorMsg.set(message);
  }

  public get isComponentReady(): boolean {
    return this.ready();
  }

  public get errorMessage(): string {
    return this.errorMsg();
  }

  public get statusMessage(): string {
    return this.statusMsg();
  }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

استفاده مجدد از منطق از طریق وراثت مانند این کاملاً خوب است – وقتی با دقت انجام شد.

با این حال ، هنگامی که صحبت از اشکال می شود ، یک رویکرد پاک کننده و مقیاس پذیر تر این است که به جای وراثت آن ، عملکردی را در داخل اجزای ایجاد کنید.

در این راهنما خواهیم دید که چگونه:

  • یک مدل دامنه ایجاد کنید (ChatRoom)
  • یک فرم قابل استفاده مجدد بسازید (ChatRoomForm)
  • رابط مؤلفه را تعریف کنید (HasForm شبیه OnInit)
  • یک کلاس پایه ایجاد کنید که می تواند توسط کلاسهای مرتبط با فرم گسترش یابد.

بخش 1: ایجاد ChatRoom

بیایید یک مدل دامنه ساده را برای ایجاد یا به روزرسانی اتاق گپ تعریف کنیم.

// src/app/model/domain/chat-room.ts

export class ChatRoom {
  public readonly id: number | string;
  public readonly title: string;
  public readonly description: string;
  public readonly tags: string;
  public readonly guidelinesOrRules: string;
  public readonly visibility: ChatRoomVisibility;

  public constructor(data: ChatRoom) {
    this.id = data.id ?? 0;
    this.title = data.title ?? '';
    this.description = data.description ?? '';
    this.tags = data.tags ?? '';
    this.guidelinesOrRules = data.guidelinesOrRules ?? '';
    this.visibility = data.visibility ?? '';
  }

  public static of(data: ChatRoom): ChatRoom {
    return new ChatRoom(data);
  }

  public static empty(): ChatRoom {
    return new ChatRoom({} as ChatRoom);
  }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

بخش 2: CreatingBaseForm

حال ، بیایید فرم پایه را به شرح زیر تعریف کنیم:

// src/app/model/form/base.form.ts

export abstract class BaseForm<T> {

  protected formGroup!: FormGroup;

  private submitting: WritableSignal<boolean> = signal(false);
  private formReady: WritableSignal<boolean> = signal(false);
  private formCompleted: WritableSignal<boolean> = signal(false);

  protected constructor() {}

  protected initForm(): void {}

  protected control(name: string): AbstractControl | null | undefined {
    return this.form?.get(name);
  }

  public enableFormComplete(): void {
    this.formCompleted.set(true);
  }

  public disableFormCompleted(): void {
    this.formCompleted.set(false);
  }

  public openForm(): void {
    this.formReady.set(true);
  }

  public startSubmitting(): void {
    this.submitting.set(true);
  }

  public stopSubmitting(): void {
    this.submitting.set(false);
  }

  public get form(): FormGroup {
    return this.formGroup;
  }

  public get value(): T {
    return this.form.value as T;
  }

  public get isFormValid(): boolean {
    return this.formGroup.valid;
  }

  public get isFormCompleted(): boolean {
    return this.formCompleted();
  }

  public get isFormReady(): boolean {
    return this.formReady();
  }

  public get isSubmitting(): boolean {
    return this.submitting();
  }

  public get isNotSubmitting(): boolean {
    return !(this.isSubmitting);
  }

}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در formGroup حاوی فرم شما است.

در submitting سیگنال وضعیت ارسال فرم شما را ردیابی می کند. تا زمان پردازش درخواست می توانید از آن برای جلوگیری از ارسال های متعدد استفاده کنید.

در formReady سیگنال به شما در نمایش فرم HTML در UI در هنگام اولیه سازی فرم با کنترل و داده های آن کمک می کند.

در formCompleted سیگنال اختیاری است. این به شما کمک می کند تا پس از اتمام ارسال فرم ، انیمیشن های UI یا انتقال را به عنوان بازخورد نمایش دهید.

در control روش به شما امکان می دهد بدون تکرار نحو ، کنترل فرم را از یک گروه فرم بازیابی کنید. به عنوان مثال:

  public get title(): AbstractControl | null | undefined {
    return this.control('title');
  }

  public get description(): AbstractControl | null | undefined {
    return this.control('description');
  }
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در مقایسه با

  public get title(): AbstractControl | null | undefined {
    return this.formGroup?.get('title');
  }

  public get description(): AbstractControl | null | undefined {
    return this.formGroup?.get('description');
  }
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

نام متغیر فرم ممکن است تغییر کند ، اما با استفاده از control روش یک منبع واحد از حقیقت را فراهم می کند و منطق بازیابی کنترل فرم محصور می شود. هنگامی که تغییری وجود دارد ، فقط این روش را به روز می کنید.

در getter و روش های دیگر راهی است محاصره کننده منطق و رفتار فرم و همچنین به شما امکان دسترسی به وضعیت و داده های آن را می دهد.

بخش 3: CreatingChatRoomForm

اکنون ، ما ایجاد خواهیم کرد ChatRoomForm

// src/app/model/form/chat-room.form.ts

export class ChatRoomForm extends BaseForm<CreateChatRoomPayload> {

  private constructor(
      private formBuilder: FormBuilder,
      private chatRoom: ChatRoom) {
    super();
  }

  public get title(): AbstractControl | null | undefined {
    return this.control('title');
  }

  public get description(): AbstractControl | null | undefined {
    return this.control('description');
  }

  public get guidelines(): AbstractControl | null | undefined {
    return this.control('guidelinesOrRules');
  }

  public get tags(): AbstractControl | null | undefined {
    return this.control('tags');
  }

  public get visibility(): AbstractControl | null | undefined {
    return this.control('visibility');
  }

  public get visibilities(): string[] {
    return Object.values(ChatRoomVisibility);
  }

  protected override initForm(): void {
    this.formGroup = this.formBuilder.group({
      title: [this.chatRoom.title, [
        required,
        minLength(10),
        maxLength(500),
      ]],

      description: [this.chatRoom.description, [
        required,
        maxLength(1000),
      ]],

      tags: [this.chatRoom.tags, [
        required,
        minLength(10),
        maxLength(500),
      ]],

      guidelinesOrRules: [this.chatRoom.guidelinesOrRules, [
        required,
        maxLength(1500),
      ]],

      visibility: [this.chatRoom.visibility, [
        required,
        oneOf(ChatRoomVisibility)
      ]],

    });
  }

  public static of(formBuilder: FormBuilder, chatRoom: ChatRoom): ChatRoomForm {
    const chatRoomForm: ChatRoomForm = new ChatRoomForm(formBuilder, chatRoom);
    chatRoomForm.initForm();

    return chatRoomForm;
  }

  public static empty(formBuilder: FormBuilder): ChatRoomForm {
    return new ChatRoomForm(formBuilder, ChatRoom.empty());
  }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در ChatRoomForm از آنجا که در تزریق وابستگی استفاده نمی شود ، یک سازنده خصوصی دارد. در عوض ، از طریق static of روش ، به دنبال الگوی کارخانه.

با استفاده از اعتبار سنجی از این طریق ، تکرار کلاس اعتبار سنج ها را کاهش می دهد و از پیاده سازی های چند شکل پشتیبانی می کند. می توانید اعتبار سنجی های سفارشی را معرفی کنید و بدون به روزرسانی کل پایگاه کد ، بلافاصله اعمال می شود. به عنوان مثال ، می توانید اعتبار سنجی مانند این را تعریف کنید:

// src/app/shared/validators/index.ts

export const required = Validators.required;
export const minLength = Validators.minLength;
export const maxLength = Validators.maxLength;
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در مورد نحوه سازماندهی اعتبار سنج ها در یک برنامه زاویه ای بیشتر بدانید.

بخش 4: ایجاد HasForm رابط

در HasForm رابط ، برای مؤلفه هایی که به یک فرم پر از کاربر نیاز دارند ، به نظر می رسد:

// src/app/model/interface/form/has-form.interface.ts

export interface HasForm {

  formReady(): void;

  initForm(data?: any): void;

  startSubmitting(): void;

  stopSubmitting(): void;

  completeForm(): void;

  get formModel(): BaseForm<any>;

  get payload(): any;

  get isFormReady(): boolean;

  get isFormCompleted(): boolean;

  get isFormValid(): boolean

  get isNotSubmitting(): boolean;

  get isSubmitting(): boolean;
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

این روشهای دریافت کننده رفتار و منطق فرم واقعی را محاصره می کنند و در UI و بخشی از مؤلفه شما استفاده می شوند.

این رفتارها معمولاً درBaseComponentقبل و سپس با گسترش مؤلفه ها به ارث رسیده است. این همیشه ایده آل نیست به عنوان مؤلفه هایی مانند ChatRoomItem وت ChatRoomList به طور معمول داده ها را بدون جهش یا به روزرسانی آن نمایش دهید.

شما می توانید این رابط را برای کار با الگوهای طراحی و معماری مؤلفه موجود خود سازگار کنید.

در CreateChatRoom مؤلفه به نظر می رسد

@Component({
  selector: 'app-create-chat-room',
  imports: [
    ValidationErrorComponent,
    ReactiveFormsModule,
    FaIconComponent,
  ],
  providers: [
    ChatRoomService,
  ],
  templateUrl: './create-chat-room.html',
  styleUrl: './create-chat-room.css'
})
export class CreateChatRoom extends BaseComponent implements OnInit, HasForm {

  protected readonly chatRoomService: ChatRoomService = inject(ChatRoomService);
  protected readonly formBuilder: FormBuilder = inject(FormBuilder);

  protected readonly chatRoomForm: WritableSignal<ChatRoomForm> = signal(ChatRoomForm.empty(this.formBuilder));

  public ngOnInit(): void {
    this.initForm();
  }

  public formReady(): void {
    this.formModel.openForm();
    this.componentReady();
  }

  public completeForm(): void {
    this.formModel.completeForm();
  }

  public initForm(): void {
    const createChatRoomForm: ChatRoomForm = ChatRoomForm.of(this.formBuilder, ChatRoom.empty());
    this.chatRoomForm.set(createChatRoomForm);

    this.formReady();
  }

  public createChatRoom(): void {
    if (this.isFormValid && this.isNotSubmitting) {
      this.startSubmitting();

      const payload: CreateChatRoomPayload = this.payload;
      this.chatRoomService.create(payload)
        .subscribe({
          next: (response: CreateChatRoomResponse): void => { this.createChatRoomSuccess(response); },
          error: (error: ErrorResponse): void => { this.createChatRoomFailure(error); },
          complete: (): void => { this.createChatRoomComplete(); }
      });
    }
  }

  protected createChatRoomSuccess(result: CreateChatRoomResponse): void {
    this.updateStatusMessage(result.message);
  }

  protected createChatRoomFailure(error: ErrorResponse): void {
    this.updateErrorMessage(error.message);
    this.createChatRoomComplete();
  }

  protected createChatRoomComplete(): void {
    this.stopSubmitting();
    this.completeForm();
  }

  public startSubmitting(): void {
    this.formModel.startSubmitting();
  }

  public stopSubmitting(): void {
    this.formModel.stopSubmitting();
  }

  public get createChatRoomForm(): FormGroup {
    return this.formModel.form;
  }

  public get payload(): CreateChatRoomPayload {
    return this.formModel.value;
  }

  public get formModel(): ChatRoomForm {
    return this.chatRoomForm();
  }

  public get isFormReady(): boolean {
    return this.formModel.isFormReady;
  }

  public get isFormValid(): boolean {
    return this.formModel.isFormValid;
  }

  public get isFormCompleted(): boolean {
    return this.formModel.isFormCompleted;
  }

  public get isSubmitting(): boolean {
    return this.formModel.isSubmitting;
  }

  public get isNotSubmitting(): boolean {
    return this.formModel.isNotSubmitting;
  }

  protected readonly faCheck = faCheck;
  protected readonly faSpinner = faSpinner;
  protected readonly faPlus = faPlus;
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در signal(ChatRoomForm.empty()) سیگنال را با فرم شروع کنید اما وضعیت آن آماده نخواهد شد ، UI و کد با بررسی ISFormReady در الگوی HTML محافظت و محافظت می شود.

@if (isFormReady) {
  
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در initForm روش جدید ایجاد می کند ChatRoomForm با استفاده از استاتیک روش ، فرم را با فرم باز کنید و آماده شوید تا با آن تعامل کنید و سپس آن را برگردانید.

در openForm روش تنظیم می کند formReady حالت سیگنال به true که نشان می دهد اولیه سازی کامل است و فرم آماده است تا توسط کاربر با یا تکمیل شود.

در payload روش Getter مقدار فرم مورد استفاده خود را برای ایجاد اتاق چت برمی گرداند. آن را type به نظر می رسد

export type CreateChatRoomPayload = {
  title: string;
  description: string;
  guidelinesOrRules: string;
  tags: string;
  visibility: ChatRoomVisibility | string;
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

روش Payload Getter به شما امکان می دهد قبل از استفاده از داده ها ، از پیش پردازش یا اضافه کردن جزئیات دیگر استفاده کنید. به عنوان مثال

public get payload(): CreateLiveStreamPayload {
  const data: CreateLiveStreamPayload = this.formModel.value;
  return {
    ...data,
    startDateTime: addSecondsToDate(this.formModel.startDateTimeValue),
    endDateTime: addSecondsToDate(this.formModel.endDateTimeValue),
  };
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

بخش 5: الگوی HTML

@if (isComponentReady && isFormReady) {
  
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

شما از روشهای دریافت کننده برای تقویت UI و منطق شکل استفاده می کنید.

علاوه بر این:

چه در مورد کامل ()؟

پیش از این ، من ذکر کردم که چگونه formCompleted انیمیشن های UI به سیگنال کمک می کند. اجرای به شرح زیر است:

  protected formCompleted: WritableSignal<boolean> = signal(false);
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

اجرای در شکل پایه چیزی شبیه به این خواهد بود:

  public completeForm(delayToReset: number = 5000): void {
    // Ensure delay is non-negative
    const safeDelay: number = Math.max(0, delayToReset);

    // Mark the form as completed
    this.enableFormComplete();

    // Set a timer to reset form completion status
    timer(safeDelay).pipe(
      // Reset form completion status
      tap(() => this.disableFormCompleted()),
    ).subscribe();
  }
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

پس از اتمام فرم کاربر و completeForm() نامیده می شود ، انیمیشن جذاب قلم همانطور که در اینجا نشان داده شده است شروع می شود

@if (isFormCompleted) {<fa-icon [icon]="faCheck"/>}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

پس از تأخیر پیش فرض 5 ثانیه ، وضعیت دوباره تنظیم می شود false و بررسی یا علامت گذاری نماد ناپدید می شودبشر

توجه:

البته ، روش ها در HasForm قبلاً در formModel یا BaseFormبشر این قابل بحث است که آیا این امر باعث افزایش بیش از حد است. شما می توانید تصمیم بگیرید که تمام روش های موجود در Hasform را پیاده سازی کنید یا می توانید برخی از آنها را حذف کرده و به آن دسترسی پیدا کنید formModel در جزء کد یا مستقیماً در الگوی HTML.

مخزن: ترکیب فرم زاویه ای

استفاده از ترکیب منجر به جداسازی بهتر نگرانی ها ، آزمایش آسانتر و اجزای قابل استفاده مجدد می شود. امیدوارم از آن لذت ببرید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا