440 lines
10 KiB
Vue
440 lines
10 KiB
Vue
<template>
|
|
<form class="ui-form" @keydown="onKeydown" @submit.prevent="onSubmit" @change="onChange">
|
|
<slot v-if="loadingState === 'default'" v-bind="slotProps" />
|
|
<div v-if="loadingState == 'loading'" class="ui-form-loading">
|
|
<i class="ui-form-loading-progress"></i>
|
|
</div>
|
|
<div v-if="loadingState === 'error'">
|
|
error [not implemented]
|
|
</div>
|
|
<!--<form-error-view v-if="loadingState === 'error'" :error="loadingError" />-->
|
|
</form>
|
|
</template>
|
|
|
|
|
|
<script lang="ts">
|
|
import { defineComponent } from 'vue';
|
|
//import FormErrorView from './form-error-view.vue';
|
|
import * as overlays from '../services/overlay';
|
|
import * as notifications from '../services/notification';
|
|
import { arrayGroupBy } from '../utils/arrays';
|
|
|
|
export default defineComponent({
|
|
name: 'uiForm',
|
|
|
|
//components: { FormErrorView },
|
|
|
|
props: {
|
|
errorComponents: {
|
|
type: Array,
|
|
default: () => ['uiError']
|
|
},
|
|
inputComponents: {
|
|
type: Array,
|
|
default: () => ['uiProperty']
|
|
},
|
|
route: {
|
|
type: [String, Object],
|
|
default: null
|
|
}
|
|
},
|
|
|
|
data: () => ({
|
|
dirty: false,
|
|
loadingState: 'default',
|
|
loadingError: null,
|
|
state: 'default',
|
|
errors: [],
|
|
canEdit: true,
|
|
slotProps: {
|
|
state: null
|
|
},
|
|
submitBlocked: false
|
|
}),
|
|
|
|
watch: {
|
|
'$route': {
|
|
deep: true,
|
|
handler: function (val)
|
|
{
|
|
this.$nextTick(() =>
|
|
{
|
|
this.$emit('load', this);
|
|
});
|
|
}
|
|
},
|
|
state(val)
|
|
{
|
|
this.slotProps.state = val;
|
|
},
|
|
canEdit(val)
|
|
{
|
|
this.$nextTick(() =>
|
|
{
|
|
this.setCanEdit(val);
|
|
});
|
|
}
|
|
},
|
|
|
|
|
|
created()
|
|
{
|
|
this.slotProps.state = this.state;
|
|
this.$emit('load', this);
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
// shows a confirmation dialog for dirty forms when the route tries to change
|
|
// it only works when this method is attached to the route component
|
|
beforeRouteLeave(to, from, next)
|
|
{
|
|
if (this.dirty)
|
|
{
|
|
Overlay.confirm({
|
|
title: '@unsavedchanges.title',
|
|
text: '@unsavedchanges.text',
|
|
confirmLabel: '@unsavedchanges.confirm',
|
|
closeLabel: '@unsavedchanges.close'
|
|
}).then(
|
|
() => next(false),
|
|
() =>
|
|
{
|
|
this.dirty = false;
|
|
next();
|
|
}
|
|
);
|
|
}
|
|
else
|
|
{
|
|
next()
|
|
}
|
|
},
|
|
|
|
// loads data on creation of the form
|
|
async load(promise)
|
|
{
|
|
this.setState('loading');
|
|
const response = await promise();
|
|
|
|
if (!response.success)
|
|
{
|
|
this.setState('error');
|
|
if (response.errors)
|
|
{
|
|
this.loadingError = response.errors[0].message;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
this.canEdit = true;
|
|
this.setState('default');
|
|
this.$nextTick(() =>
|
|
{
|
|
this.$emit('loaded', this);
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
// handles a promise as result of the form submission
|
|
async handle(response, isCreate)
|
|
{
|
|
this.setState('loading');
|
|
this.clearErrors();
|
|
|
|
if (!response.success)
|
|
{
|
|
this.setState('error');
|
|
this.setErrors(response.errors);
|
|
return null;
|
|
}
|
|
|
|
this.setState('success');
|
|
this.setDirty(false);
|
|
|
|
if (response.data && this.route && response.status === 201)
|
|
{
|
|
let routeObj = typeof this.route === 'object' ? this.route : { name: this.route };
|
|
routeObj.params = routeObj.params || {};
|
|
routeObj.query = this.$route.query || {};
|
|
routeObj.params.id = response.data.id;
|
|
|
|
this.$router.replace(routeObj);
|
|
}
|
|
},
|
|
|
|
|
|
// submits the form
|
|
onSubmit(e)
|
|
{
|
|
if (!this.submitBlocked)
|
|
{
|
|
this.$emit('submit', this, e);
|
|
}
|
|
},
|
|
|
|
|
|
// set the form to dirty when one of the fields changes
|
|
onChange(e)
|
|
{
|
|
this.dirty = true;
|
|
},
|
|
|
|
|
|
// handle delete event
|
|
async onDelete(promise)
|
|
{
|
|
const overlay = await overlays.confirmDelete();
|
|
|
|
if (overlay.eventType == 'close')
|
|
{
|
|
return;
|
|
}
|
|
|
|
const opts = overlay.value;
|
|
|
|
opts.state('loading');
|
|
const response = await promise();
|
|
|
|
if (response.success)
|
|
{
|
|
opts.state('success');
|
|
opts.close();
|
|
this.$router.go(-1);
|
|
notifications.success('@deleteoverlay.success', '@deleteoverlay.success_text');
|
|
}
|
|
else
|
|
{
|
|
opts.state('error');
|
|
opts.errors(response.errors);
|
|
}
|
|
},
|
|
|
|
|
|
// prevent submission of form on enter key press
|
|
onKeydown(e)
|
|
{
|
|
if (e.keyCode === 13)
|
|
{
|
|
this.submitBlocked = true;
|
|
clearTimeout(this.submitBlockedTimeout);
|
|
this.submitBlockedTimeout = setTimeout(() => this.submitBlocked = false, 300);
|
|
}
|
|
},
|
|
|
|
|
|
// set dirty status
|
|
setDirty(dirty)
|
|
{
|
|
this.dirty = dirty;
|
|
},
|
|
|
|
|
|
// set state for this component and all child components which attached it's state from the slot props
|
|
setState(state)
|
|
{
|
|
this.state = state;
|
|
},
|
|
|
|
|
|
// clears all errors from the form
|
|
clearErrors()
|
|
{
|
|
this.getErrorComponents().forEach(component =>
|
|
{
|
|
component.clearErrors();
|
|
|
|
if (component.tab)
|
|
{
|
|
component.tab.clearErrors();
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
// tries to find matching fields for the given errors and displays them
|
|
setErrors(errors)
|
|
{
|
|
if (typeof errors === 'undefined' || !errors)
|
|
{
|
|
this.errors = [];
|
|
}
|
|
else
|
|
{
|
|
this.errors = !Array.isArray(errors) ? [errors] : errors;
|
|
}
|
|
|
|
// get all components + grouped errors
|
|
let errorComponents = this.getErrorComponents();
|
|
let errorGroups = arrayGroupBy(this.errors, 'property');
|
|
let handledGroups = [];
|
|
|
|
// set errors
|
|
errorComponents.forEach(component =>
|
|
{
|
|
let field = component.field;
|
|
|
|
if (field && errorGroups[field])
|
|
{
|
|
handledGroups.push(field);
|
|
component.setErrors(errorGroups[field]);
|
|
|
|
if (component.tab)
|
|
{
|
|
component.tab.setErrors(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
for (var field in errorGroups)
|
|
{
|
|
let errorGroup = errorGroups[field];
|
|
if (handledGroups.indexOf(field) < 0)
|
|
{
|
|
errorComponents.forEach(component =>
|
|
{
|
|
if (component.catchRemaining || component.catchAll)
|
|
{
|
|
component.setErrors(errorGroup, true);
|
|
|
|
if (component.tab)
|
|
{
|
|
component.tab.setErrors(true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
// find all error components in form
|
|
getErrorComponents()
|
|
{
|
|
let errorComponents = [];
|
|
|
|
// find components which can output errors
|
|
let traverseChildren = (parent, tab) =>
|
|
{
|
|
parent.$children.forEach(component =>
|
|
{
|
|
if (this.errorComponents.indexOf(component.$options.name) > -1)
|
|
{
|
|
errorComponents.push({
|
|
name: component.$options.name,
|
|
field: component.field,
|
|
catchAll: component.catchAll,
|
|
catchRemaining: component.catchRemaining,
|
|
component: component,
|
|
setErrors: component.setErrors,
|
|
clearErrors: component.clearErrors,
|
|
tab: tab
|
|
});
|
|
}
|
|
else
|
|
{
|
|
traverseChildren(component, tab || (component.$options.name === 'uiTab' ? component : null));
|
|
}
|
|
});
|
|
};
|
|
|
|
traverseChildren(this);
|
|
|
|
return errorComponents;
|
|
},
|
|
|
|
|
|
// sets the form editing ability
|
|
setCanEdit(canEdit)
|
|
{
|
|
// find components which can output errors
|
|
//let traverseChildren = parent =>
|
|
//{
|
|
// parent.$children.forEach(component =>
|
|
// {
|
|
// const isValidComponent = this.inputComponents.indexOf(component.$options.name) > -1;
|
|
// //const field = component.field;
|
|
|
|
// if (typeof component._props.disabled === 'boolean')
|
|
// {
|
|
// component.$emit('update:disabled', canEdit);
|
|
// }
|
|
// else
|
|
// {
|
|
// traverseChildren(component);
|
|
// }
|
|
// //if (isErrorComponent)
|
|
// //{
|
|
// // errorComponents.push(component);
|
|
// //}
|
|
|
|
// //if (isErrorComponent && field)
|
|
// //{
|
|
// // let errorGroup = errorGroups[field];
|
|
|
|
// // if (errorGroup)
|
|
// // {
|
|
// // handledGroups.push(field);
|
|
// // component.set(errorGroup);
|
|
// // }
|
|
// //}
|
|
// //else
|
|
// //{
|
|
// // traverseChildren(component);
|
|
// //}
|
|
// });
|
|
//};
|
|
|
|
//traverseChildren(this);
|
|
|
|
//console.info('done');
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.ui-form
|
|
{
|
|
min-height: 100%;
|
|
font-size: var(--font-size);
|
|
}
|
|
|
|
.ui-form-loading
|
|
{
|
|
display: flex;
|
|
width: 100%;
|
|
height: 100vh;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.ui-form-loading-progress
|
|
{
|
|
width: 32px;
|
|
height: 32px;
|
|
z-index: 2;
|
|
border-radius: 40px;
|
|
border: 2px solid var(--color-bg-shade-3);
|
|
border-left-color: var(--color-text);
|
|
opacity: 1;
|
|
will-change: transform;
|
|
animation: rotating .5s linear infinite;
|
|
transition: opacity .25s ease;
|
|
}
|
|
|
|
@keyframes rotating
|
|
{
|
|
from
|
|
{
|
|
-webkit-transform: rotate(0);
|
|
transform: rotate(0)
|
|
}
|
|
to
|
|
{
|
|
-webkit-transform: rotate(1turn);
|
|
transform: rotate(1turn)
|
|
}
|
|
}
|
|
</style> |