useForm.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import { ref, computed, onMounted, onBeforeUnmount, watch } 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: JSON.parse(JSON.stringify(input.value)),
  36. errors: [],
  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 saveButton = ref();
  71. const useCallback = (status: string, messages?: Record<string, string>) =>
  72. new Promise((resolve, reject: (reason: Error) => void) => {
  73. cb(
  74. {
  75. status,
  76. messages: { ...messages },
  77. values: Object.fromEntries(
  78. Object.entries(inputs.value).map(([name, input]) => [
  79. name,
  80. input.value
  81. ])
  82. )
  83. },
  84. resolve,
  85. reject
  86. );
  87. });
  88. const resetOriginalValues = () => {
  89. inputs.value = Object.fromEntries(
  90. Object.entries(inputs.value).map(([name, input]) => [
  91. name,
  92. {
  93. ...input,
  94. originalValue: JSON.parse(JSON.stringify(input.value)),
  95. sourceChanged: false
  96. }
  97. ])
  98. );
  99. };
  100. const validate = () => {
  101. const invalid: Record<string, string[]> = {};
  102. Object.entries(inputs.value).forEach(([name, input]) => {
  103. input.errors = [];
  104. if (
  105. input.required &&
  106. (input.value === undefined ||
  107. input.value === "" ||
  108. input.value === null)
  109. )
  110. input.errors.push(`Invalid ${name}. Please provide value`);
  111. if (input.validate) {
  112. const valid = input.validate(input.value, inputs);
  113. if (valid !== true) {
  114. input.errors.push(
  115. valid === false ? `Invalid ${name}` : valid
  116. );
  117. }
  118. }
  119. if (input.errors.length > 0)
  120. invalid[name] = input.errors.join(", ");
  121. });
  122. return invalid;
  123. };
  124. const save = (saveCb?: () => void) => {
  125. if (saveButton.value) saveButton.value.status = "disabled";
  126. const errors = validate();
  127. const errorCount = Object.keys(errors).length;
  128. if (errorCount === 0 && unsavedChanges.value.length > 0) {
  129. const onSave = () => {
  130. useCallback("success")
  131. .then(() => {
  132. resetOriginalValues();
  133. if (saveCb) saveCb();
  134. saveButton.value?.handleSuccessfulSave();
  135. })
  136. .catch((err: Error) =>
  137. useCallback("error", { error: err.message }).then(
  138. () => {
  139. saveButton.value?.handleFailedSave();
  140. }
  141. )
  142. );
  143. };
  144. if (sourceChanged.value.length > 0)
  145. openModal({
  146. modal: "confirm",
  147. props: {
  148. message:
  149. "Updates have been made whilst you were making changes. Are you sure you want to continue?",
  150. onCompleted: onSave
  151. }
  152. });
  153. else onSave();
  154. } else if (errorCount === 0) {
  155. useCallback("unchanged", { unchanged: "No changes have been made" })
  156. .then(() => {
  157. if (saveCb) saveCb();
  158. saveButton.value?.handleSuccessfulSave();
  159. })
  160. .catch((err: Error) =>
  161. useCallback("error", { error: err.message }).then(() => {
  162. saveButton.value?.handleFailedSave();
  163. })
  164. );
  165. } else {
  166. useCallback("error", {
  167. ...errors,
  168. error: `${errorCount} ${
  169. errorCount === 1 ? "input" : "inputs"
  170. } failed validation.`
  171. }).then(() => {
  172. saveButton.value?.handleFailedSave();
  173. });
  174. }
  175. };
  176. const setValue = (value: Record<string, any>, reset?: boolean) => {
  177. Object.entries(value).forEach(([name, inputValue]) => {
  178. if (inputs.value[name]) {
  179. inputs.value[name].value = JSON.parse(
  180. JSON.stringify(inputValue)
  181. );
  182. if (reset) {
  183. inputs.value[name].sourceChanged = false;
  184. inputs.value[name].originalValue = JSON.parse(
  185. JSON.stringify(inputValue)
  186. );
  187. }
  188. }
  189. });
  190. };
  191. const setOriginalValue = (value: Record<string, any>) => {
  192. Object.entries(value).forEach(([name, inputValue]) => {
  193. if (inputs.value[name]) {
  194. if (
  195. JSON.stringify(inputValue) !==
  196. JSON.stringify(inputs.value[name].originalValue)
  197. ) {
  198. if (unsavedChanges.value.find(change => change === name))
  199. inputs.value[name].sourceChanged = true;
  200. else
  201. inputs.value[name].value = JSON.parse(
  202. JSON.stringify(inputValue)
  203. );
  204. inputs.value[name].originalValue = JSON.parse(
  205. JSON.stringify(inputValue)
  206. );
  207. }
  208. }
  209. });
  210. };
  211. const setModelValues = (model: Record<string, any>, keys: string[]) => {
  212. const getModelValue = (key, modelObject) => {
  213. let value = JSON.parse(JSON.stringify(modelObject));
  214. key.split(".").forEach(property => {
  215. value = value[property];
  216. });
  217. return value;
  218. };
  219. const setMappedValues = modelObject => {
  220. setOriginalValue(
  221. Object.fromEntries(
  222. keys.map(key => [key, getModelValue(key, modelObject)])
  223. )
  224. );
  225. };
  226. setMappedValues(model);
  227. watch(model, setMappedValues);
  228. };
  229. onMounted(() => {
  230. if (
  231. options &&
  232. options.modalUuid &&
  233. options.preventCloseUnsaved !== false
  234. )
  235. preventCloseUnsaved[options.modalUuid] = () =>
  236. unsavedChanges.value.length > 0;
  237. });
  238. onBeforeUnmount(() => {
  239. if (
  240. options &&
  241. options.modalUuid &&
  242. options.preventCloseUnsaved !== false
  243. )
  244. delete preventCloseUnsaved[options.modalUuid];
  245. });
  246. return {
  247. inputs,
  248. unsavedChanges,
  249. saveButton,
  250. save,
  251. setValue,
  252. setOriginalValue,
  253. setModelValues
  254. };
  255. };