useForm.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import { ref, computed, onMounted, onBeforeUnmount } from "vue";
  2. import { useModalsStore } from "@/stores/modals";
  3. export const useForm = (
  4. inputOptions: Record<
  5. string,
  6. | {
  7. value: any;
  8. validate?: (value: any) => boolean | string;
  9. }
  10. | any
  11. >,
  12. cb: (
  13. response: {
  14. status: string;
  15. messages: Record<string, string>;
  16. values: Record<string, any>;
  17. },
  18. resolve: (value?: undefined) => void,
  19. reject: (value: Error) => void
  20. ) => void,
  21. options?: {
  22. modalUuid?: string;
  23. preventCloseUnsaved?: boolean;
  24. }
  25. ) => {
  26. const { openModal, preventCloseUnsaved } = useModalsStore();
  27. const inputs = ref(
  28. Object.fromEntries(
  29. Object.entries(inputOptions).map(([name, input]) => {
  30. if (typeof input !== "object") input = { value: input };
  31. return [
  32. name,
  33. {
  34. ...input,
  35. originalValue: input.value,
  36. errors: <string[]>[],
  37. ref: ref(),
  38. sourceChanged: false,
  39. ignoreUnsaved: input.ignoreUnsaved === true,
  40. required:
  41. input.required === undefined ? true : input.required
  42. }
  43. ];
  44. })
  45. )
  46. );
  47. const unsavedChanges = computed(() => {
  48. const changed = <string[]>[];
  49. Object.entries(inputs.value).forEach(([name, input]) => {
  50. if (
  51. !input.ignoreUnsaved &&
  52. JSON.stringify(input.value) !==
  53. JSON.stringify(input.originalValue)
  54. )
  55. changed.push(name);
  56. });
  57. return changed;
  58. });
  59. const sourceChanged = computed(() => {
  60. const _sourceChanged = <string[]>[];
  61. Object.entries(inputs.value).forEach(([name, input]) => {
  62. if (
  63. input.sourceChanged &&
  64. unsavedChanges.value.find(change => change === name)
  65. )
  66. _sourceChanged.push(name);
  67. });
  68. return _sourceChanged;
  69. });
  70. const useCallback = (status: string, messages?: Record<string, string>) =>
  71. new Promise((resolve, reject: (reason: Error) => void) => {
  72. cb(
  73. {
  74. status,
  75. messages: { ...messages },
  76. values: Object.fromEntries(
  77. Object.entries(inputs.value).map(([name, input]) => [
  78. name,
  79. input.value
  80. ])
  81. )
  82. },
  83. resolve,
  84. reject
  85. );
  86. });
  87. const resetOriginalValues = () => {
  88. inputs.value = Object.fromEntries(
  89. Object.entries(inputs.value).map(([name, input]) => [
  90. name,
  91. {
  92. ...input,
  93. originalValue: input.value,
  94. sourceChanged: false
  95. }
  96. ])
  97. );
  98. };
  99. const validate = () => {
  100. const invalid = <Record<string, string[]>>{};
  101. Object.entries(inputs.value).forEach(([name, input]) => {
  102. input.errors = [];
  103. if (
  104. input.required &&
  105. (input.value === undefined ||
  106. input.value === "" ||
  107. input.value === null)
  108. )
  109. input.errors.push(`Invalid ${name}. Please provide value`);
  110. if (input.validate) {
  111. const valid = input.validate(input.value);
  112. if (valid !== true) {
  113. input.errors.push(
  114. valid === false ? `Invalid ${name}` : valid
  115. );
  116. }
  117. }
  118. if (input.errors.length > 0)
  119. invalid[name] = input.errors.join(", ");
  120. });
  121. return invalid;
  122. };
  123. const save = (saveCb?: () => void) => {
  124. const errors = validate();
  125. const errorCount = Object.keys(errors).length;
  126. if (errorCount === 0 && unsavedChanges.value.length > 0) {
  127. const onSave = () => {
  128. useCallback("success")
  129. .then(() => {
  130. resetOriginalValues();
  131. if (saveCb) saveCb();
  132. })
  133. .catch((err: Error) =>
  134. useCallback("error", { error: err.message })
  135. );
  136. };
  137. if (sourceChanged.value.length > 0)
  138. openModal({
  139. modal: "confirm",
  140. data: {
  141. message:
  142. "Updates have been made whilst you were making changes. Are you sure you want to continue?",
  143. onCompleted: onSave
  144. }
  145. });
  146. else onSave();
  147. } else if (errorCount === 0) {
  148. useCallback("unchanged", { unchanged: "No changes to update" });
  149. if (saveCb) saveCb();
  150. } else {
  151. useCallback("error", {
  152. ...errors,
  153. error: `${errorCount} ${
  154. errorCount === 1 ? "input" : "inputs"
  155. } failed validation.`
  156. });
  157. }
  158. };
  159. const setValue = (value: Record<string, any>, reset?: boolean) => {
  160. Object.entries(value).forEach(([name, inputValue]) => {
  161. if (inputs.value[name]) {
  162. inputs.value[name].value = JSON.parse(
  163. JSON.stringify(inputValue)
  164. );
  165. if (reset) {
  166. inputs.value[name].sourceChanged = false;
  167. inputs.value[name].originalValue = JSON.parse(
  168. JSON.stringify(inputValue)
  169. );
  170. }
  171. }
  172. });
  173. };
  174. const setOriginalValue = (value: Record<string, any>) => {
  175. Object.entries(value).forEach(([name, inputValue]) => {
  176. if (inputs.value[name]) {
  177. if (
  178. JSON.stringify(inputValue) !==
  179. JSON.stringify(inputs.value[name].originalValue)
  180. ) {
  181. if (unsavedChanges.value.find(change => change === name))
  182. inputs.value[name].sourceChanged = true;
  183. else
  184. inputs.value[name].value = JSON.parse(
  185. JSON.stringify(inputValue)
  186. );
  187. inputs.value[name].originalValue = JSON.parse(
  188. JSON.stringify(inputValue)
  189. );
  190. }
  191. }
  192. });
  193. };
  194. onMounted(() => {
  195. if (
  196. options &&
  197. options.modalUuid &&
  198. options.preventCloseUnsaved !== false
  199. )
  200. preventCloseUnsaved[options.modalUuid] = () =>
  201. unsavedChanges.value.length > 0;
  202. });
  203. onBeforeUnmount(() => {
  204. if (
  205. options &&
  206. options.modalUuid &&
  207. options.preventCloseUnsaved !== false
  208. )
  209. delete preventCloseUnsaved[options.modalUuid];
  210. });
  211. return {
  212. inputs,
  213. unsavedChanges,
  214. save,
  215. setValue,
  216. setOriginalValue
  217. };
  218. };