diff --git a/client/src/mixins/formValidationMixin.js b/client/src/mixins/formValidationMixin.js new file mode 100644 index 00000000..0f955d25 --- /dev/null +++ b/client/src/mixins/formValidationMixin.js @@ -0,0 +1,77 @@ +/** + * Mixin providing common form validation helper methods for Vuelidate. + * + * This mixin centralizes the repeated validation and form reset patterns used + * across multiple components that handle new/edit forms. + * + * Usage: + * 1. Import and include in your component's mixins array + * 2. Define your form state properties (e.g., newFormState, editFormState) + * 3. Define your validations object as usual + * 4. Use the helper methods in your template and component logic + * + * Example: + * import formValidationMixin from '@/mixins/formValidationMixin'; + * + * export default { + * mixins: [formValidationMixin], + * data() { + * return { + * newFormState: { name: '' }, + * editFormState: { id: null, name: '' }, + * }; + * }, + * validations: { + * newFormState: { name: { required } }, + * editFormState: { name: { required } }, + * }, + * methods: { + * resetNewForm() { + * this.resetForm('newFormState', { name: '' }); + * }, + * }, + * }; + */ +export default { + methods: { + /** + * Get Bootstrap validation state for a form field. + * + * @param {string} formStateKey - The validation group key (e.g., 'newFormState', 'editFormState') + * @param {string} fieldName - The field name within the validation group + * @returns {boolean|null} - true if valid, false if invalid, null if pristine + */ + getValidationState(formStateKey, fieldName) { + const field = this.$v[formStateKey][fieldName]; + if (!field) { + return null; + } + const { $dirty, $error } = field; + return $dirty ? !$error : null; + }, + + /** + * Reset a form to its initial state and reset validation. + * + * @param {string} formStateKey - The data property key (e.g., 'newFormState') + * @param {Object} initialState - The initial state object to reset to + */ + resetForm(formStateKey, initialState) { + this[formStateKey] = { ...initialState }; + this.$nextTick(() => { + this.$v.$reset(); + }); + }, + + /** + * Check if a form has validation errors after touching all fields. + * + * @param {string} formStateKey - The validation group key + * @returns {boolean} - true if the form has any validation errors + */ + hasFormErrors(formStateKey) { + this.$v[formStateKey].$touch(); + return this.$v[formStateKey].$anyError; + }, + }, +}; diff --git a/client/src/views/show/config/ConfigCast.vue b/client/src/views/show/config/ConfigCast.vue index 39b0e7f7..ae2de076 100644 --- a/client/src/views/show/config/ConfigCast.vue +++ b/client/src/views/show/config/ConfigCast.vue @@ -71,7 +71,7 @@ id="new-first-name-input" v-model="$v.newFormState.firstName.$model" name="new-first-name-input" - :state="validateNewState('firstName')" + :state="getValidationState('newFormState', 'firstName')" aria-describedby="new-first-name-feedback" /> @@ -87,7 +87,7 @@ id="new-last-name-input" v-model="$v.newFormState.lastName.$model" name="new-last-name-input" - :state="validateNewState('lastName')" + :state="getValidationState('newFormState', 'lastName')" aria-describedby="new-last-name-feedback" /> @@ -115,7 +115,7 @@ id="edit-first-name-input" v-model="$v.editFormState.firstName.$model" name="edit-first-name-input" - :state="validateEditState('firstName')" + :state="getValidationState('editFormState', 'firstName')" aria-describedby="edit-first-name-feedback" /> @@ -131,7 +131,7 @@ id="edit-last-name-input" v-model="$v.editFormState.lastName.$model" name="edit-last-name-input" - :state="validateEditState('lastName')" + :state="getValidationState('editFormState', 'lastName')" aria-describedby="edit-last-name-feedback" /> @@ -148,10 +148,12 @@ import { required } from 'vuelidate/lib/validators'; import { mapGetters, mapActions } from 'vuex'; import CastLineStats from '@/vue_components/show/config/cast/CastLineStats.vue'; import log from 'loglevel'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'ConfigCast', components: { CastLineStats }, + mixins: [formValidationMixin], data() { return { castFields: ['first_name', 'last_name', { key: 'btn', label: '' }], @@ -198,15 +200,11 @@ export default { }, methods: { resetNewForm() { - this.newFormState = { + this.resetForm('newFormState', { firstName: '', lastName: '', - }; - this.submittingNewCast = false; - - this.$nextTick(() => { - this.$v.$reset(); }); + this.submittingNewCast = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -227,10 +225,6 @@ export default { this.submittingNewCast = false; } }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; - }, openEditForm(castMember) { if (castMember != null) { this.editFormState.id = castMember.item.id; @@ -241,18 +235,14 @@ export default { } }, resetEditForm() { - this.editFormState = { + this.resetForm('editFormState', { id: null, showID: null, firstName: '', lastName: '', - }; + }); this.submittingEditCast = false; this.deletingCast = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, async onSubmitEdit(event) { this.$v.editFormState.$touch(); @@ -273,10 +263,6 @@ export default { this.submittingEditCast = false; } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, async deleteCastMember(castMember) { if (this.deletingCast) { return; diff --git a/client/src/views/show/config/ConfigCharacters.vue b/client/src/views/show/config/ConfigCharacters.vue index ffe45695..1ce8c5c1 100644 --- a/client/src/views/show/config/ConfigCharacters.vue +++ b/client/src/views/show/config/ConfigCharacters.vue @@ -78,7 +78,7 @@ id="new-name-input" v-model="$v.newFormState.name.$model" name="new-name-input" - :state="validateNewState('name')" + :state="getValidationState('newFormState', 'name')" aria-describedby="new-name-feedback" /> @@ -94,7 +94,7 @@ id="new-description-input" v-model="$v.newFormState.description.$model" name="new-description-input" - :state="validateNewState('description')" + :state="getValidationState('newFormState', 'description')" /> @@ -126,7 +126,7 @@ id="edit-name-input" v-model="$v.editFormState.name.$model" name="edit-name-input" - :state="validateEditState('name')" + :state="getValidationState('editFormState', 'name')" aria-describedby="edit-name-feedback" /> @@ -142,7 +142,7 @@ id="edit-description-input" v-model="$v.editFormState.description.$model" name="edit-description-input" - :state="validateEditState('description')" + :state="getValidationState('editFormState', 'description')" /> @@ -168,10 +168,12 @@ import { mapGetters, mapActions } from 'vuex'; import CharacterLineStats from '@/vue_components/show/config/characters/CharacterLineStats.vue'; import log from 'loglevel'; import CharacterGroups from '@/vue_components/show/config/characters/CharacterGroups.vue'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'ConfigCharacters', components: { CharacterGroups, CharacterLineStats }, + mixins: [formValidationMixin], data() { return { rowsPerPage: 15, @@ -232,16 +234,12 @@ export default { }, methods: { resetNewForm() { - this.newFormState = { + this.resetForm('newFormState', { name: '', description: '', played_by: null, - }; - this.submittingNewCharacter = false; - - this.$nextTick(() => { - this.$v.$reset(); }); + this.submittingNewCharacter = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -262,10 +260,6 @@ export default { this.submittingNewCharacter = false; } }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; - }, openEditForm(character) { if (character != null) { this.editFormState.id = character.item.id; @@ -277,19 +271,15 @@ export default { } }, resetEditForm() { - this.editFormState = { + this.resetForm('editFormState', { id: null, showID: null, name: '', description: '', played_by: null, - }; + }); this.submittingEditCharacter = false; this.deletingCharacter = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, async onSubmitEdit(event) { this.$v.editFormState.$touch(); @@ -310,10 +300,6 @@ export default { this.submittingEditCharacter = false; } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, async deleteCharacter(character) { if (this.deletingCharacter) { return; diff --git a/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue b/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue index e1487819..beda3fd1 100644 --- a/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue +++ b/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue @@ -72,7 +72,7 @@ id="new-name-input" v-model="$v.newFormState.name.$model" name="new-name-input" - :state="validateNewState('name')" + :state="getValidationState('newFormState', 'name')" aria-describedby="new-name-feedback" /> @@ -119,7 +119,7 @@ id="edit-name-input" v-model="$v.editFormState.name.$model" name="edit-name-input" - :state="validateEditState('name')" + :state="getValidationState('editFormState', 'name')" aria-describedby="edit-name-feedback" /> @@ -146,7 +146,7 @@ id="edit-previous-act-input" v-model="$v.editFormState.previous_act_id.$model" :options="editFormActOptions" - :state="validateEditState('previous_act_id')" + :state="getValidationState('editFormState', 'previous_act_id')" aria-describedby="edit-previous-act-feedback" /> @@ -165,9 +165,11 @@ import { required, integer } from 'vuelidate/lib/validators'; import { mapGetters, mapActions } from 'vuex'; import log from 'loglevel'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'ConfigActs', + mixins: [formValidationMixin], data() { return { loading: true, @@ -271,20 +273,12 @@ export default { }, methods: { resetNewForm() { - this.newFormState = { + this.resetForm('newFormState', { name: '', interval_after: false, previous_act_id: null, - }; - this.submittingNewAct = false; - - this.$nextTick(() => { - this.$v.$reset(); }); - }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; + this.submittingNewAct = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -318,19 +312,15 @@ export default { } }, resetEditForm() { - this.editFormState = { + this.resetForm('editFormState', { id: null, showID: null, name: '', interval_after: false, previous_act_id: null, - }; + }); this.submittingEditAct = false; this.deletingAct = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, async onSubmitEdit(event) { this.$v.editFormState.$touch(); @@ -351,10 +341,6 @@ export default { this.submittingEditAct = false; } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, async deleteAct(act) { if (this.deletingAct) { return; diff --git a/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue b/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue index 78d1cc18..9b46ea37 100644 --- a/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue +++ b/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue @@ -96,7 +96,7 @@ id="new-name-input" v-model="$v.newFormState.name.$model" name="new-name-input" - :state="validateNewState('name')" + :state="getValidationState('newFormState', 'name')" aria-describedby="new-name-feedback" /> @@ -108,7 +108,7 @@ id="new-act-input" v-model="$v.newFormState.act_id.$model" :options="actOptions" - :state="validateNewState('act_id')" + :state="getValidationState('newFormState', 'act_id')" aria-describedby="new-act-feedback" /> @@ -144,7 +144,7 @@ id="edit-name-input" v-model="$v.editFormState.name.$model" name="edit-name-input" - :state="validateEditState('name')" + :state="getValidationState('editFormState', 'name')" aria-describedby="edit-name-feedback" /> @@ -156,7 +156,7 @@ id="edit-act-input" v-model="$v.editFormState.act_id.$model" :options="actOptions" - :state="validateEditState('act_id')" + :state="getValidationState('editFormState', 'act_id')" aria-describedby="edit-act-feedback" @change="editActChanged" /> @@ -173,7 +173,7 @@ id="edit-previous-scene-input" v-model="$v.editFormState.previous_scene_id.$model" :options="editFormPrevScenes" - :state="validateEditState('previous_scene_id')" + :state="getValidationState('editFormState', 'previous_scene_id')" aria-describedby="edit-previous-scene-feedback" /> @@ -215,9 +215,11 @@ import { required, integer } from 'vuelidate/lib/validators'; import { mapGetters, mapActions } from 'vuex'; import log from 'loglevel'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'ConfigScenes', + mixins: [formValidationMixin], data() { return { loading: true, @@ -420,20 +422,12 @@ export default { }, methods: { resetNewForm() { - this.newFormState = { + this.resetForm('newFormState', { name: '', act_id: null, previous_scene_id: null, - }; - this.submittingNewScene = false; - - this.$nextTick(() => { - this.$v.$reset(); }); - }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; + this.submittingNewScene = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -473,15 +467,11 @@ export default { } }, resetFirstSceneForm() { - this.firstSceneFormState = { + this.resetForm('firstSceneFormState', { act_id: null, scene_id: null, - }; - this.submittingFirstScene = false; - - this.$nextTick(() => { - this.$v.$reset(); }); + this.submittingFirstScene = false; }, openFirstSceneEdit(act) { if (act != null) { @@ -513,18 +503,14 @@ export default { }, resetEditForm() { this.editSceneID = null; - this.editFormState = { + this.resetForm('editFormState', { scene_id: null, name: '', act_id: null, previous_scene_id: null, - }; + }); this.submittingEditScene = false; this.deletingScene = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, openEditForm(scene) { if (scene != null) { @@ -540,10 +526,6 @@ export default { this.$bvModal.show('edit-scene'); } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, async onSubmitEdit(event) { this.$v.editFormState.$touch(); if (this.$v.editFormState.$anyError || this.submittingEditScene) { diff --git a/client/src/vue_components/show/config/characters/CharacterGroups.vue b/client/src/vue_components/show/config/characters/CharacterGroups.vue index 1788981a..a199364b 100644 --- a/client/src/vue_components/show/config/characters/CharacterGroups.vue +++ b/client/src/vue_components/show/config/characters/CharacterGroups.vue @@ -67,7 +67,7 @@ id="new-name-input" v-model="$v.newFormState.name.$model" name="new-name-input" - :state="validateNewState('name')" + :state="getValidationState('newFormState', 'name')" aria-describedby="new-name-feedback" /> @@ -83,7 +83,7 @@ id="new-description-input" v-model="$v.newFormState.description.$model" name="new-description-input" - :state="validateNewState('description')" + :state="getValidationState('newFormState', 'description')" /> @@ -120,7 +120,7 @@ id="edit-name-input" v-model="$v.editFormState.name.$model" name="edit-name-input" - :state="validateEditState('name')" + :state="getValidationState('editFormState', 'name')" aria-describedby="edit-name-feedback" /> @@ -136,7 +136,7 @@ id="edit-description-input" v-model="$v.editFormState.description.$model" name="edit-description-input" - :state="validateEditState('description')" + :state="getValidationState('editFormState', 'description')" /> @@ -168,9 +168,11 @@ import { mapActions, mapGetters } from 'vuex'; import { required } from 'vuelidate/lib/validators'; import log from 'loglevel'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'CharacterGroups', + mixins: [formValidationMixin], data() { return { loading: true, @@ -224,16 +226,12 @@ export default { }, resetNewForm() { this.tempCharacterList = []; - this.newFormState = { + this.resetForm('newFormState', { name: '', description: '', characters: [], - }; - this.submittingNewGroup = false; - - this.$nextTick(() => { - this.$v.$reset(); }); + this.submittingNewGroup = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -254,10 +252,6 @@ export default { this.submittingNewGroup = false; } }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; - }, async deleteCharacterGroup(characterGroup) { if (this.deletingGroup) { return; @@ -293,19 +287,15 @@ export default { } }, resetEditForm() { - this.editFormState = { + this.resetForm('editFormState', { id: null, name: '', description: '', characters: [], - }; + }); this.tempEditCharacterList = []; this.submittingEditGroup = false; this.deletingGroup = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, editSelectChanged(value, id) { this.$v.editFormState.characters.$model = value.map((character) => character.id); @@ -329,10 +319,6 @@ export default { this.submittingEditGroup = false; } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, ...mapActions([ 'GET_CHARACTER_LIST', 'GET_CHARACTER_GROUP_LIST', diff --git a/client/src/vue_components/show/config/stage/CrewList.vue b/client/src/vue_components/show/config/stage/CrewList.vue index 0a5388c6..782cc523 100644 --- a/client/src/vue_components/show/config/stage/CrewList.vue +++ b/client/src/vue_components/show/config/stage/CrewList.vue @@ -47,7 +47,7 @@ id="new-first-name-input" v-model="$v.newFormState.firstName.$model" name="new-first-name-input" - :state="validateNewState('firstName')" + :state="getValidationState('newFormState', 'firstName')" aria-describedby="new-first-name-feedback" /> @@ -63,7 +63,7 @@ id="new-last-name-input" v-model="$v.newFormState.lastName.$model" name="new-last-name-input" - :state="validateNewState('lastName')" + :state="getValidationState('newFormState', 'lastName')" aria-describedby="new-last-name-feedback" /> @@ -90,7 +90,7 @@ id="edit-first-name-input" v-model="$v.editFormState.firstName.$model" name="edit-first-name-input" - :state="validateEditState('firstName')" + :state="getValidationState('editFormState', 'firstName')" aria-describedby="edit-first-name-feedback" /> @@ -106,7 +106,7 @@ id="edit-last-name-input" v-model="$v.editFormState.lastName.$model" name="edit-last-name-input" - :state="validateEditState('lastName')" + :state="getValidationState('editFormState', 'lastName')" aria-describedby="edit-last-name-feedback" /> @@ -121,9 +121,11 @@