diff --git a/public/js/components/Form/Form.js b/public/js/components/Form/Form.js
new file mode 100644
index 000000000..bf9e318c0
--- /dev/null
+++ b/public/js/components/Form/Form.js
@@ -0,0 +1,129 @@
+import FhcFragment from "../Fragment.js";
+
+export default {
+ components: {
+ FhcFragment
+ },
+ provide() {
+ return {
+ $registerToForm: component => {
+ if (this.inputs.indexOf(component) < 0)
+ this.inputs.push(component);
+ }
+ };
+ },
+ props: {
+ tag: {
+ type: String,
+ default: 'form'
+ }
+ },
+ data() {
+ return {
+ inputs: []
+ }
+ },
+ computed: {
+ sortedInputs() {
+ return this.inputs.reduce((a,c) => {
+ let name = c.name || '_default';
+ if (!a[name])
+ a[name] = [];
+ a[name].push(c);
+ return a;
+ }, {});
+ }
+ },
+ methods: {
+ _sendFeedbackToInput(inputs, feedback, valid) {
+ if (inputs.length) {
+ inputs.forEach(input => input.setFeedback(feedback, valid));
+ return false;
+ }
+ if (this.$fhcAlert) {
+ this.$fhcAlert[valid ? 'alertSuccess' : 'alertError'](feedback);
+ return false;
+ }
+ return true;
+ },
+ setFeedback(feedback, valid) {
+ if (Array.isArray(feedback)) {
+ let remaining = feedback.filter(fb =>
+ this._sendFeedbackToInput(
+ this.sortedInputs['_default'] || [],
+ fb,
+ valid
+ )
+ );
+ return remaining.length ? remaining : null;
+ }
+ if (typeof feedback === 'object') {
+ let remaining = Object.entries(feedback).filter(([name, fb]) =>
+ this._sendFeedbackToInput(
+ this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [],
+ fb,
+ valid
+ )
+ );
+ return remaining.length ? Object.fromEntries(remaining) : null;
+ }
+
+ let remaining = this._sendFeedbackToInput(
+ this.sortedInputs['_default'] || [],
+ feedback,
+ valid
+ );
+ return remaining ? feedback : null;
+ },
+ clearValidation() {
+ this.inputs.forEach(input => input.clearValidation());
+ },
+ send(promise) {
+ return new Promise((resolve, reject) => {
+ promise.then(result => {
+ if (result?.status == 200 && result.data) {
+ if (typeof result.data !== 'object' || !result.data.hasOwnProperty('retval'))
+ // TODO(chris): IMPLEMENT! Error in API
+ return reject(result);
+ if (result.data.error)
+ // TODO(chris): IMPLEMENT! Error in API
+ return reject(result);
+ const data = result.data.retval;
+ // TODO(chris): check for something better/add new standardized return value
+ if (result.data.code == 1)
+ this.setFeedback(data, true);
+ return resolve(data);
+ }
+ // TODO(chris): IMPLEMENT! Wrong result object
+ reject(result);
+ }).catch(result => {
+ if (result?.response?.status == 400 && result.response.data) {
+ if (typeof result.response.data !== 'object' || !result.response.data.hasOwnProperty('retval'))
+ // TODO(chris): IMPLEMENT! Error in API
+ return reject(result);
+ this.clearValidation();
+ const remaining = this.setFeedback(
+ result.response.data.retval,
+ false
+ );
+ if (remaining) {
+ result.response.data.retval = remaining;
+ return reject(result);
+ }
+ } else if (result?.response?.status == 500) {
+ if (this.$fhcAlert)
+ this.$fhcAlert.handleSystemError(result);
+ else
+ return reject(result);
+ } else {
+ return reject(result);
+ }
+ });
+ });
+ }
+ },
+ template: `
+
+
+ `
+}
\ No newline at end of file
diff --git a/public/js/components/Form/Input.js b/public/js/components/Form/Input.js
new file mode 100644
index 000000000..6b3ed5eb1
--- /dev/null
+++ b/public/js/components/Form/Input.js
@@ -0,0 +1,207 @@
+import FhcFragment from "../Fragment.js";
+
+let _uuid = {};
+
+export default {
+ inheritAttrs: false,
+ components: {
+ FhcFragment
+ },
+ inject: [
+ '$registerToForm'
+ ],
+ props: {
+ bsFeedback: Boolean,
+ noAutoClass: Boolean,
+ type: String,
+ name: String
+ },
+ data() {
+ return {
+ valid: undefined,
+ feedback: []
+ }
+ },
+ computed: {
+ lcType() {
+ if (!this.type)
+ return 'text';
+ return this.type.toLowerCase();
+ },
+ tag() {
+ switch (this.lcType) {
+ case 'textarea':
+ case 'select':
+ return this.lcType;
+ case 'datepicker':
+ return 'VueDatePicker';
+ case 'autocomplete':
+ return 'PvAutocomplete';
+ default:
+ return 'input';
+ }
+ },
+ validationClass() {
+ const classes = [];
+ if (this.valid)
+ classes.push('is-valid');
+ else if (this.valid === false)
+ classes.push('is-invalid');
+
+ if (!this.noAutoClass) {
+ let c = this.$attrs.class ? this.$attrs.class.split(' ') : [];
+ switch (this.lcType) {
+ // TODO(chris): complete list!
+ case 'select':
+ if (!c.includes('form-select'))
+ classes.push('form-select');
+ break;
+ case 'range':
+ if (!c.includes('form-range'))
+ classes.push('form-range');
+ break;
+ case 'radio':
+ case 'checkbox':
+ // TODO(chris): maybe different handling?
+ if (!c.includes('form-check-input') && !c.includes('btn-check'))
+ classes.push('form-check-input');
+ break;
+ case 'autocomplete':
+ case 'datepicker':
+ classes.push('p-0');
+ classes.push('border-0');
+ case 'text':
+ case 'textarea':
+ if (!c.includes('form-control'))
+ classes.push('form-control');
+ break;
+ }
+ }
+
+ return classes;
+ },
+ feedbackClass() {
+ if (!this.feedback || this.feedback === true)
+ return '';
+ if (!this.bsFeedback)
+ return {
+ 'valid-tooltip': this.valid === true,
+ 'invalid-tooltip': this.valid === false
+ };
+ return {
+ 'valid-feedback': this.valid === true,
+ 'invalid-feedback': this.valid === false
+ };
+ },
+ modelValueCmp: {
+ get() {
+ return this.$attrs.modelValue;
+ },
+ set(v) {
+ this.$emit('update:modelValue', v);
+ }
+ },
+ idCmp() {
+ let uuid = this.$attrs.id;
+ if (this.lcType == 'datepicker')
+ uuid = this.$attrs.uid;
+ if (!uuid && this.$attrs.label)
+ uuid = 'fhc-form-input';
+ if (!uuid)
+ return undefined;
+ if (this.lcType == 'datepicker')
+ uuid = 'dp-input-' + uuid;
+ if (_uuid[uuid] === undefined)
+ _uuid[uuid] = 0;
+ return uuid + '-' + (_uuid[uuid]++);
+ }
+ },
+ methods: {
+ clearValidation() {
+ this.valid = undefined;
+ this.feedback = [];
+ },
+ setFeedback(feedback, valid) {
+ if (!Array.isArray(feedback))
+ feedback = [feedback];
+ this.valid = valid;
+ this.feedback = feedback;
+ },
+ _loadComponents() {
+ if (this.tag == 'VueDatePicker' && !this._.components.VueDatePicker) {
+ this._.components.VueDatePicker = Vue.defineAsyncComponent(() => import("../vueDatepicker.js.php"));
+ } else if (this.tag == 'PvAutocomplete' && !this._.components.PvAutocomplete) {
+ this._.components.PvAutocomplete = Vue.defineAsyncComponent(() => import(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/public/js/components/primevue/autocomplete/autocomplete.esm.min.js"));;
+ }
+ }
+ },
+ beforeMount() {
+ this._loadComponents();
+ },
+ beforeUpdate() {
+ this._loadComponents();
+ },
+ mounted() {
+ if (this.$registerToForm)
+ this.$registerToForm(this);
+ // TODO(chris): wrap check in div?
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{msg}}
+
+
+
+
+ `
+}
\ No newline at end of file
diff --git a/public/js/components/Form/Validation.js b/public/js/components/Form/Validation.js
index 3b4e69112..51ea3c689 100644
--- a/public/js/components/Form/Validation.js
+++ b/public/js/components/Form/Validation.js
@@ -1,84 +1,36 @@
+const FEEDBACK_DEFAULT = {
+ success: [],
+ danger: []
+};
export default {
- props: {
- name: String
- },
+ inject: [
+ '$registerToForm'
+ ],
data() {
return {
- isValid: false,
- isInvalid: false,
- invalidFeedback: ''
+ feedback: FEEDBACK_DEFAULT
};
},
methods: {
- reset() {
- this.isValid = false;
- this.isInvalid = false;
- this.invalidFeedback = '';
+ clearValidation() {
+ this.feedback = FEEDBACK_DEFAULT;
},
- setValid() {
- this.isValid = true;
- },
- setInvalid(feedback) {
- this.invalidFeedback = feedback?.detail || '';
- this.isInvalid = true;
+ setFeedback(feedback, valid) {
+ if (!Array.isArray(feedback))
+ feedback = [feedback];
+ this.feedback[valid ? 'success' : 'danger'] = feedback;
}
},
- render() {
- if (!this.$slots.default)
- return Vue.h('div', {
- 'data-fhc-form-error': true,
- class: {
- 'alert': true,
- 'alert-danger': true,
- 'd-none': !this.isInvalid
- },
- role: 'alert',
- onFhcFormReset: this.reset,
- onFhcFormError: this.setInvalid
- }, (this.invalidFeedback || []).map((txt, i) => Vue.h('p', {
- key: i,
- class: i+1 == (this.invalidFeedback || []).length ? 'mb-0' : ''
- }, txt)));
-
- const res = this.$slots.default();
- const orig = res.shift();
-
- let options = {
- 'data-fhc-form-validate': this.name,
- onFhcFormReset: this.reset,
- onFhcFormValidate: this.setValid,
- onFhcFormInvalidate: this.setInvalid,
- onInput: this.reset,
- class: {
- 'form-control': orig.type !== 'select' && orig.props.type !== 'checkbox' && orig.props.type !== 'radio' && !(orig.props?.class || '').split(' ').includes('form-control'),
- 'form-select': orig.type === 'select' && !(orig.props?.class || '').split(' ').includes('form-select'),
- 'is-valid': this.isValid,
- 'is-invalid': this.isInvalid
- }
- };
- if (orig.type.__name == 'VueDatePicker') {
- options.class['p-0'] = true;
- options.inputClassName = 'border-0';
- }
- res.unshift(Vue.cloneVNode(orig, options));
-
- if (this.isInvalid && this.invalidFeedback && this.$slots.default) {
- /* NOTE(chris): use bootstraps 'feedback' */
- /*res.push(Vue.h('div', {
- class: 'invalid-feedback'
- }, this.invalidFeedback));*/
-
- /* NOTE(chris): use bootstraps 'tooltip' */
- res.push(Vue.h('div', {
- class: 'invalid-tooltip'
- }, this.invalidFeedback));
- }
- /* NOTE(chris): use bootstraps 'feedback' */
- /*return res;*/
-
- /* NOTE(chris): use bootstraps 'tooltip' */
- return Vue.h('div', {
- class: 'position-relative'
- }, res);
+ mounted() {
+ if (this.$registerToForm)
+ this.$registerToForm(this);
},
+ template: `
+
+
+ {{msg}}
+
+
+
+ `
};
\ No newline at end of file
diff --git a/public/js/components/Fragment.js b/public/js/components/Fragment.js
new file mode 100644
index 000000000..4d4af87c7
--- /dev/null
+++ b/public/js/components/Fragment.js
@@ -0,0 +1,5 @@
+export default {
+ render() {
+ return (this.$slots && this.$slots.default) ? this.$slots.default() : null;
+ }
+};
\ No newline at end of file
diff --git a/public/js/components/Stv/Studentenverwaltung/List/New.js b/public/js/components/Stv/Studentenverwaltung/List/New.js
index 3709acb5f..8df462909 100644
--- a/public/js/components/Stv/Studentenverwaltung/List/New.js
+++ b/public/js/components/Stv/Studentenverwaltung/List/New.js
@@ -1,7 +1,9 @@
import {CoreRESTClient} from '../../../../RESTClient.js';
import BsModal from '../../../Bootstrap/Modal.js';
-import FhcFormValidation from '../../../Form/Validation.js';
-import VueDatePicker from '../../../vueDatepicker.js.php';
+import FhcForm from '../../../Form/Form.js';
+import FormValidation from '../../../Form/Validation.js';
+import FormInput from '../../../Form/Input.js';
+import { useForm } from '../../../../composables/Form.js';
import accessibility from '../../../../directives/accessibility.js';
var _uuid = 0;
@@ -20,8 +22,9 @@ const FORMDATA_DEFAULT = {
export default {
components: {
BsModal,
- FhcFormValidation,
- VueDatePicker
+ FhcForm,
+ FormValidation,
+ FormInput
},
directives: {
accessibility
@@ -95,9 +98,9 @@ export default {
this.formData = FORMDATA_DEFAULT;
this.person = null;
this.suggestions = [];
- this.$fhcAlert.resetFormValidation(this.$refs.form)
+ this.$refs.form.clearValidation();
},
- loadSuggestions() {
+ loadSuggestions() {console.log('loadSuggestions');
if (this.abortController.suggestions)
this.abortController.suggestions.abort();
if (this.person !== null)
@@ -129,21 +132,22 @@ export default {
return;
this.abortController.places = new AbortController();
- CoreRESTClient
- .get('components/stv/address/getPlaces/' + this.formData.address.plz, undefined, {
- signal: this.abortController.places.signal
- })
- .then(result => CoreRESTClient.getData(result.data) || [])
+ this.$refs.form
+ .send(CoreRESTClient.get(
+ 'components/stv/address/getPlaces/' + this.formData.address.plz,
+ undefined,
+ {
+ signal: this.abortController.places.signal
+ }
+ ))
.then(result => {
- this.places = result;
+ this.places = result
})
.catch(error => {
- if (error.code == 'ERR_BAD_REQUEST') {
- return this.$fhcAlert.handleFormValidation(error, this.$refs.form);
- }
- // NOTE(chris): repeat request
if (error.code != "ERR_CANCELED")
window.setTimeout(this.loadPlaces, 100);
+ else
+ console.error(error);
});
},
loadStudienplaene() {
@@ -180,26 +184,20 @@ export default {
if (this.person === null)
return this.person = 0;
- this.$fhcAlert.resetFormValidation(this.$refs.form);
+ //this.$fhcAlert.resetFormValidation(this.$refs.form);
const data = {...this.formData, ...(this.person || {})};
if (data.studiengang_kz === undefined)
data.studiengang_kz = this.studiengangKz;
if (data.studiensemester_kurzbz === undefined)
data.studiensemester_kurzbz = this.studiensemesterKurzbz;
- CoreRESTClient
- .post('components/stv/student/add', data)
- .then(result => result.data)
- .then(result => {
- if (CoreRESTClient.isError(result))
- throw new Error(CoreRESTClient.getError(result));
- return CoreRESTClient.getData(result);
- })
+ this.$refs.form
+ .send(CoreRESTClient.post('components/stv/student/add', data))
.then(result => {
this.$fhcAlert.alertSuccess('Gespeichert');
this.$refs.modal.hide();
})
- .catch(this.$fhcAlert.handleFormValidation(this.$refs.form));
+ .catch(console.error);
}
},
created() {
@@ -211,35 +209,59 @@ export default {
this.semester = result;
})
.catch(this.$fhcAlert.handleSystemError);
-
},
template: `
-
`
+ `
};
\ No newline at end of file