@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.
- package/fesm2022/fuentis-phoenix-ui.mjs +85 -107
- package/fesm2022/fuentis-phoenix-ui.mjs.map +1 -1
- package/index.d.ts +47 -67
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
3824
|
-
*
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3907
|
-
|
|
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).
|
|
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
|
-
*
|
|
3925
|
-
*
|
|
3926
|
-
*
|
|
3927
|
-
*
|
|
3928
|
-
*
|
|
3929
|
-
*
|
|
3930
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
3949
|
-
this.
|
|
3950
|
-
//
|
|
3951
|
-
|
|
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
|
-
|
|
3967
|
-
|
|
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
|
-
//
|
|
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
|
|
4045
|
-
*
|
|
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
|
-
//
|
|
4067
|
-
|
|
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
|
}
|