@fuentis/phoenix-ui 0.0.9-alpha.526 → 0.0.9-alpha.528

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3783,33 +3783,45 @@ function getFieldType(control) {
3783
3783
  }
3784
3784
  }
3785
3785
 
3786
+ /**
3787
+ * MetaFormService is a small UI-state bus used by MetaForm/GroupsForm components.
3788
+ *
3789
+ * - formReadOnlyState: global read-only mode (Edit toggles it).
3790
+ * - formBuildCompleted: emits TRUE when MetaForm finished rebuilding controls & validators.
3791
+ *
3792
+ * Angular dev-mode NOTE:
3793
+ * Emitting "build completed = true" synchronously during the same change detection cycle
3794
+ * can cause NG0100 ExpressionChangedAfterItHasBeenCheckedError.
3795
+ * Therefore we emit TRUE in a macrotask (setTimeout 0).
3796
+ */
3786
3797
  class MetaFormService {
3787
3798
  router = inject(Router);
3788
3799
  formEvent = new Subject();
3789
3800
  formReadOnlySubject = new BehaviorSubject(false);
3790
3801
  formTabContent = new BehaviorSubject('FORM');
3791
3802
  formDirtyStatus = new BehaviorSubject(false);
3803
+ /**
3804
+ * Build completion flag:
3805
+ * - false = rebuilding (controls/validators may change)
3806
+ * - true = stable and ready
3807
+ */
3792
3808
  formBuildCompletedSubject = new BehaviorSubject(false);
3809
+ /** Prevent stale timers from flipping the flag after a new rebuild already started. */
3810
+ buildDoneTimer = null;
3811
+ // Public streams
3793
3812
  currentFormState = this.formEvent.asObservable();
3794
3813
  formReadOnlyState = this.formReadOnlySubject.asObservable();
3795
3814
  currentFormTabContent = this.formTabContent.asObservable();
3796
3815
  currentFormDirtyStatus = this.formDirtyStatus.asObservable();
3797
- /**
3798
- * Notify when form is completed
3799
- * @description Notifies you about when form bild is completed in meta-form component class. Usefull when you need to attach some listeners on form controls or any other action related to form when all controls are avaiable
3800
- * @returns {boolean}
3801
- */
3802
3816
  formBuildCompleted = this.formBuildCompletedSubject.asObservable();
3803
- constructor() { }
3804
3817
  setFormEvent(event) {
3805
3818
  this.formEvent.next(event);
3806
3819
  }
3807
3820
  setFormReadOnlyState(state) {
3808
3821
  this.formReadOnlySubject.next(state);
3822
+ // Optional query param sync (disabled by default):
3809
3823
  // this.router.navigate([], {
3810
- // queryParams: {
3811
- // edit: state ? null : !state,
3812
- // },
3824
+ // queryParams: { edit: state ? null : !state },
3813
3825
  // queryParamsHandling: 'merge',
3814
3826
  // });
3815
3827
  }
@@ -3820,25 +3832,46 @@ class MetaFormService {
3820
3832
  this.formDirtyStatus.next(status);
3821
3833
  }
3822
3834
  /**
3823
- * Sets the form build completion status to indicate when the form is created.
3824
- *
3825
- * @param {boolean} isCompleted - A boolean value to indicate whether the form build is completed.
3826
- * Set to `true` when the form is fully created.
3827
- *
3828
- * @description This method is used to notify when the form is created and ready for use.
3835
+ * Call at the BEGINNING of each rebuild.
3836
+ * Safe to emit synchronously.
3837
+ */
3838
+ startFormBuild() {
3839
+ if (this.buildDoneTimer != null) {
3840
+ clearTimeout(this.buildDoneTimer);
3841
+ this.buildDoneTimer = null;
3842
+ }
3843
+ this.formBuildCompletedSubject.next(false);
3844
+ }
3845
+ /**
3846
+ * Call at the END of each rebuild.
3847
+ * Emit in a macrotask to avoid NG0100.
3848
+ */
3849
+ finishFormBuild() {
3850
+ if (this.buildDoneTimer != null) {
3851
+ clearTimeout(this.buildDoneTimer);
3852
+ this.buildDoneTimer = null;
3853
+ }
3854
+ this.buildDoneTimer = setTimeout(() => {
3855
+ this.formBuildCompletedSubject.next(true);
3856
+ this.buildDoneTimer = null;
3857
+ }, 0);
3858
+ }
3859
+ /**
3860
+ * Backward compatibility for old callers.
3829
3861
  */
3830
3862
  setFormBuildCompletition(isCompleted) {
3831
- this.formBuildCompletedSubject.next(isCompleted);
3863
+ if (!isCompleted)
3864
+ this.startFormBuild();
3865
+ else
3866
+ this.finishFormBuild();
3832
3867
  }
3833
3868
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaFormService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3834
3869
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaFormService, providedIn: 'root' });
3835
3870
  }
3836
3871
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaFormService, decorators: [{
3837
3872
  type: Injectable,
3838
- args: [{
3839
- providedIn: 'root',
3840
- }]
3841
- }], ctorParameters: () => [] });
3873
+ args: [{ providedIn: 'root' }]
3874
+ }] });
3842
3875
 
3843
3876
  class MetaFormAbstract {
3844
3877
  fb;
@@ -3848,8 +3881,10 @@ class MetaFormAbstract {
3848
3881
  metaFormValues;
3849
3882
  metaFormControls;
3850
3883
  /**
3851
- * Hook for wiring dependent fields (category -> color, entity -> assignees etc).
3852
- * This hook is re-bound safely when meta changes, without duplicating subscriptions.
3884
+ * Hook for wiring dependent fields (category -> color, entity -> assignees etc.).
3885
+ * Rebound safely when metadata changes, without duplicating subscriptions.
3886
+ *
3887
+ * Optionally return a cleanup function.
3853
3888
  */
3854
3889
  setupDependencies;
3855
3890
  loading = false;
@@ -3860,26 +3895,9 @@ class MetaFormAbstract {
3860
3895
  onFormSubmit = new EventEmitter();
3861
3896
  onFormCancel = new EventEmitter();
3862
3897
  formActive$;
3863
- /**
3864
- * IMPORTANT:
3865
- * Do not subscribe in the constructor, because @Input() metaForm arrives later.
3866
- * We wire subscriptions in ngOnChanges when the real FormGroup instance is available.
3867
- */
3868
3898
  formSub;
3869
- /**
3870
- * Dependency setup may create subscriptions. We allow cleanup by supporting
3871
- * optional cleanup functions returned by setupDependencies().
3872
- */
3873
3899
  dependencyCleanup = [];
3874
- /**
3875
- * Metadata signature used to detect when controls changed.
3876
- * If the signature changes, dependencies are cleaned up and re-bound.
3877
- */
3878
3900
  lastMetaSignature = '';
3879
- /**
3880
- * Subscriptions used to remove server-side validation errors when the user edits the field.
3881
- * This prevents "sticky" errors blocking save after the value is corrected.
3882
- */
3883
3901
  clearServerErrorSubs = new Map();
3884
3902
  constructor(fb, metaService, translateService) {
3885
3903
  this.fb = fb;
@@ -3888,29 +3906,22 @@ class MetaFormAbstract {
3888
3906
  this.formActive$ = this.metaService.formReadOnlyState;
3889
3907
  }
3890
3908
  ngOnChanges(changes) {
3891
- /**
3892
- * Re-wire form subscription whenever the input form instance changes.
3893
- * This is critical because the component does NOT own the FormGroup;
3894
- * it is provided by the parent.
3895
- */
3896
3909
  if (changes['metaForm']) {
3897
3910
  this.rewireFormSubscription();
3898
3911
  }
3899
- /**
3900
- * If metadata, initial values, or disabled flag changes, rebuild/sync the form.
3901
- * Safe to call multiple times.
3902
- */
3903
3912
  if (changes['metaFormControls'] ||
3904
3913
  changes['metaFormValues'] ||
3905
- changes['disableForm']) {
3906
- if (!this.metaForm)
3907
- return;
3914
+ changes['disableForm'] ||
3915
+ changes['setupDependencies']) {
3916
+ if (!this.metaForm) {
3917
+ throw new Error('[MetaFormAbstract] metaForm input is required. Parent must pass [metaForm].');
3918
+ }
3908
3919
  this.syncForm();
3909
3920
  }
3910
3921
  }
3911
3922
  /**
3912
3923
  * Backwards compatible API:
3913
- * Older code may call createForm(controls). Keep it working.
3924
+ * Older code may call createForm(controls).
3914
3925
  */
3915
3926
  createForm(controls) {
3916
3927
  if (controls)
@@ -3921,60 +3932,48 @@ class MetaFormAbstract {
3921
3932
  this.syncForm();
3922
3933
  }
3923
3934
  /**
3924
- * Main sync:
3925
- * - ensures controls exist
3926
- * - applies validators
3927
- * - patches values (without emitting)
3928
- * - applies disabled state
3929
- * - binds dependencies (safe rebind)
3930
- * - recomputes validity
3935
+ * Rebuild flow (stable + fast):
3936
+ * 1) startFormBuild() (sync)
3937
+ * 2) ensure controls & validators (no emits)
3938
+ * 3) patch values (emitEvent:false)
3939
+ * 4) enable/disable (emitEvent:false)
3940
+ * 5) bind dependencies (safe rebind)
3941
+ * 6) updateValueAndValidity (emitEvent:false)
3942
+ * 7) finishFormBuild() (macrotask -> avoids NG0100)
3931
3943
  */
3932
3944
  syncForm() {
3933
3945
  const flatControls = this.flattenControls(this.metaFormControls);
3934
3946
  const values = this.metaFormValues ?? null;
3935
- // 1) Ensure form controls exist + validators are applied
3947
+ this.metaService.startFormBuild();
3936
3948
  this.ensureControls(flatControls);
3937
- // 2) Patch initial values WITHOUT firing valueChanges
3938
3949
  if (values) {
3939
3950
  this.metaForm.patchValue(values, { emitEvent: false });
3940
3951
  }
3941
- // 3) Enable/disable entire form if requested
3942
3952
  if (this.disableForm)
3943
3953
  this.metaForm.disable({ emitEvent: false });
3944
3954
  else
3945
3955
  this.metaForm.enable({ emitEvent: false });
3946
- // 4) Bind dependencies if needed (rebind when metadata changes)
3947
3956
  this.bindDependenciesIfNeeded(flatControls);
3948
- // 5) Notify that the form is ready
3949
- this.metaService.setFormBuildCompletition(true);
3950
- // 6) Emit one stable status update AFTER the current CD cycle
3951
- queueMicrotask(() => {
3952
- this.metaForm.updateValueAndValidity({ emitEvent: true });
3953
- });
3957
+ // Recompute validity WITHOUT emitting events during build.
3958
+ this.metaForm.updateValueAndValidity({ emitEvent: false });
3959
+ // Emit "build completed" AFTER CD cycle.
3960
+ this.metaService.finishFormBuild();
3954
3961
  }
3955
- /**
3956
- * Supports both:
3957
- * - Flat controls: [ {configuration:{key}}, ...]
3958
- * - Grouped controls: [ { ctrl: [ ... ] }, ... ]
3959
- */
3960
3962
  flattenControls(input) {
3961
3963
  if (!Array.isArray(input))
3962
3964
  return [];
3963
3965
  const isFlat = input.every((x) => !!x?.configuration?.key);
3964
3966
  if (isFlat)
3965
3967
  return input;
3966
- const flattened = input.flatMap((x) => Array.isArray(x?.ctrl) ? x.ctrl : x);
3967
- return flattened.filter((x) => !!x?.configuration?.key);
3968
+ return input
3969
+ .flatMap((x) => (Array.isArray(x?.ctrl) ? x.ctrl : x))
3970
+ .filter((x) => !!x?.configuration?.key);
3968
3971
  }
3969
3972
  metaSignature(controls) {
3970
3973
  return (controls ?? [])
3971
3974
  .map((c) => `${c?.configuration?.key ?? ''}:${c?.configuration?.type ?? ''}`)
3972
3975
  .join('|');
3973
3976
  }
3974
- /**
3975
- * Ensures dependencies are bound exactly once per metadata signature.
3976
- * If metadata changes (new controls, different keys), we clean up and re-bind.
3977
- */
3978
3977
  bindDependenciesIfNeeded(flatControls) {
3979
3978
  if (!this.setupDependencies)
3980
3979
  return;
@@ -3990,7 +3989,6 @@ class MetaFormAbstract {
3990
3989
  findMeta: (key) => flatControls.find((c) => c?.configuration?.key === key) ?? null,
3991
3990
  };
3992
3991
  const maybeCleanup = this.setupDependencies(ctx);
3993
- // Allow setupDependencies to return a cleanup function (optional)
3994
3992
  if (typeof maybeCleanup === 'function') {
3995
3993
  this.dependencyCleanup.push(maybeCleanup);
3996
3994
  }
@@ -4004,31 +4002,22 @@ class MetaFormAbstract {
4004
4002
  catch { }
4005
4003
  });
4006
4004
  this.dependencyCleanup = [];
4007
- this.lastMetaSignature = '';
4008
4005
  }
4009
- /**
4010
- * Creates missing controls, applies validators, and removes stale controls.
4011
- * Also attaches "clear server error on edit" subscriptions per control.
4012
- */
4013
4006
  ensureControls(controls) {
4014
4007
  (controls ?? []).forEach((control) => {
4015
4008
  const key = control?.configuration?.key;
4016
4009
  if (!key)
4017
4010
  return;
4018
- // Create control once
4019
4011
  if (!this.metaForm.contains(key)) {
4020
4012
  this.metaForm.addControl(key, this.fb.control(getFieldType(control)));
4021
4013
  }
4022
- // Apply sync validators
4023
4014
  this.applyValidators(control);
4024
- // Clear server-side errors when user edits this field
4025
4015
  this.bindClearServerError(key);
4026
- // Disable if metadata says so
4027
4016
  if (control?.disable) {
4028
4017
  this.metaForm.get(key)?.disable({ emitEvent: false });
4029
4018
  }
4030
4019
  });
4031
- // Remove stale controls when metadata changes
4020
+ // remove stale controls when metadata changes
4032
4021
  const allowed = new Set((controls ?? [])
4033
4022
  .map((c) => c?.configuration?.key)
4034
4023
  .filter(Boolean));
@@ -4041,10 +4030,8 @@ class MetaFormAbstract {
4041
4030
  });
4042
4031
  }
4043
4032
  /**
4044
- * Removes only server-originated uniqueness errors when the user changes the value.
4045
- * This prevents a corrected value from staying blocked by an old server error.
4046
- *
4047
- * We DO NOT touch other errors (required, pattern, etc.).
4033
+ * Removes ONLY server-origin uniqueness errors on user edit.
4034
+ * Keeps required/pattern/email/etc.
4048
4035
  */
4049
4036
  bindClearServerError(key) {
4050
4037
  if (this.clearServerErrorSubs.has(key))
@@ -4063,17 +4050,11 @@ class MetaFormAbstract {
4063
4050
  ctrl.setErrors(Object.keys(rest).length ? rest : null, {
4064
4051
  emitEvent: false,
4065
4052
  });
4066
- // Recompute the parent form validity so UI can enable Save immediately.
4067
- queueMicrotask(() => {
4068
- this.metaForm.updateValueAndValidity({ emitEvent: true });
4069
- });
4053
+ // Here it's a user interaction, so emitting is safe and desired:
4054
+ this.metaForm.updateValueAndValidity({ emitEvent: true });
4070
4055
  });
4071
4056
  this.clearServerErrorSubs.set(key, sub);
4072
4057
  }
4073
- /**
4074
- * Applies synchronous validators based on metadata.
4075
- * Uses emitEvent:false to avoid unnecessary loops.
4076
- */
4077
4058
  applyValidators(control) {
4078
4059
  const key = control?.configuration?.key;
4079
4060
  if (!key)
@@ -4109,12 +4090,9 @@ class MetaFormAbstract {
4109
4090
  ctrl.setValidators(validators);
4110
4091
  ctrl.updateValueAndValidity({ emitEvent: false });
4111
4092
  }
4112
- /**
4113
- * Subscribes MetaFormService to the correct FormGroup instance.
4114
- * Must run after @Input metaForm is available.
4115
- */
4116
4093
  rewireFormSubscription() {
4117
4094
  this.formSub?.unsubscribe();
4095
+ // metaForm is required, but keep fallback safe
4118
4096
  if (!this.metaForm) {
4119
4097
  this.metaForm = this.fb.group({});
4120
4098
  }