@fuentis/phoenix-ui 0.0.9-alpha.527 → 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,59 +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
- // MetaFormAbstract (samo bitne izmene – kompletno je kompatibilno sa tvojim kodom)
3933
3944
  syncForm() {
3934
3945
  const flatControls = this.flattenControls(this.metaFormControls);
3935
3946
  const values = this.metaFormValues ?? null;
3936
- // 1) Ensure controls + apply validators
3947
+ this.metaService.startFormBuild();
3937
3948
  this.ensureControls(flatControls);
3938
- // 2) Patch initial values WITHOUT firing valueChanges
3939
3949
  if (values) {
3940
3950
  this.metaForm.patchValue(values, { emitEvent: false });
3941
3951
  }
3942
- // 3) Enable/disable entire form without emitting
3943
3952
  if (this.disableForm)
3944
3953
  this.metaForm.disable({ emitEvent: false });
3945
3954
  else
3946
3955
  this.metaForm.enable({ emitEvent: false });
3947
- // 4) Bind dependencies (safe rebind)
3948
3956
  this.bindDependenciesIfNeeded(flatControls);
3949
- // 5) Compute validity NOW (sync) but DO NOT emit (critical!)
3957
+ // Recompute validity WITHOUT emitting events during build.
3950
3958
  this.metaForm.updateValueAndValidity({ emitEvent: false });
3951
- // 6) Notify that build is complete (signal to parent)
3952
- this.metaService.setFormBuildCompletition(true);
3959
+ // Emit "build completed" AFTER CD cycle.
3960
+ this.metaService.finishFormBuild();
3953
3961
  }
3954
- /**
3955
- * Supports both:
3956
- * - Flat controls: [ {configuration:{key}}, ...]
3957
- * - Grouped controls: [ { ctrl: [ ... ] }, ... ]
3958
- */
3959
3962
  flattenControls(input) {
3960
3963
  if (!Array.isArray(input))
3961
3964
  return [];
3962
3965
  const isFlat = input.every((x) => !!x?.configuration?.key);
3963
3966
  if (isFlat)
3964
3967
  return input;
3965
- const flattened = input.flatMap((x) => Array.isArray(x?.ctrl) ? x.ctrl : x);
3966
- 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);
3967
3971
  }
3968
3972
  metaSignature(controls) {
3969
3973
  return (controls ?? [])
3970
3974
  .map((c) => `${c?.configuration?.key ?? ''}:${c?.configuration?.type ?? ''}`)
3971
3975
  .join('|');
3972
3976
  }
3973
- /**
3974
- * Ensures dependencies are bound exactly once per metadata signature.
3975
- * If metadata changes (new controls, different keys), we clean up and re-bind.
3976
- */
3977
3977
  bindDependenciesIfNeeded(flatControls) {
3978
3978
  if (!this.setupDependencies)
3979
3979
  return;
@@ -3989,7 +3989,6 @@ class MetaFormAbstract {
3989
3989
  findMeta: (key) => flatControls.find((c) => c?.configuration?.key === key) ?? null,
3990
3990
  };
3991
3991
  const maybeCleanup = this.setupDependencies(ctx);
3992
- // Allow setupDependencies to return a cleanup function (optional)
3993
3992
  if (typeof maybeCleanup === 'function') {
3994
3993
  this.dependencyCleanup.push(maybeCleanup);
3995
3994
  }
@@ -4003,31 +4002,22 @@ class MetaFormAbstract {
4003
4002
  catch { }
4004
4003
  });
4005
4004
  this.dependencyCleanup = [];
4006
- this.lastMetaSignature = '';
4007
4005
  }
4008
- /**
4009
- * Creates missing controls, applies validators, and removes stale controls.
4010
- * Also attaches "clear server error on edit" subscriptions per control.
4011
- */
4012
4006
  ensureControls(controls) {
4013
4007
  (controls ?? []).forEach((control) => {
4014
4008
  const key = control?.configuration?.key;
4015
4009
  if (!key)
4016
4010
  return;
4017
- // Create control once
4018
4011
  if (!this.metaForm.contains(key)) {
4019
4012
  this.metaForm.addControl(key, this.fb.control(getFieldType(control)));
4020
4013
  }
4021
- // Apply sync validators
4022
4014
  this.applyValidators(control);
4023
- // Clear server-side errors when user edits this field
4024
4015
  this.bindClearServerError(key);
4025
- // Disable if metadata says so
4026
4016
  if (control?.disable) {
4027
4017
  this.metaForm.get(key)?.disable({ emitEvent: false });
4028
4018
  }
4029
4019
  });
4030
- // Remove stale controls when metadata changes
4020
+ // remove stale controls when metadata changes
4031
4021
  const allowed = new Set((controls ?? [])
4032
4022
  .map((c) => c?.configuration?.key)
4033
4023
  .filter(Boolean));
@@ -4040,10 +4030,8 @@ class MetaFormAbstract {
4040
4030
  });
4041
4031
  }
4042
4032
  /**
4043
- * Removes only server-originated uniqueness errors when the user changes the value.
4044
- * This prevents a corrected value from staying blocked by an old server error.
4045
- *
4046
- * 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.
4047
4035
  */
4048
4036
  bindClearServerError(key) {
4049
4037
  if (this.clearServerErrorSubs.has(key))
@@ -4059,17 +4047,14 @@ class MetaFormAbstract {
4059
4047
  if (!hasServerUnique)
4060
4048
  return;
4061
4049
  const { uniqueEntry, unique, ...rest } = errors;
4062
- // Clear only server errors, keep other validators
4063
- ctrl.setErrors(Object.keys(rest).length ? rest : null, { emitEvent: false });
4064
- // IMPORTANT: after changing errors, re-run validity WITH EMIT
4050
+ ctrl.setErrors(Object.keys(rest).length ? rest : null, {
4051
+ emitEvent: false,
4052
+ });
4053
+ // Here it's a user interaction, so emitting is safe and desired:
4065
4054
  this.metaForm.updateValueAndValidity({ emitEvent: true });
4066
4055
  });
4067
4056
  this.clearServerErrorSubs.set(key, sub);
4068
4057
  }
4069
- /**
4070
- * Applies synchronous validators based on metadata.
4071
- * Uses emitEvent:false to avoid unnecessary loops.
4072
- */
4073
4058
  applyValidators(control) {
4074
4059
  const key = control?.configuration?.key;
4075
4060
  if (!key)
@@ -4105,12 +4090,9 @@ class MetaFormAbstract {
4105
4090
  ctrl.setValidators(validators);
4106
4091
  ctrl.updateValueAndValidity({ emitEvent: false });
4107
4092
  }
4108
- /**
4109
- * Subscribes MetaFormService to the correct FormGroup instance.
4110
- * Must run after @Input metaForm is available.
4111
- */
4112
4093
  rewireFormSubscription() {
4113
4094
  this.formSub?.unsubscribe();
4095
+ // metaForm is required, but keep fallback safe
4114
4096
  if (!this.metaForm) {
4115
4097
  this.metaForm = this.fb.group({});
4116
4098
  }