Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Presentational component usage #24

Open
alvipeo opened this issue Jul 25, 2022 · 0 comments
Open

Presentational component usage #24

alvipeo opened this issue Jul 25, 2022 · 0 comments

Comments

@alvipeo
Copy link

alvipeo commented Jul 25, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant