feature-form 0.0.59 → 0.1.0

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.
Files changed (62) hide show
  1. package/MIGRATION.md +153 -0
  2. package/README.md +390 -68
  3. package/dist/cjs/create-form-field.js +1 -0
  4. package/dist/cjs/create-form.js +1 -1
  5. package/dist/cjs/features/dirty.js +1 -0
  6. package/dist/cjs/index.js +1 -1
  7. package/dist/cjs/lib/deep-copy.js +1 -0
  8. package/dist/cjs/lib/standard-schema.js +1 -0
  9. package/dist/cjs/lib/validation-status.js +1 -0
  10. package/dist/esm/create-form-field.js +1 -0
  11. package/dist/esm/create-form.js +1 -1
  12. package/dist/esm/features/dirty.js +1 -0
  13. package/dist/esm/index.js +1 -1
  14. package/dist/esm/lib/deep-copy.js +1 -0
  15. package/dist/esm/lib/standard-schema.js +1 -0
  16. package/dist/esm/lib/validation-status.js +1 -0
  17. package/dist/types/create-form-field.d.ts +18 -0
  18. package/dist/types/create-form.d.ts +48 -29
  19. package/dist/types/features/dirty.d.ts +33 -0
  20. package/dist/types/features/index.d.ts +1 -0
  21. package/dist/types/index.d.ts +2 -4
  22. package/dist/types/lib/deep-copy.d.ts +2 -0
  23. package/dist/types/lib/index.d.ts +3 -0
  24. package/dist/types/lib/standard-schema.d.ts +4 -0
  25. package/dist/types/lib/validation-status.d.ts +3 -0
  26. package/dist/types/types.d.ts +198 -0
  27. package/package.json +26 -11
  28. package/dist/cjs/form-field/create-form-field-validation-context.js +0 -1
  29. package/dist/cjs/form-field/create-form-field.js +0 -1
  30. package/dist/cjs/form-field/create-status.js +0 -1
  31. package/dist/cjs/form-field/is-form-field-status.js +0 -1
  32. package/dist/cjs/form-field/is-form-field.js +0 -1
  33. package/dist/cjs/helper/from-validator.js +0 -1
  34. package/dist/cjs/helper/has-form-changed.js +0 -1
  35. package/dist/cjs/helper/reset-form-submitted.js +0 -1
  36. package/dist/cjs/is-form-with-features.js +0 -1
  37. package/dist/cjs/types/form-field.js +0 -1
  38. package/dist/esm/form-field/create-form-field-validation-context.js +0 -1
  39. package/dist/esm/form-field/create-form-field.js +0 -1
  40. package/dist/esm/form-field/create-status.js +0 -1
  41. package/dist/esm/form-field/is-form-field-status.js +0 -1
  42. package/dist/esm/form-field/is-form-field.js +0 -1
  43. package/dist/esm/helper/from-validator.js +0 -1
  44. package/dist/esm/helper/has-form-changed.js +0 -1
  45. package/dist/esm/helper/reset-form-submitted.js +0 -1
  46. package/dist/esm/is-form-with-features.js +0 -1
  47. package/dist/esm/types/form-field.js +0 -1
  48. package/dist/types/form-field/create-form-field-validation-context.d.ts +0 -2
  49. package/dist/types/form-field/create-form-field.d.ts +0 -7
  50. package/dist/types/form-field/create-status.d.ts +0 -2
  51. package/dist/types/form-field/index.d.ts +0 -4
  52. package/dist/types/form-field/is-form-field-status.d.ts +0 -2
  53. package/dist/types/form-field/is-form-field.d.ts +0 -2
  54. package/dist/types/helper/from-validator.d.ts +0 -3
  55. package/dist/types/helper/has-form-changed.d.ts +0 -2
  56. package/dist/types/helper/index.d.ts +0 -3
  57. package/dist/types/helper/reset-form-submitted.d.ts +0 -2
  58. package/dist/types/is-form-with-features.d.ts +0 -3
  59. package/dist/types/types/features.d.ts +0 -0
  60. package/dist/types/types/form-field.d.ts +0 -71
  61. package/dist/types/types/form.d.ts +0 -49
  62. package/dist/types/types/index.d.ts +0 -2
@@ -1 +1 @@
1
- "use strict";var g=require("@blgc/utils"),y=require("feature-state"),k=require("./form-field/create-form-field.js"),E=require("./form-field/is-form-field.js"),h=require("./types/form-field.js"),P=Object.defineProperty,M=Object.getOwnPropertySymbols,C=Object.prototype.hasOwnProperty,D=Object.prototype.propertyIsEnumerable,j=(s,l,i)=>l in s?P(s,l,{enumerable:!0,configurable:!0,writable:!0,value:i}):s[l]=i,w=(s,l)=>{for(var i in l||(l={}))C.call(l,i)&&j(s,i,l[i]);if(M)for(var i of M(l))D.call(l,i)&&j(s,i,l[i]);return s},p=(s,l,i)=>new Promise((V,v)=>{var F=o=>{try{u(i.next(o))}catch(e){v(e)}},S=o=>{try{u(i.throw(o))}catch(e){v(e)}},u=o=>o.done?V(o.value):Promise.resolve(o.value).then(F,S);u((i=i.apply(s,l)).next())});function I(s){const{fields:l,collectErrorMode:i="firstError",disabled:V=!1,validateMode:v=g.bitwiseFlag(h.FormFieldValidateMode.OnSubmit),reValidateMode:F=g.bitwiseFlag(h.FormFieldReValidateMode.OnBlur,h.FormFieldReValidateMode.OnSubmit),onValidSubmit:S,onInvalidSubmit:u,notifyOnStatusChange:o=!0}=s;return g.withNew({_features:[],_config:{disabled:V},_validSubmitCallbacks:S!=null?[S]:[],_invalidSubmitCallbacks:u!=null?[u]:[],fields:Object.fromEntries(Object.entries(l).map(([e,t])=>{var r,b,m,d;return[e,E.isFormField(t)?t:k.createFormField(t.defaultValue,{key:e,validator:t.validator,collectErrorMode:(r=t.collectErrorMode)!=null?r:i,validateMode:(b=t.validateMode)!=null?b:v,reValidateMode:(m=t.reValidateMode)!=null?m:F,editable:(d=t.editable)!=null?d:!0,notifyOnStatusChange:o})]})),isValid:y.createState(!1),isValidating:y.createState(!1),isSubmitted:y.createState(!1),isSubmitting:y.createState(!1),_new(){for(const e of Object.values(this.fields))e.status.listen(()=>p(this,null,function*(){yield this._revalidate(!0)}),{key:"form_revalidate"})},_revalidate(e=!1){return p(this,null,function*(){const t=Object.values(this.fields);return e||(this.isValidating.set(!0),yield Promise.all(t.map(r=>r.validate())),this.isValidating.set(!1)),this.isValid.set(t.every(r=>r.isValid())),this.isValid.get()})},submit(){return p(this,arguments,function*(e={}){const{context:t,assignToInitial:r=!1,onInvalidSubmit:b,onValidSubmit:m,postSubmitCallback:d}=e;this.isSubmitting.set(!0);const _=[];for(const a of Object.values(this.fields))a.isSubmitting.set(!0),(a.isSubmitted.get()&&a._config.reValidateMode.has(h.FormFieldReValidateMode.OnSubmit)||!a.isSubmitted.get()&&a._config.validateMode.has(h.FormFieldValidateMode.OnSubmit))&&_.push(a.validate());yield Promise.all(_),this._revalidate(!0);const c=this.getValidData(),f=[];if(c!=null){for(const a of this._validSubmitCallbacks)f.push(a(c,t));typeof m=="function"&&f.push(m(c,t))}else{const a=this.getErrors();for(const n of this._invalidSubmitCallbacks)f.push(n(a,t));typeof b=="function"&&f.push(b(a,t))}let O=null;d!=null?O=(yield Promise.all(f)).reduce((a,n)=>n!=null&&typeof n=="object"?w(w({},a),n):a,{}):yield Promise.all(f);for(const[a,n]of Object.entries(this.fields))c!=null&&Object.prototype.hasOwnProperty.call(c,a)&&r&&(n._intialValue=g.deepCopy(c[a])),n.isSubmitted.set(!0),n.isSubmitting.set(!1);return this.isSubmitted.set(!0),this.isSubmitting.set(!1),d==null||d(this,O!=null?O:{}),this.isValid.get()})},validate(){return p(this,null,function*(){return this._revalidate(!1)})},getField(e){return this.fields[e]},getValidData(){if(!this.isValid.get())return null;const e={};for(const[t,r]of Object.entries(this.fields))e[t]=r.get();return e},getErrors(){const e={};for(const[t,r]of Object.entries(this.fields))switch(r.status._v.type){case"INVALID":e[t]=r.status._v.errors;break;case"UNVALIDATED":e[t]=[{code:"unvalidated",message:`${t.toString()} was not yet validated!`,path:t}];break}return e},reset(){for(const e of Object.values(this.fields))e.reset();this.isSubmitted.set(!1),this._revalidate(!0)}})}exports.createForm=I;
1
+ "use strict";var k=require("feature-core"),h=require("feature-state"),V=require("./create-form-field.js"),j=require("./lib/deep-copy.js"),I=require("./lib/standard-schema.js"),q=require("./lib/validation-status.js"),P=Object.defineProperty,w=Object.defineProperties,D=Object.getOwnPropertyDescriptors,O=Object.getOwnPropertySymbols,x=Object.prototype.hasOwnProperty,R=Object.prototype.propertyIsEnumerable,F=(t,i,e)=>i in t?P(t,i,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[i]=e,M=(t,i)=>{for(var e in i||(i={}))x.call(i,e)&&F(t,e,i[e]);if(O)for(var e of O(i))R.call(i,e)&&F(t,e,i[e]);return t},C=(t,i)=>w(t,D(i)),m=(t,i,e)=>new Promise((l,n)=>{var c=o=>{try{d(e.next(o))}catch(u){n(u)}},s=o=>{try{d(e.throw(o))}catch(u){n(u)}},d=o=>o.done?l(o.value):Promise.resolve(o.value).then(c,s);d((e=e.apply(t,i)).next())});function T(t){const{fields:i,validator:e,validateOn:l=["submit"],revalidateOn:n=["submit","change"],collectErrorMode:c="firstError",onInvalidSubmit:s,onValidSubmit:d}=t,o=e==null?{type:"valid"}:{type:"unvalidated"},u=k.createFeatureHost({_validation:e==null?void 0:{validator:e,config:{validateOn:l,revalidateOn:n,collectErrorMode:c}},_validationRunId:0,_formValidatorStatus:o,_callbacks:{invalidSubmit:s==null?[]:[s],validSubmit:d==null?[]:[d]},status:h.createState(o).with(h.isEqualFeature(q.areValidationStatusesEqual)),isValidating:h.createState(!1),isSubmitted:h.createState(!1),isSubmitting:h.createState(!1),fields:Object.fromEntries(Object.entries(i).map(([a,r])=>{var b,v,p;return[a,V.isFormField(r)?r:V.createFormField(r.defaultValue,{key:a,validator:r.validator,collectErrorMode:(b=r.collectErrorMode)!=null?b:c,revalidateOn:(v=r.revalidateOn)!=null?v:n,validateOn:(p=r.validateOn)!=null?p:l})]})),onValidSubmit(a){return this._callbacks.validSubmit.push(a),()=>{const r=this._callbacks.validSubmit.indexOf(a);r!==-1&&this._callbacks.validSubmit.splice(r,1)}},onInvalidSubmit(a){return this._callbacks.invalidSubmit.push(a),()=>{const r=this._callbacks.invalidSubmit.indexOf(a);r!==-1&&this._callbacks.invalidSubmit.splice(r,1)}},submit(){return m(this,arguments,function*(a={}){this.isSubmitting.set(!0);try{yield S(this,{shouldValidateField:v=>v._validation!=null&&(v.isSubmitted.get()?v._validation.config.revalidateOn.includes("submit"):v._validation.config.validateOn.includes("submit")),shouldValidateForm:this._validation!=null&&(this.isSubmitted.get()?this._validation.config.revalidateOn.includes("submit"):this._validation.config.validateOn.includes("submit"))});for(const v of f(this.fields))v.isSubmitted.set(!0);this.isSubmitted.set(!0);const r=this.getValidData();if(r!=null){if(a.updateDefaultValues)for(const[v,p]of _(this.fields))p.defaultValue=j.deepCopy(r[v]);return yield E(a.onValidSubmit!=null?[...this._callbacks.validSubmit,a.onValidSubmit]:this._callbacks.validSubmit,r,a.context),!0}const b=this.getErrors();return yield E(a.onInvalidSubmit!=null?[...this._callbacks.invalidSubmit,a.onInvalidSubmit]:this._callbacks.invalidSubmit,b,a.context),!1}finally{this.isSubmitting.set(!1)}})},validate(){return m(this,null,function*(){return S(this,{shouldValidateField:a=>a._validation!=null,shouldValidateForm:this._validation!=null})})},reset(){for(const a of f(this.fields))a.reset();this._validationRunId++,y(this,this._validation==null?{type:"valid"}:{type:"unvalidated"}),this.status.set(g(this)),this.isValidating.set(!1),this.isSubmitted.set(!1),this.isSubmitting.set(!1)},getField(a){return this.fields[a]},getData(){return Object.fromEntries(_(this.fields).map(([a,r])=>[a,r.get()]))},getValidData(){return this.status.get().type!=="valid"?null:this.getData()},getErrors(){return{fields:G(this.fields),form:this._formValidatorStatus.type==="invalid"?this._formValidatorStatus.errors:[]}}});return y(u,u._formValidatorStatus),u.status.set(g(u)),B(u),u}function B(t){for(const i of f(t.fields))i.status.listen(()=>{t.status.set(g(t))}),i.onBlur(({wasTouched:e})=>{t._validation!=null&&(t.isSubmitted.get()?t._validation.config.revalidateOn.includes("blur"):t._validation.config.validateOn.includes("blur")||!e&&t._validation.config.validateOn.includes("touched"))&&S(t,{shouldValidateField:()=>!1,shouldValidateForm:!0})}),i.listen(({source:e})=>{e===V.formFieldResetSourceKey||!(t._validation!=null&&(t.isSubmitted.get()?t._validation.config.revalidateOn.includes("change"):t._validation.config.validateOn.includes("change")||t._validation.config.validateOn.includes("touched")&&i.isTouched.get()))||S(t,{shouldValidateField:()=>!1,shouldValidateForm:!0})})}function S(t,i){return m(this,null,function*(){const{shouldValidateField:e,shouldValidateForm:l}=i,n=f(t.fields).filter(e).map(o=>o.validate()),c=n.length>0||l,s=c?++t._validationRunId:null;c&&t.isValidating.set(!0);try{yield Promise.all([...n,...l&&s!=null?[K(t,s)]:[]])}finally{s!=null&&s===t._validationRunId&&t.isValidating.set(!1)}const d=g(t);return t.status.set(d),d.type==="valid"})}function K(t,i){return m(this,null,function*(){if(t._validation==null){y(t,{type:"valid"});return}let e={type:"valid"};try{e=yield I.validateStandardSchema(t._validation.validator,t.getData(),t._validation.config.collectErrorMode)}catch(l){e={type:"invalid",errors:[{message:l instanceof Error?l.message:String(l),path:["form"]}]}}i===t._validationRunId&&y(t,e)})}function g(t){const i=[];let e=!1;for(const l of f(t.fields)){const n=l.status.get();n.type==="invalid"?i.push(...n.errors):n.type==="unvalidated"&&(e=!0)}return t._formValidatorStatus.type==="invalid"?i.push(...t._formValidatorStatus.errors):t._formValidatorStatus.type==="unvalidated"&&(e=!0),i.length>0?{type:"invalid",errors:i}:e?{type:"unvalidated"}:{type:"valid"}}function y(t,i){var e,l;if(i.type!=="invalid"){t._formValidatorStatus=i;for(const s of f(t.fields))s._applyFormValidatorErrors([]);return}const n={},c=[];for(const s of i.errors){const d=s.path;if(d==null){c.push(s);continue}const o=d[0];if(typeof o!="string"||!(o in t.fields)){c.push(s);continue}const u=d.slice(1);n[o]=[...(e=n[o])!=null?e:[],C(M({},s),{path:u.length>0?u:void 0})]}t._formValidatorStatus=c.length>0?{type:"invalid",errors:c}:{type:"valid"};for(const[s,d]of _(t.fields))d._applyFormValidatorErrors((l=n[s])!=null?l:[])}function G(t){const i={};for(const[e,l]of _(t)){const n=l.status.get();n.type==="invalid"&&(i[e]=n.errors)}return i}function E(t,i,e){return m(this,null,function*(){yield Promise.all(t.map(l=>l(i,e)))})}function f(t){return Object.values(t)}function _(t){return Object.entries(t)}exports.createForm=T;
@@ -0,0 +1 @@
1
+ "use strict";var p=require("feature-core"),l=require("feature-state"),j=require("../lib/deep-copy.js"),O=(e,t,r)=>new Promise((u,i)=>{var s=o=>{try{n(r.next(o))}catch(c){i(c)}},a=o=>{try{n(r.throw(o))}catch(c){i(c)}},n=o=>o.done?u(o.value):Promise.resolve(o.value).then(s,a);n((r=r.apply(e,t)).next())});function m(e={}){const{isEqual:t=f}=e;return p.defineFeature({key:"dirty",overrides:["submit"],install(r){const u=l.createState(y(r,t)),i=l.createState(d(u.get())),s=()=>{const n=y(r,t);g(u.get(),n)||u.set(n),i.set(d(n))},a=r.submit.bind(r);for(const n of v(r.fields))n.listen(s);return{isDirty:i,dirtyFields:u,resetDirty(){for(const n of v(r.fields))n.defaultValue=j.deepCopy(n.get());s()},submit(n){return O(this,null,function*(){try{return yield a(n)}finally{s()}})}}}})}function y(e,t){return Object.fromEntries(h(e.fields).map(([r,u])=>[r,!t(u.get(),u.defaultValue)]))}function d(e){return Object.values(e).some(t=>t)}function g(e,t){for(const r of Object.keys(t))if(e[r]!==t[r])return!1;return!0}function f(e,t){if(Object.is(e,t))return!0;if(Array.isArray(e)&&Array.isArray(t))return e.length!==t.length?!1:e.every((i,s)=>f(i,t[s]));if(!b(e)||!b(t))return!1;const r=Object.keys(e),u=Object.keys(t);return r.length!==u.length?!1:r.every(i=>i in t&&f(e[i],t[i]))}function b(e){return typeof e=="object"&&e!=null&&Object.getPrototypeOf(e)===Object.prototype}function v(e){return Object.values(e)}function h(e){return Object.entries(e)}exports.dirtyFeature=m;
package/dist/cjs/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("@blgc/utils"),t=require("./create-form.js"),i=require("./form-field/create-form-field.js"),a=require("./form-field/create-status.js"),F=require("./form-field/is-form-field.js"),o=require("./form-field/is-form-field-status.js"),m=require("./helper/from-validator.js"),d=require("./helper/has-form-changed.js"),u=require("./helper/reset-form-submitted.js"),s=require("./is-form-with-features.js"),r=require("./types/form-field.js");Object.defineProperty(exports,"BitwiseFlag",{enumerable:!0,get:function(){return e.BitwiseFlag}}),Object.defineProperty(exports,"bitwiseFlag",{enumerable:!0,get:function(){return e.bitwiseFlag}}),exports.createForm=t.createForm,exports.createFormField=i.createFormField,exports.createStatus=a.createStatus,exports.isFormField=F.isFormField,exports.isFormFieldStatus=o.isFormFieldStatus,exports.fromValidator=m.fromValidator,exports.hasFormChanged=d.hasFormChanged,exports.resetFormSubmitted=u.resetFormSubmitted,exports.isFormWithFeatures=s.isFormWithFeatures,exports.FormFieldReValidateMode=r.FormFieldReValidateMode,exports.FormFieldValidateMode=r.FormFieldValidateMode;
1
+ "use strict";var r=require("./create-form.js"),F=require("./features/dirty.js"),e=require("./create-form-field.js");exports.createForm=r.createForm,exports.dirtyFeature=F.dirtyFeature,exports.createFormField=e.createFormField,exports.formFieldResetSourceKey=e.formFieldResetSourceKey,exports.isFormField=e.isFormField;
@@ -0,0 +1 @@
1
+ "use strict";function o(e){if(typeof e!="object"||e==null)return e;if(Array.isArray(e))return e.map(r=>o(r));if(Object.getPrototypeOf(e)!==Object.prototype)return e;const t={};for(const r of Object.keys(e))t[r]=o(e[r]);return t}exports.deepCopy=o;
@@ -0,0 +1 @@
1
+ "use strict";var o=(e,t,n)=>new Promise((r,l)=>{var c=a=>{try{i(n.next(a))}catch(s){l(s)}},u=a=>{try{i(n.throw(a))}catch(s){l(s)}},i=a=>a.done?r(a.value):Promise.resolve(a.value).then(c,u);i((n=n.apply(e,t)).next())});function h(e,t,n){return o(this,null,function*(){const{issues:r}=yield e["~standard"].validate(t);return r!=null&&r.length?{type:"invalid",errors:(n==="firstError"?r.slice(0,1):r).map(l=>({message:l.message,path:d(l.path)}))}:{type:"valid"}})}function d(e){if(e!=null&&e.length)return e.map(t=>typeof t=="object"&&"key"in t?t.key:t)}exports.validateStandardSchema=h;
@@ -0,0 +1 @@
1
+ "use strict";function s(t,e){return t.type!==e.type?!1:t.type!=="invalid"||e.type!=="invalid"?!0:i(t.errors,e.errors)}function i(t,e){return t.length!==e.length?!1:t.every((n,r)=>{var l,u;return n.message===((l=e[r])==null?void 0:l.message)&&a(n.path,(u=e[r])==null?void 0:u.path)})}function a(t,e){return t==null||e==null?t==null&&e==null:t.length!==e.length?!1:t.every((n,r)=>Object.is(n,e[r]))}exports.areValidationStatusesEqual=s;
@@ -0,0 +1 @@
1
+ import{hasFeature as f,defineFeature as _}from"feature-core";import{createState as d,isEqualFeature as m}from"feature-state";import{deepCopy as v}from"./lib/deep-copy.js";import{validateStandardSchema as g}from"./lib/standard-schema.js";import{areValidationStatusesEqual as p}from"./lib/validation-status.js";var y=(t,e,s)=>new Promise((n,r)=>{var i=o=>{try{l(s.next(o))}catch(c){r(c)}},a=o=>{try{l(s.throw(o))}catch(c){r(c)}},l=o=>o.done?n(o.value):Promise.resolve(o.value).then(i,a);l((s=s.apply(t,e)).next())});function V(t,e){const{key:s,validator:n,validateOn:r=["submit"],revalidateOn:i=["submit","change"],collectErrorMode:a="firstError"}=e,l=d(t).with(S({key:s,validation:n==null?void 0:{validator:n,config:{validateOn:r,revalidateOn:i,collectErrorMode:a}}}));return E(l),l}function b(t){return f(t,"form-field")}function S(t){const{key:e,validation:s}=t;return _({key:"form-field",install(n){const r=s==null?{type:"valid"}:{type:"unvalidated"};return{_validation:s,_validationRunId:0,_fieldValidatorStatus:r,_formValidatorErrors:[],_callbacks:{blur:[]},key:e,defaultValue:v(n._v),isTouched:d(!1),isSubmitted:d(!1),isValidating:d(!1),status:d(r).with(m(p)),_applyFormValidatorErrors(i){this._formValidatorErrors=i,this.status.set(u(this))},validate(){return y(this,null,function*(){if(this._validation==null)return this._fieldValidatorStatus={type:"valid"},this.status.set(u(this)),this.status.get().type==="valid";const i=++this._validationRunId;let a={type:"valid"};this.isValidating.set(!0);try{a=yield g(this._validation.validator,this.get(),this._validation.config.collectErrorMode)}catch(l){a={type:"invalid",errors:[{message:l instanceof Error?l.message:String(l)}]}}finally{i===this._validationRunId&&this.isValidating.set(!1)}return i!==this._validationRunId?this.status.get().type==="valid":(this._fieldValidatorStatus=a,this.status.set(u(this)),this.status.get().type==="valid")})},onBlur(i){return this._callbacks.blur.push(i),()=>{const a=this._callbacks.blur.indexOf(i);a!==-1&&this._callbacks.blur.splice(a,1)}},blur(){const i=this.isTouched.get();this._validation!=null&&(this.isSubmitted.get()?this._validation.config.revalidateOn.includes("blur"):this._validation.config.validateOn.includes("blur")||this._validation.config.validateOn.includes("touched")&&!i)&&this.validate(),this.isTouched.set(!0);for(const a of this._callbacks.blur)a({wasTouched:i})},reset(){this.set(v(this.defaultValue),{listenerContext:{source:h}}),this._validationRunId++,this.isTouched.set(!1),this.isSubmitted.set(!1),this.isValidating.set(!1),this._formValidatorErrors=[],this._fieldValidatorStatus=this._validation==null?{type:"valid"}:{type:"unvalidated"},this.status.set(u(this))}}}})}const h="formFieldReset";function E(t){t.listen(({source:e})=>{e!==h&&t._validation!=null&&(t.isSubmitted.get()?t._validation.config.revalidateOn.includes("change"):t._validation.config.validateOn.includes("change")||t._validation.config.validateOn.includes("touched")&&t.isTouched.get())&&t.validate()})}function u(t){const e=t._fieldValidatorStatus.type==="invalid"?[...t._fieldValidatorStatus.errors,...t._formValidatorErrors]:t._formValidatorErrors;return e.length>0?{type:"invalid",errors:e}:t._fieldValidatorStatus}export{V as createFormField,h as formFieldResetSourceKey,b as isFormField};
@@ -1 +1 @@
1
- import{bitwiseFlag as j,withNew as E,deepCopy as P}from"@blgc/utils";import{createState as p}from"feature-state";import{createFormField as C}from"./form-field/create-form-field.js";import{isFormField as D}from"./form-field/is-form-field.js";import{FormFieldValidateMode as w,FormFieldReValidateMode as V}from"./types/form-field.js";var I=Object.defineProperty,M=Object.getOwnPropertySymbols,x=Object.prototype.hasOwnProperty,A=Object.prototype.propertyIsEnumerable,F=(r,s,i)=>s in r?I(r,s,{enumerable:!0,configurable:!0,writable:!0,value:i}):r[s]=i,k=(r,s)=>{for(var i in s||(s={}))x.call(s,i)&&F(r,i,s[i]);if(M)for(var i of M(s))A.call(s,i)&&F(r,i,s[i]);return r},g=(r,s,i)=>new Promise((y,h)=>{var S=o=>{try{u(i.next(o))}catch(t){h(t)}},v=o=>{try{u(i.throw(o))}catch(t){h(t)}},u=o=>o.done?y(o.value):Promise.resolve(o.value).then(S,v);u((i=i.apply(r,s)).next())});function N(r){const{fields:s,collectErrorMode:i="firstError",disabled:y=!1,validateMode:h=j(w.OnSubmit),reValidateMode:S=j(V.OnBlur,V.OnSubmit),onValidSubmit:v,onInvalidSubmit:u,notifyOnStatusChange:o=!0}=r;return E({_features:[],_config:{disabled:y},_validSubmitCallbacks:v!=null?[v]:[],_invalidSubmitCallbacks:u!=null?[u]:[],fields:Object.fromEntries(Object.entries(s).map(([t,e])=>{var a,b,m,d;return[t,D(e)?e:C(e.defaultValue,{key:t,validator:e.validator,collectErrorMode:(a=e.collectErrorMode)!=null?a:i,validateMode:(b=e.validateMode)!=null?b:h,reValidateMode:(m=e.reValidateMode)!=null?m:S,editable:(d=e.editable)!=null?d:!0,notifyOnStatusChange:o})]})),isValid:p(!1),isValidating:p(!1),isSubmitted:p(!1),isSubmitting:p(!1),_new(){for(const t of Object.values(this.fields))t.status.listen(()=>g(this,null,function*(){yield this._revalidate(!0)}),{key:"form_revalidate"})},_revalidate(t=!1){return g(this,null,function*(){const e=Object.values(this.fields);return t||(this.isValidating.set(!0),yield Promise.all(e.map(a=>a.validate())),this.isValidating.set(!1)),this.isValid.set(e.every(a=>a.isValid())),this.isValid.get()})},submit(){return g(this,arguments,function*(t={}){const{context:e,assignToInitial:a=!1,onInvalidSubmit:b,onValidSubmit:m,postSubmitCallback:d}=t;this.isSubmitting.set(!0);const _=[];for(const l of Object.values(this.fields))l.isSubmitting.set(!0),(l.isSubmitted.get()&&l._config.reValidateMode.has(V.OnSubmit)||!l.isSubmitted.get()&&l._config.validateMode.has(w.OnSubmit))&&_.push(l.validate());yield Promise.all(_),this._revalidate(!0);const f=this.getValidData(),c=[];if(f!=null){for(const l of this._validSubmitCallbacks)c.push(l(f,e));typeof m=="function"&&c.push(m(f,e))}else{const l=this.getErrors();for(const n of this._invalidSubmitCallbacks)c.push(n(l,e));typeof b=="function"&&c.push(b(l,e))}let O=null;d!=null?O=(yield Promise.all(c)).reduce((l,n)=>n!=null&&typeof n=="object"?k(k({},l),n):l,{}):yield Promise.all(c);for(const[l,n]of Object.entries(this.fields))f!=null&&Object.prototype.hasOwnProperty.call(f,l)&&a&&(n._intialValue=P(f[l])),n.isSubmitted.set(!0),n.isSubmitting.set(!1);return this.isSubmitted.set(!0),this.isSubmitting.set(!1),d==null||d(this,O!=null?O:{}),this.isValid.get()})},validate(){return g(this,null,function*(){return this._revalidate(!1)})},getField(t){return this.fields[t]},getValidData(){if(!this.isValid.get())return null;const t={};for(const[e,a]of Object.entries(this.fields))t[e]=a.get();return t},getErrors(){const t={};for(const[e,a]of Object.entries(this.fields))switch(a.status._v.type){case"INVALID":t[e]=a.status._v.errors;break;case"UNVALIDATED":t[e]=[{code:"unvalidated",message:`${e.toString()} was not yet validated!`,path:e}];break}return t},reset(){for(const t of Object.values(this.fields))t.reset();this.isSubmitted.set(!1),this._revalidate(!0)}})}export{N as createForm};
1
+ import{createFeatureHost as E}from"feature-core";import{createState as p,isEqualFeature as k}from"feature-state";import{isFormField as j,createFormField as I,formFieldResetSourceKey as P}from"./create-form-field.js";import{deepCopy as w}from"./lib/deep-copy.js";import{validateStandardSchema as x}from"./lib/standard-schema.js";import{areValidationStatusesEqual as D}from"./lib/validation-status.js";var R=Object.defineProperty,M=Object.defineProperties,T=Object.getOwnPropertyDescriptors,V=Object.getOwnPropertySymbols,q=Object.prototype.hasOwnProperty,B=Object.prototype.propertyIsEnumerable,O=(t,i,a)=>i in t?R(t,i,{enumerable:!0,configurable:!0,writable:!0,value:a}):t[i]=a,C=(t,i)=>{for(var a in i||(i={}))q.call(i,a)&&O(t,a,i[a]);if(V)for(var a of V(i))B.call(i,a)&&O(t,a,i[a]);return t},K=(t,i)=>M(t,T(i)),m=(t,i,a)=>new Promise((l,n)=>{var c=o=>{try{d(a.next(o))}catch(u){n(u)}},r=o=>{try{d(a.throw(o))}catch(u){n(u)}},d=o=>o.done?l(o.value):Promise.resolve(o.value).then(c,r);d((a=a.apply(t,i)).next())});function G(t){const{fields:i,validator:a,validateOn:l=["submit"],revalidateOn:n=["submit","change"],collectErrorMode:c="firstError",onInvalidSubmit:r,onValidSubmit:d}=t,o=a==null?{type:"valid"}:{type:"unvalidated"},u=E({_validation:a==null?void 0:{validator:a,config:{validateOn:l,revalidateOn:n,collectErrorMode:c}},_validationRunId:0,_formValidatorStatus:o,_callbacks:{invalidSubmit:r==null?[]:[r],validSubmit:d==null?[]:[d]},status:p(o).with(k(D)),isValidating:p(!1),isSubmitted:p(!1),isSubmitting:p(!1),fields:Object.fromEntries(Object.entries(i).map(([e,s])=>{var h,f,b;return[e,j(s)?s:I(s.defaultValue,{key:e,validator:s.validator,collectErrorMode:(h=s.collectErrorMode)!=null?h:c,revalidateOn:(f=s.revalidateOn)!=null?f:n,validateOn:(b=s.validateOn)!=null?b:l})]})),onValidSubmit(e){return this._callbacks.validSubmit.push(e),()=>{const s=this._callbacks.validSubmit.indexOf(e);s!==-1&&this._callbacks.validSubmit.splice(s,1)}},onInvalidSubmit(e){return this._callbacks.invalidSubmit.push(e),()=>{const s=this._callbacks.invalidSubmit.indexOf(e);s!==-1&&this._callbacks.invalidSubmit.splice(s,1)}},submit(){return m(this,arguments,function*(e={}){this.isSubmitting.set(!0);try{yield g(this,{shouldValidateField:f=>f._validation!=null&&(f.isSubmitted.get()?f._validation.config.revalidateOn.includes("submit"):f._validation.config.validateOn.includes("submit")),shouldValidateForm:this._validation!=null&&(this.isSubmitted.get()?this._validation.config.revalidateOn.includes("submit"):this._validation.config.validateOn.includes("submit"))});for(const f of v(this.fields))f.isSubmitted.set(!0);this.isSubmitted.set(!0);const s=this.getValidData();if(s!=null){if(e.updateDefaultValues)for(const[f,b]of _(this.fields))b.defaultValue=w(s[f]);return yield F(e.onValidSubmit!=null?[...this._callbacks.validSubmit,e.onValidSubmit]:this._callbacks.validSubmit,s,e.context),!0}const h=this.getErrors();return yield F(e.onInvalidSubmit!=null?[...this._callbacks.invalidSubmit,e.onInvalidSubmit]:this._callbacks.invalidSubmit,h,e.context),!1}finally{this.isSubmitting.set(!1)}})},validate(){return m(this,null,function*(){return g(this,{shouldValidateField:e=>e._validation!=null,shouldValidateForm:this._validation!=null})})},reset(){for(const e of v(this.fields))e.reset();this._validationRunId++,S(this,this._validation==null?{type:"valid"}:{type:"unvalidated"}),this.status.set(y(this)),this.isValidating.set(!1),this.isSubmitted.set(!1),this.isSubmitting.set(!1)},getField(e){return this.fields[e]},getData(){return Object.fromEntries(_(this.fields).map(([e,s])=>[e,s.get()]))},getValidData(){return this.status.get().type!=="valid"?null:this.getData()},getErrors(){return{fields:z(this.fields),form:this._formValidatorStatus.type==="invalid"?this._formValidatorStatus.errors:[]}}});return S(u,u._formValidatorStatus),u.status.set(y(u)),H(u),u}function H(t){for(const i of v(t.fields))i.status.listen(()=>{t.status.set(y(t))}),i.onBlur(({wasTouched:a})=>{t._validation!=null&&(t.isSubmitted.get()?t._validation.config.revalidateOn.includes("blur"):t._validation.config.validateOn.includes("blur")||!a&&t._validation.config.validateOn.includes("touched"))&&g(t,{shouldValidateField:()=>!1,shouldValidateForm:!0})}),i.listen(({source:a})=>{a===P||!(t._validation!=null&&(t.isSubmitted.get()?t._validation.config.revalidateOn.includes("change"):t._validation.config.validateOn.includes("change")||t._validation.config.validateOn.includes("touched")&&i.isTouched.get()))||g(t,{shouldValidateField:()=>!1,shouldValidateForm:!0})})}function g(t,i){return m(this,null,function*(){const{shouldValidateField:a,shouldValidateForm:l}=i,n=v(t.fields).filter(a).map(o=>o.validate()),c=n.length>0||l,r=c?++t._validationRunId:null;c&&t.isValidating.set(!0);try{yield Promise.all([...n,...l&&r!=null?[N(t,r)]:[]])}finally{r!=null&&r===t._validationRunId&&t.isValidating.set(!1)}const d=y(t);return t.status.set(d),d.type==="valid"})}function N(t,i){return m(this,null,function*(){if(t._validation==null){S(t,{type:"valid"});return}let a={type:"valid"};try{a=yield x(t._validation.validator,t.getData(),t._validation.config.collectErrorMode)}catch(l){a={type:"invalid",errors:[{message:l instanceof Error?l.message:String(l),path:["form"]}]}}i===t._validationRunId&&S(t,a)})}function y(t){const i=[];let a=!1;for(const l of v(t.fields)){const n=l.status.get();n.type==="invalid"?i.push(...n.errors):n.type==="unvalidated"&&(a=!0)}return t._formValidatorStatus.type==="invalid"?i.push(...t._formValidatorStatus.errors):t._formValidatorStatus.type==="unvalidated"&&(a=!0),i.length>0?{type:"invalid",errors:i}:a?{type:"unvalidated"}:{type:"valid"}}function S(t,i){var a,l;if(i.type!=="invalid"){t._formValidatorStatus=i;for(const r of v(t.fields))r._applyFormValidatorErrors([]);return}const n={},c=[];for(const r of i.errors){const d=r.path;if(d==null){c.push(r);continue}const o=d[0];if(typeof o!="string"||!(o in t.fields)){c.push(r);continue}const u=d.slice(1);n[o]=[...(a=n[o])!=null?a:[],K(C({},r),{path:u.length>0?u:void 0})]}t._formValidatorStatus=c.length>0?{type:"invalid",errors:c}:{type:"valid"};for(const[r,d]of _(t.fields))d._applyFormValidatorErrors((l=n[r])!=null?l:[])}function z(t){const i={};for(const[a,l]of _(t)){const n=l.status.get();n.type==="invalid"&&(i[a]=n.errors)}return i}function F(t,i,a){return m(this,null,function*(){yield Promise.all(t.map(l=>l(i,a)))})}function v(t){return Object.values(t)}function _(t){return Object.entries(t)}export{G as createForm};
@@ -0,0 +1 @@
1
+ import{defineFeature as p}from"feature-core";import{createState as a}from"feature-state";import{deepCopy as j}from"../lib/deep-copy.js";var O=(e,t,r)=>new Promise((o,i)=>{var u=s=>{try{n(r.next(s))}catch(l){i(l)}},f=s=>{try{n(r.throw(s))}catch(l){i(l)}},n=s=>s.done?o(s.value):Promise.resolve(s.value).then(u,f);n((r=r.apply(e,t)).next())});function v(e={}){const{isEqual:t=c}=e;return p({key:"dirty",overrides:["submit"],install(r){const o=a(y(r,t)),i=a(b(o.get())),u=()=>{const n=y(r,t);g(o.get(),n)||o.set(n),i.set(b(n))},f=r.submit.bind(r);for(const n of d(r.fields))n.listen(u);return{isDirty:i,dirtyFields:o,resetDirty(){for(const n of d(r.fields))n.defaultValue=j(n.get());u()},submit(n){return O(this,null,function*(){try{return yield f(n)}finally{u()}})}}}})}function y(e,t){return Object.fromEntries(h(e.fields).map(([r,o])=>[r,!t(o.get(),o.defaultValue)]))}function b(e){return Object.values(e).some(t=>t)}function g(e,t){for(const r of Object.keys(t))if(e[r]!==t[r])return!1;return!0}function c(e,t){if(Object.is(e,t))return!0;if(Array.isArray(e)&&Array.isArray(t))return e.length!==t.length?!1:e.every((i,u)=>c(i,t[u]));if(!m(e)||!m(t))return!1;const r=Object.keys(e),o=Object.keys(t);return r.length!==o.length?!1:r.every(i=>i in t&&c(e[i],t[i]))}function m(e){return typeof e=="object"&&e!=null&&Object.getPrototypeOf(e)===Object.prototype}function d(e){return Object.values(e)}function h(e){return Object.entries(e)}export{v as dirtyFeature};
package/dist/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{BitwiseFlag as o,bitwiseFlag as t}from"@blgc/utils";import{createForm as i}from"./create-form.js";import{createFormField as a}from"./form-field/create-form-field.js";import{createStatus as f}from"./form-field/create-status.js";import{isFormField as x}from"./form-field/is-form-field.js";import{isFormFieldStatus as s}from"./form-field/is-form-field-status.js";import{fromValidator as c}from"./helper/from-validator.js";import{hasFormChanged as h}from"./helper/has-form-changed.js";import{resetFormSubmitted as V}from"./helper/reset-form-submitted.js";import{isFormWithFeatures as w}from"./is-form-with-features.js";import{FormFieldReValidateMode as n,FormFieldValidateMode as B}from"./types/form-field.js";export{o as BitwiseFlag,n as FormFieldReValidateMode,B as FormFieldValidateMode,t as bitwiseFlag,i as createForm,a as createFormField,f as createStatus,c as fromValidator,h as hasFormChanged,x as isFormField,s as isFormFieldStatus,w as isFormWithFeatures,V as resetFormSubmitted};
1
+ import{createForm as o}from"./create-form.js";import{dirtyFeature as m}from"./features/dirty.js";import{createFormField as i,formFieldResetSourceKey as d,isFormField as f}from"./create-form-field.js";export{o as createForm,i as createFormField,m as dirtyFeature,d as formFieldResetSourceKey,f as isFormField};
@@ -0,0 +1 @@
1
+ function o(e){if(typeof e!="object"||e==null)return e;if(Array.isArray(e))return e.map(r=>o(r));if(Object.getPrototypeOf(e)!==Object.prototype)return e;const t={};for(const r of Object.keys(e))t[r]=o(e[r]);return t}export{o as deepCopy};
@@ -0,0 +1 @@
1
+ var c=(t,e,a)=>new Promise((r,l)=>{var o=n=>{try{i(a.next(n))}catch(s){l(s)}},u=n=>{try{i(a.throw(n))}catch(s){l(s)}},i=n=>n.done?r(n.value):Promise.resolve(n.value).then(o,u);i((a=a.apply(t,e)).next())});function h(t,e,a){return c(this,null,function*(){const{issues:r}=yield t["~standard"].validate(e);return r!=null&&r.length?{type:"invalid",errors:(a==="firstError"?r.slice(0,1):r).map(l=>({message:l.message,path:p(l.path)}))}:{type:"valid"}})}function p(t){if(t!=null&&t.length)return t.map(e=>typeof e=="object"&&"key"in e?e.key:e)}export{h as validateStandardSchema};
@@ -0,0 +1 @@
1
+ function s(n,e){return n.type!==e.type?!1:n.type!=="invalid"||e.type!=="invalid"?!0:i(n.errors,e.errors)}function i(n,e){return n.length!==e.length?!1:n.every((t,r)=>{var l,u;return t.message===((l=e[r])==null?void 0:l.message)&&o(t.path,(u=e[r])==null?void 0:u.path)})}function o(n,e){return n==null||e==null?n==null&&e==null:n.length!==e.length?!1:n.every((t,r)=>Object.is(t,e[r]))}export{s as areValidationStatusesEqual};
@@ -0,0 +1,18 @@
1
+ import { type TFormField, type TFormFieldValidationConfig, type TFormFieldValidator } from './types';
2
+ /**
3
+ * Creates a standalone reactive form field.
4
+ *
5
+ * Use this when a field should exist independently of a specific form, for example a shared
6
+ * search input or a field composed into different forms. Pass the result directly into
7
+ * `createForm`'s `fields` config.
8
+ */
9
+ export declare function createFormField<GValue>(defaultValue: GValue, config: TCreateFormFieldConfig<GValue>): TFormField<GValue>;
10
+ export interface TCreateFormFieldConfig<GValue> extends Partial<TFormFieldValidationConfig> {
11
+ /** Stable field key used in validation contexts and form error paths. */
12
+ key: string;
13
+ /** Optional field-level Standard Schema validator. */
14
+ validator?: TFormFieldValidator<GValue>;
15
+ }
16
+ export declare function isFormField<GValue = unknown>(value: unknown): value is TFormField<GValue>;
17
+ /** Identifies listener events caused by field reset so validation can ignore reset changes. */
18
+ export declare const formFieldResetSourceKey = "formFieldReset";
@@ -1,35 +1,54 @@
1
- import { type BitwiseFlag } from '@blgc/utils';
2
- import { TCollectErrorMode } from 'validation-adapter';
3
- import { FormFieldReValidateMode, FormFieldValidateMode, TFormField, type TForm, type TFormConfig, type TFormData, type TFormFieldStateConfig, type TFormFieldValidator, type TInvalidSubmitCallback, type TValidSubmitCallback } from './types';
1
+ import { type TForm, type TFormData, type TFormField, type TFormFieldKey, type TFormFieldValidationConfig, type TFormFieldValidator, type TFormInvalidSubmitCallback, type TFormValidationConfig, type TFormValidator, type TFormValidSubmitCallback } from './types';
2
+ /**
3
+ * Creates a reactive form.
4
+ *
5
+ * Each field is a reactive state with its own `status`, `isTouched`, and `isSubmitted` states.
6
+ * Field configs in `fields` are upgraded to reactive form fields automatically.
7
+ * Validators must implement the Standard Schema interface (Zod, Valibot, or custom).
8
+ * Extend the form with features using `.with(feature())`.
9
+ */
10
+ export declare function createForm<GFields extends TCreateFormFieldsInput>(config: TCreateFormConfig<TCreateFormDataFromFields<GFields>> & {
11
+ fields: GFields;
12
+ }): TForm<TCreateFormDataFromFields<GFields>, []>;
13
+ /** Creates a form from field configs or existing form fields. */
4
14
  export declare function createForm<GFormData extends TFormData>(config: TCreateFormConfig<GFormData>): TForm<GFormData, []>;
5
- export interface TCreateFormConfig<GFormData extends TFormData> extends Partial<TFormConfig> {
6
- /**
7
- * Form fields
8
- */
15
+ export interface TCreateFormConfig<GFormData extends TFormData> {
16
+ /** Field configs or pre-built form fields keyed by form data property. */
9
17
  fields: TCreateFormConfigFormFields<GFormData>;
10
- /**
11
- * Error collection mode. 'firstError' gathers only the first error per field, 'all' gathers all errors.
12
- */
13
- collectErrorMode?: TCollectErrorMode;
14
- /**
15
- * Validation strategy **before** submitting.
16
- */
17
- validateMode?: BitwiseFlag<FormFieldValidateMode>;
18
- /**
19
- * Validation strategy **after** submitting.
20
- */
21
- reValidateMode?: BitwiseFlag<FormFieldReValidateMode>;
22
- /**
23
- * Whether to notify the form field if its status has changed
24
- */
25
- notifyOnStatusChange?: boolean;
26
- onInvalidSubmit?: TInvalidSubmitCallback<GFormData>;
27
- onValidSubmit?: TValidSubmitCallback<GFormData>;
18
+ /** Optional form-level validator for cross-field constraints. */
19
+ validator?: TFormValidator<NoInfer<GFormData>>;
20
+ /** Default validation triggers for the form validator and fields that do not override them. */
21
+ validateOn?: TFormValidationConfig['validateOn'];
22
+ /** Default revalidation triggers for the form validator and fields that do not override them. */
23
+ revalidateOn?: TFormValidationConfig['revalidateOn'];
24
+ /** Default error collection mode for the form validator and fields that do not override it. */
25
+ collectErrorMode?: TFormValidationConfig['collectErrorMode'];
26
+ /** Called on every valid submit. Per-call overrides can be passed directly to `submit()`. */
27
+ onValidSubmit?: TFormValidSubmitCallback<GFormData>;
28
+ /** Called on every invalid submit. Per-call overrides can be passed directly to `submit()`. */
29
+ onInvalidSubmit?: TFormInvalidSubmitCallback<GFormData>;
28
30
  }
31
+ /** Maps each form data property to a form field config or an existing form field. */
29
32
  export type TCreateFormConfigFormFields<GFormData extends TFormData> = {
30
- [Key in keyof GFormData]: TCreateFormConfigFormField<GFormData[Key]> | TFormField<GFormData[Key]>;
33
+ [Key in TFormFieldKey<GFormData>]: TCreateFormConfigFormField<GFormData[Key]> | TFormField<GFormData[Key]>;
31
34
  };
32
- export interface TCreateFormConfigFormField<GValue> extends Partial<TFormFieldStateConfig> {
33
- defaultValue?: GValue;
34
- validator?: TFormFieldValidator<GValue>;
35
+ /** Infers form data from field configs and existing form fields passed to `createForm()`. */
36
+ export type TCreateFormDataFromFields<GFields extends TCreateFormFieldsInput> = {
37
+ [Key in Extract<keyof GFields, string>]: GFields[Key] extends TFormField<infer GValue> ? GValue : GFields[Key] extends {
38
+ defaultValue: infer GValue;
39
+ } ? GValue : never;
40
+ };
41
+ export type TCreateFormFieldsInput = Record<string, TCreateFormConfigFormField<unknown> | TFormField<unknown>>;
42
+ /** Configures one form field when `createForm()` should create the field. */
43
+ export interface TCreateFormConfigFormField<GValue> {
44
+ /** Initial value for the created field. Also restored by `reset()`. */
45
+ defaultValue: GValue;
46
+ /** Optional field-level Standard Schema validator. */
47
+ validator?: TFormFieldValidator<NoInfer<GValue>>;
48
+ /** Overrides the form default for this field. */
49
+ collectErrorMode?: TFormFieldValidationConfig['collectErrorMode'];
50
+ /** Overrides the form default for this field after first submit. */
51
+ revalidateOn?: TFormFieldValidationConfig['revalidateOn'];
52
+ /** Overrides the form default for this field before first submit. */
53
+ validateOn?: TFormFieldValidationConfig['validateOn'];
35
54
  }
@@ -0,0 +1,33 @@
1
+ import { type TFeature } from 'feature-core';
2
+ import { type TState } from 'feature-state';
3
+ import { type TForm, type TFormData, type TFormFieldKey } from '../types';
4
+ /**
5
+ * Adds `isDirty`, `dirtyFields`, and `resetDirty()` to a form.
6
+ *
7
+ * Tracks whether each field differs from its `defaultValue` using deep structural equality.
8
+ * `isDirty` and `dirtyFields` are reactive states. `resetDirty()` makes the current values
9
+ * the new baseline without clearing them. Dirty state clears automatically when
10
+ * `submit({ updateDefaultValues: true })` succeeds.
11
+ */
12
+ export declare function dirtyFeature<GFormData extends TFormData = TFormData>(config?: TDirtyFeatureConfig): TDirtyFeature<GFormData>;
13
+ export interface TDirtyFeatureConfig {
14
+ /** Custom equality function. Defaults to deep structural equality. */
15
+ isEqual?: TDirtyFieldComparator;
16
+ }
17
+ /** Returns `true` when a current field value matches its default value. */
18
+ export type TDirtyFieldComparator = (value: unknown, defaultValue: unknown) => boolean;
19
+ export type TDirtyFeature<GFormData extends TFormData = TFormData> = TFeature<'dirty', {
20
+ /** True when at least one field differs from its default value. */
21
+ isDirty: TState<boolean, []>;
22
+ /** Field-keyed reactive map where `true` means the field differs from its default value. */
23
+ dirtyFields: TState<TDirtyFields<GFormData>, []>;
24
+ /** Makes the current field values the new default values without clearing the form. */
25
+ resetDirty(): void;
26
+ /** Updates dirty state after the original submit flow completes. */
27
+ submit: TForm<GFormData, []>['submit'];
28
+ }, [
29
+ ], 'submit'>;
30
+ /** Field-keyed dirty map where `true` means the field differs from its default value. */
31
+ export type TDirtyFields<GFormData extends TFormData> = {
32
+ [Key in TFormFieldKey<GFormData>]: boolean;
33
+ };
@@ -0,0 +1 @@
1
+ export * from './dirty';
@@ -1,6 +1,4 @@
1
- export { BitwiseFlag, bitwiseFlag } from '@blgc/utils';
2
1
  export * from './create-form';
3
- export * from './form-field';
4
- export * from './helper';
5
- export * from './is-form-with-features';
2
+ export * from './features';
3
+ export * from './create-form-field';
6
4
  export * from './types';
@@ -0,0 +1,2 @@
1
+ /** Deeply copies plain objects and arrays. Non-plain objects (class instances, Date, Map, etc.) are returned by reference. */
2
+ export declare function deepCopy<GValue>(value: GValue): GValue;
@@ -0,0 +1,3 @@
1
+ export * from './deep-copy';
2
+ export * from './standard-schema';
3
+ export * from './validation-status';
@@ -0,0 +1,4 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { type TCollectErrorMode, type TValidationStatusValue } from '../types';
3
+ /** Runs a Standard Schema validator and converts its issues into a validation status. */
4
+ export declare function validateStandardSchema<GValue>(schema: StandardSchemaV1<GValue, unknown>, value: GValue, collectErrorMode: TCollectErrorMode): Promise<TValidationStatusValue>;
@@ -0,0 +1,3 @@
1
+ import { type TValidationStatusValue } from '../types';
2
+ /** Compares validation statuses so reactive status state can skip equivalent updates. */
3
+ export declare function areValidationStatusesEqual(current: TValidationStatusValue, next: TValidationStatusValue): boolean;
@@ -0,0 +1,198 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { type TAnyFeature, type TFeature, type TFeatureHost } from 'feature-core';
3
+ import { type TIsEqualFeature, type TState } from 'feature-state';
4
+ /** Form object returned by `createForm()`. */
5
+ export type TForm<GFormData extends TFormData, GFeatures extends TAnyFeature[] = []> = TFeatureHost<TFormBase<GFormData>, GFeatures>;
6
+ /**
7
+ * Core form API used by feature installers.
8
+ * Use this type when a feature only needs the base methods,
9
+ * regardless of which other features are already installed on the host.
10
+ */
11
+ export interface TFormBase<GFormData extends TFormData> {
12
+ /** @internal */
13
+ _validation?: TFormValidation<GFormData>;
14
+ /** @internal Increments to prevent stale async validation runs from committing after reset or newer validation. */
15
+ _validationRunId: number;
16
+ /** @internal Stores form-level validator status after field-path errors have been routed to fields. */
17
+ _formValidatorStatus: TValidationStatusValue;
18
+ /** @internal */
19
+ _callbacks: TFormCallbacks<GFormData>;
20
+ /** Reactive aggregate validation status for the whole form. */
21
+ status: TFormStatus;
22
+ /** True while any async validator is running. */
23
+ isValidating: TState<boolean, []>;
24
+ /** True after the first submit attempt, regardless of validity. */
25
+ isSubmitted: TState<boolean, []>;
26
+ /** True while the submit handler is executing. */
27
+ isSubmitting: TState<boolean, []>;
28
+ /** Map of field keys to their reactive `TFormField` instances. */
29
+ fields: TFormFields<GFormData>;
30
+ /** Registers a callback for every valid submit. Returns an unsubscribe function. */
31
+ onValidSubmit(callback: TFormValidSubmitCallback<GFormData>): () => void;
32
+ /** Registers a callback for every invalid submit. Returns an unsubscribe function. */
33
+ onInvalidSubmit(callback: TFormInvalidSubmitCallback<GFormData>): () => void;
34
+ /** Runs all validators (including async), fires submit callbacks, and returns `true` if the form was valid. */
35
+ submit(options?: TFormSubmitOptions<GFormData>): Promise<boolean>;
36
+ /** Runs all validators and returns true if valid, false otherwise. */
37
+ validate(): Promise<boolean>;
38
+ /** Resets all fields to their default values and clears validation state. */
39
+ reset(): void;
40
+ /** Returns the `TFormField` for the given key. */
41
+ getField<GKey extends TFormFieldKey<GFormData>>(key: GKey): TFormFields<GFormData>[GKey];
42
+ /** Returns current field values without checking validity. */
43
+ getData(): Readonly<GFormData>;
44
+ /** Returns current field values when the form is valid, otherwise `null`. */
45
+ getValidData(): Readonly<GFormData> | null;
46
+ /** Returns current field and form-level validation errors. */
47
+ getErrors(): TFormErrors<GFormData>;
48
+ }
49
+ export type TFormData = object;
50
+ export type TFormFieldKey<GFormData extends TFormData> = Extract<keyof GFormData, string>;
51
+ /** Maps each form data property to its reactive field. */
52
+ export type TFormFields<GFormData extends TFormData> = {
53
+ [Key in TFormFieldKey<GFormData>]: TFormField<GFormData[Key]>;
54
+ };
55
+ /** Aggregate validation status for the whole form, including field and form-level errors. */
56
+ export type TFormStatus = TValidationStatus;
57
+ export interface TFormCallbacks<GFormData extends TFormData> {
58
+ validSubmit: TFormValidSubmitCallback<GFormData>[];
59
+ invalidSubmit: TFormInvalidSubmitCallback<GFormData>[];
60
+ }
61
+ export interface TFormSubmitOptions<GFormData extends TFormData> {
62
+ /** One-time valid-submit callback for this call, in addition to registered listeners. */
63
+ onValidSubmit?: TFormValidSubmitCallback<GFormData>;
64
+ /** One-time invalid-submit callback for this call, in addition to registered listeners. */
65
+ onInvalidSubmit?: TFormInvalidSubmitCallback<GFormData>;
66
+ /** Arbitrary data forwarded to all submit callbacks as the second argument. */
67
+ context?: TFormSubmitContext;
68
+ /** Updates field default values after a successful submit so future resets return to submitted data. */
69
+ updateDefaultValues?: boolean;
70
+ }
71
+ /** Receives typed form data after a successful submit. */
72
+ export type TFormValidSubmitCallback<GFormData extends TFormData> = (formData: Readonly<GFormData>, context?: TFormSubmitContext) => Promise<void> | void;
73
+ /** Receives collected form and field errors after an invalid submit. */
74
+ export type TFormInvalidSubmitCallback<GFormData extends TFormData> = (errors: TFormErrors<GFormData>, context?: TFormSubmitContext) => Promise<void> | void;
75
+ /** Metadata forwarded to submit callbacks. */
76
+ export interface TFormSubmitContext {
77
+ [key: string]: unknown;
78
+ /** The HTML submit event, if the form was submitted via a DOM form element. */
79
+ event?: unknown;
80
+ }
81
+ export interface TFormValidation<GFormData extends TFormData> {
82
+ validator: TFormValidator<GFormData>;
83
+ config: TFormValidationConfig;
84
+ }
85
+ /** Controls form-level validation timing and error collection. */
86
+ export interface TFormValidationConfig {
87
+ /** Validation triggers used before the first submit. */
88
+ validateOn: readonly TValidateTrigger[];
89
+ /** Validation triggers used after the first submit. */
90
+ revalidateOn: readonly TRevalidateTrigger[];
91
+ /** `'firstError'` stops after the first error; `'all'` collects every error. */
92
+ collectErrorMode: TCollectErrorMode;
93
+ }
94
+ /** Validates the full form data for cross-field and form-level constraints. */
95
+ export type TFormValidator<GFormData extends TFormData> = StandardSchemaV1<GFormData, unknown>;
96
+ export interface TFormErrors<GFormData extends TFormData> {
97
+ /** Field-level errors keyed by field, including form-level validator errors routed by field path. */
98
+ fields: TFormFieldErrors<GFormData>;
99
+ /** Pathless form-level validator errors, plus errors whose path does not match a field. */
100
+ form: readonly TFormError[];
101
+ }
102
+ export type TFormError = TValidationError;
103
+ export type TFormFieldErrors<GFormData extends TFormData> = {
104
+ [Key in TFormFieldKey<GFormData>]?: readonly TFormFieldError[];
105
+ };
106
+ export type TFormFieldError = TValidationError;
107
+ /** Form field object returned by `createFormField()`. */
108
+ export type TFormField<GValue> = TState<GValue, [TFormFieldFeature<GValue>]>;
109
+ export type TFormFieldFeature<GValue> = TFeature<'form-field', {
110
+ /** @internal */
111
+ _validation?: TFormFieldValidation<GValue>;
112
+ /** @internal Increments to prevent stale async validation runs from committing after reset or newer validation. */
113
+ _validationRunId: number;
114
+ /** @internal Stores the field validator result before routed form-level errors are merged into `status`. */
115
+ _fieldValidatorStatus: TValidationStatusValue;
116
+ /** @internal Form-level validator errors routed to this field by path. */
117
+ _formValidatorErrors: readonly TValidationError[];
118
+ /** @internal */
119
+ _callbacks: TFormFieldCallbacks;
120
+ /** The field's key within the form data shape. */
121
+ key: string;
122
+ /** The value this field resets to. Updated by `resetDirty()` or a successful `submit({ updateDefaultValues: true })`. */
123
+ defaultValue: GValue;
124
+ /** True after the field has been blurred at least once. */
125
+ isTouched: TState<boolean, []>;
126
+ /** True after the standalone field or parent form has been submitted. */
127
+ isSubmitted: TState<boolean, []>;
128
+ /** True while an async field validator is running. */
129
+ isValidating: TState<boolean, []>;
130
+ /** Field display status, including this field's validator errors and routed form-level errors. */
131
+ status: TFormFieldStatus;
132
+ /** @internal Updates routed form-level errors and syncs the public field status. */
133
+ _applyFormValidatorErrors: (errors: readonly TValidationError[]) => void;
134
+ /** Runs the field validator and updates `status`. */
135
+ validate: () => Promise<boolean>;
136
+ /** Registers a callback for future blur events. Returns an unsubscribe function. */
137
+ onBlur: (callback: TFormFieldBlurCallback) => () => void;
138
+ /** Marks the field as touched and runs blur validation when configured. */
139
+ blur: () => void;
140
+ /** Resets the field to `defaultValue` and clears validation state. */
141
+ reset: () => void;
142
+ }>;
143
+ export type TFormFieldStatus = TValidationStatus;
144
+ /** Stores a field-level validator with its timing and error collection config. */
145
+ export interface TFormFieldValidation<GValue> {
146
+ validator: TFormFieldValidator<GValue>;
147
+ config: TFormFieldValidationConfig;
148
+ }
149
+ /** Controls field-level validation timing and error collection. */
150
+ export interface TFormFieldValidationConfig {
151
+ /** Validation triggers used before the field is submitted. */
152
+ validateOn: readonly TValidateTrigger[];
153
+ /** Validation triggers used after the field is submitted. */
154
+ revalidateOn: readonly TRevalidateTrigger[];
155
+ /** `'firstError'` stops after the first error; `'all'` collects every error. */
156
+ collectErrorMode: TCollectErrorMode;
157
+ }
158
+ /** Validates one field value with Standard Schema. */
159
+ export type TFormFieldValidator<GValue> = StandardSchemaV1<GValue, unknown>;
160
+ export interface TFormFieldCallbacks {
161
+ blur: TFormFieldBlurCallback[];
162
+ }
163
+ export type TFormFieldBlurCallback = (context: TFormFieldBlurContext) => void;
164
+ export interface TFormFieldBlurContext {
165
+ /** True if the field was already touched before this blur. */
166
+ wasTouched: boolean;
167
+ }
168
+ /** Controls whether validation keeps the first issue or all issues returned by Standard Schema. */
169
+ export type TCollectErrorMode = 'firstError' | 'all';
170
+ /** Validation event names. `touched` runs after the first blur and then on later changes. */
171
+ export type TValidateTrigger = 'blur' | 'change' | 'submit' | 'touched';
172
+ /** Revalidation runs after submit, where `touched` no longer applies. */
173
+ export type TRevalidateTrigger = Exclude<TValidateTrigger, 'touched'>;
174
+ /** Validation status state used by forms and fields. */
175
+ export type TValidationStatus = TState<TValidationStatusValue, [
176
+ TIsEqualFeature<TValidationStatusValue>
177
+ ]>;
178
+ export type TValidationStatusValue = TInvalidValidationStatus | TValidValidationStatus | TUnvalidatedValidationStatus;
179
+ /** Validation failed with one or more errors. */
180
+ export interface TInvalidValidationStatus {
181
+ type: 'invalid';
182
+ errors: readonly TValidationError[];
183
+ }
184
+ export interface TValidationError {
185
+ /** Human-readable error description. */
186
+ message: string;
187
+ /** Standard Schema-compatible path associated with the error. */
188
+ path?: TValidationPath;
189
+ }
190
+ /** Standard Schema-compatible validation path segments. */
191
+ export type TValidationPath = readonly PropertyKey[];
192
+ export interface TValidValidationStatus {
193
+ type: 'valid';
194
+ }
195
+ /** Validation has not run yet. */
196
+ export interface TUnvalidatedValidationStatus {
197
+ type: 'unvalidated';
198
+ }
package/package.json CHANGED
@@ -1,9 +1,24 @@
1
1
  {
2
2
  "name": "feature-form",
3
- "version": "0.0.59",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
- "description": "Straightforward, typesafe, and feature-based form library for ReactJs",
6
- "keywords": [],
5
+ "description": "Framework-agnostic reactive form state with Standard Schema validation and per-field triggers",
6
+ "keywords": [
7
+ "form",
8
+ "form-state",
9
+ "form-validation",
10
+ "reactive-form",
11
+ "reactive",
12
+ "validation",
13
+ "standard-schema",
14
+ "zod",
15
+ "valibot",
16
+ "arktype",
17
+ "validation-timing",
18
+ "dirty-state",
19
+ "framework-agnostic",
20
+ "typescript"
21
+ ],
7
22
  "homepage": "https://builder.group/?utm_source=package-json",
8
23
  "bugs": {
9
24
  "url": "https://github.com/builder-group/community/issues"
@@ -20,17 +35,17 @@
20
35
  "types": "./dist/types/index.d.ts",
21
36
  "files": [
22
37
  "dist",
23
- "README.md"
38
+ "README.md",
39
+ "MIGRATION.md"
24
40
  ],
25
41
  "dependencies": {
26
- "@blgc/types": "0.0.22",
27
- "@blgc/utils": "0.0.62",
28
- "feature-state": "0.0.66",
29
- "validation-adapter": "0.0.28"
42
+ "@standard-schema/spec": "^1.1.0",
43
+ "feature-core": "0.0.2",
44
+ "feature-state": "0.1.0"
30
45
  },
31
46
  "devDependencies": {
32
- "rollup-presets": "0.0.27",
33
- "@blgc/config": "0.0.41"
47
+ "@blgc/config": "0.1.0",
48
+ "rollup-presets": "0.0.27"
34
49
  },
35
50
  "size-limit": [
36
51
  {
@@ -42,7 +57,7 @@
42
57
  "build:prod": "export NODE_ENV=production && pnpm build",
43
58
  "clean": "shx rm -rf dist && shx rm -rf .turbo && shx rm -rf node_modules",
44
59
  "install:clean": "pnpm run clean && pnpm install",
45
- "lint": "eslint . --fix",
60
+ "lint": "eslint .",
46
61
  "publish:patch": "pnpm build:prod && pnpm version patch && pnpm publish --no-git-checks --access=public",
47
62
  "size": "size-limit --why",
48
63
  "start:dev": "tsc -w",
@@ -1 +0,0 @@
1
- "use strict";function t(r){return{config:{collectErrorMode:r._config.collectErrorMode,name:r.key},value:r.get(),isValue(e){return!0},hasError(){var e;return((e=r.status._nextValue)==null?void 0:e.type)==="INVALID"},registerError(e){r.status.registerNextError({code:e.code,message:e.message,path:e.path})}}}exports.createFormFieldValidationContext=t;
@@ -1 +0,0 @@
1
- "use strict";var d=require("@blgc/utils"),r=require("feature-state"),_=require("validation-adapter"),t=require("../types/form-field.js"),M=require("./create-form-field-validation-context.js"),S=require("./create-status.js"),v=(o,u,a)=>new Promise((n,l)=>{var h=e=>{try{s(a.next(e))}catch(i){l(i)}},c=e=>{try{s(a.throw(e))}catch(i){l(i)}},s=e=>e.done?n(e.value):Promise.resolve(e.value).then(h,c);s((a=a.apply(o,u)).next())});function y(o,u){const{key:a,validator:n=_.createValidator([]),editable:l=!0,reValidateMode:h=d.bitwiseFlag(t.FormFieldReValidateMode.OnBlur),validateMode:c=d.bitwiseFlag(t.FormFieldValidateMode.OnSubmit),collectErrorMode:s="firstError",notifyOnStatusChange:e=!0}=u,i=r.createState(o),V={_config:{editable:l,validateMode:c,reValidateMode:h,collectErrorMode:s},_intialValue:d.deepCopy(i._v),_validator:n,key:a,isTouched:r.createState(!1),isSubmitted:r.createState(!1),isSubmitting:r.createState(!1),isValidating:r.createState(!1),status:S.createStatus({type:"UNVALIDATED"}),validate(){return v(this,null,function*(){const f=M.createFormFieldValidationContext(this);return this.isValidating.set(!0),yield this._validator.validate(f),this.isValidating.set(!1),this.status._nextValue==null?this.status.set({type:"VALID"}):this.status.set(this.status._nextValue),this.status._nextValue=void 0,this.status.get().type==="VALID"})},isValid(){return this.status.get().type==="VALID"},blur(){(this.isSubmitted.get()&&this._config.reValidateMode.has(t.FormFieldReValidateMode.OnBlur)||!this.isSubmitted.get()&&(this._config.validateMode.has(t.FormFieldValidateMode.OnBlur)||this._config.validateMode.has(t.FormFieldValidateMode.OnTouched)&&!this.isTouched.get()))&&this.validate(),this.isTouched.set(!0)},reset(){this.set(this._intialValue,{listenerContext:{source:"form-field_reset"}}),this.isTouched.set(!1),this.isSubmitted.set(!1),this.isSubmitting.set(!1),this.status.set({type:"UNVALIDATED"})}},g=Object.assign(i,V);return g._features.push("form-field"),d.withNew(Object.assign(g,{_new(f){f&&this.status.listen(m=>{i._notify({listenerContext:{source:"form-field_status-change",status:m.value}})},{key:"form-field_status-change"}),this.listen(m=>v(this,[m],function*({source:F}){F!=="form-field_reset"&&(this.isSubmitted.get()&&this._config.reValidateMode.has(t.FormFieldReValidateMode.OnChange)||!this.isSubmitted.get()&&this._config.validateMode.has(t.FormFieldValidateMode.OnChange)||this._config.validateMode.has(t.FormFieldValidateMode.OnTouched)&&this.isTouched.get())&&(yield this.validate())}),{key:"form-field_validate"})}}),e)}exports.createFormField=y;
@@ -1 +0,0 @@
1
- "use strict";var u=require("feature-state");function i(s){const a=u.createState(s),t=Object.assign(a,{_nextValue:void 0,registerNextError(e){var r;((r=this._nextValue)==null?void 0:r.type)==="INVALID"?this._nextValue.errors.push(e):this._nextValue={type:"INVALID",errors:[e]}}});return t._features.push("form-field-status"),t}exports.createStatus=i;
@@ -1 +0,0 @@
1
- "use strict";function r(e){return typeof e=="object"&&e!=null&&"_features"in e&&Array.isArray(e._features)&&e._features.includes("form-field-status")}exports.isFormFieldStatus=r;