Skip to content

Presentational component usage #24

@alvipeo

Description

@alvipeo

First of all, thanks for the lib!

But I use it with NGRX and with Containers/Presentational (I call them Meat and Skin) components, and things get complicated. I wonder if there is an easier way, or do I use it right?

Here's the code:

const routes: Routes = [
   {
      path: "",
      component: ComponentSidenav,
      children: [
         ...
         {
            path: "edit/:id",
            component: CompanyEditComponent,
            canDeactivate: [FormDirtyGuard],
            resolve: {
               company: CompanyResolver
            }
         }
      ]
   }
];

Container:

export class CompanyEditComponent implements DirtyComponent {
   ...
   
   readonly savedSuccessfully$ = this.stateSvc.saveSuccessful$;

   isDirty$: Observable<boolean> | boolean | (() => boolean);

   constructor(private stateSvc: CompaniesStateService) {
      this.isDirty$ = this.savedSuccessfully$.pipe(
         withLatestFrom(this.frmIsDirty.asObservable()),
         map(([saved, dirty]) => {
            // when data is saved then we don't care if form is dirty or not
            return saved ? !saved : dirty;
         })
      );
   }

   setFormDirty(value: boolean) {
      this.frmIsDirty.next(value);
   }

   private frmIsDirty = new BehaviorSubject<boolean>(false);
}

Presentation:

@Component({
   selector: "ilg-company-edit",
   templateUrl: "./company-edit-form.component.html",
   styleUrls: ["./company-edit-form.component.scss"],
   changeDetection: ChangeDetectionStrategy.OnPush
})
export class CompanyEditFormComponent extends IlgDirtyPresentationBase<object, MmlCompany> implements OnInit, OnDestroy {
   @Input() company?: MmlCompany;

   get form(): FormGroup<any> {
      return this.frm;
   }
   get sourceEntity(): MmlCompany | undefined {
      return this.company;
   }

   frm = this.fb.group<CompanyForm>({...});

   constructor(private fb: NonNullableFormBuilder, private route: ActivatedRoute) {
      super();
   }

   override ngOnInit(): void {
      super.ngOnInit();

      ...

      // this will have a value 100% because of the data resolver
      if (this.company && this.company.id) {
         this.frm.patchValue(this.company);
      }
   }

   protected createEntityFromFormValue(frmVal: any): MmlCompany | undefined {
      /*
       *
       * CAREFUL WITH THIS FUNCTION => if any error occurred YOU WON'T SEE IT!
       * Use try {} catch {}
       *
       */

      if (areAllPropsFalsy(frmVal)) {
         // if all props are falsy ==> return undefined
         return undefined;
      }

      return new MmlCompany(
         frmVal.name ?? "",
         frmVal.shortName ?? "",
         frmVal.phone ?? "",
         frmVal.email ?? "",
         this.company ? this.company.printingMml : true,
         frmVal.licenseNumber,
         frmVal.licenseExpDate,
         this.company ? this.company.id : undefined,
         frmVal.comment
      );
   }
}

And here's the common class to implement dirty checking for any of Presentational form component:

export abstract class IlgDirtyPresentationBase<T, R> implements OnInit, OnDestroy {
   @Output() formIsDirty = new EventEmitter<boolean>();

   abstract form: FormGroup;
   abstract sourceEntity: R | undefined;

   protected subscriptions = new Subscription();

   isDirty$?: Observable<boolean>;

   protected abstract createEntityFromFormValue(frmVal: T): R | undefined;

   ngOnDestroy() {
      this.subscriptions.unsubscribe();
   }

   ngOnInit(): void {
      this.isDirty$ = this.form.valueChanges.pipe(this.checkIsDirty(this.sourceEntity, this.createEntityFromFormValue.bind(this))); // bind() IS IMPORTANT HERE!

      this.subscriptions.add(
         //
         // ===> this subscription MUST BE in ngOnInit !!!
         //
         this.isDirty$.subscribe((val) => this.formIsDirty.emit(val))
      );
   }

   private checkIsDirty<T, R>(sourceEntity: R | undefined, createEntityFromFormValue: (frmVal: T) => R): OperatorFunction<T, boolean> {
      function wrapUserFunction(frmVal: T) {
         try {
            return createEntityFromFormValue(frmVal);
         } catch (e) {
            console.error(`createCompanyFromValue exception: ${e}`);
            // return {} as R;
            throw e;
         }
      }

      return pipe(
         debounceTime(400),
         distinctUntilChanged(),
         map(wrapUserFunction),
         map((frmEntity) => !deepEqualRelaxed(sourceEntity, frmEntity)),
         startWith(false),
         shareReplay()
      );
   }

I did not find a way to adopt easily dirty-check-forms lib for actual checking. Store shouldn't be present in Presentational components.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions