mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
86062a8c28
Summary: - New styling for forms. - New field options for various field types (spinner, checkbox, radio buttons, alignment, sort). - Improved alignment of form fields in columns. - Support for additional select input keyboard shortcuts (Enter and Backspace). - Prevent submitting form on Enter if an input has focus. - Fix for changing form field type causing the field to disappear. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4223
184 lines
4.5 KiB
TypeScript
184 lines
4.5 KiB
TypeScript
import {FormRenderer} from 'app/client/components/FormRenderer';
|
|
import {handleSubmit, TypedFormData} from 'app/client/lib/formUtils';
|
|
import {makeT} from 'app/client/lib/localization';
|
|
import {FormModel, FormModelImpl} from 'app/client/models/FormModel';
|
|
import {buildFormFooter} from 'app/client/ui/FormContainer';
|
|
import {FormErrorPage} from 'app/client/ui/FormErrorPage';
|
|
import {FormSuccessPage} from 'app/client/ui/FormSuccessPage';
|
|
import {colors} from 'app/client/ui2018/cssVars';
|
|
import {ApiError} from 'app/common/ApiError';
|
|
import {getPageTitleSuffix} from 'app/common/gristUrls';
|
|
import {getGristConfig} from 'app/common/urlUtils';
|
|
import {Disposable, dom, makeTestId, Observable, styled, subscribe} from 'grainjs';
|
|
|
|
const t = makeT('FormPage');
|
|
|
|
const testId = makeTestId('test-form-');
|
|
|
|
export class FormPage extends Disposable {
|
|
private readonly _model: FormModel = new FormModelImpl();
|
|
private readonly _error = Observable.create<string|null>(this, null);
|
|
|
|
constructor() {
|
|
super();
|
|
this._model.fetchForm().catch(reportError);
|
|
|
|
this.autoDispose(subscribe(this._model.form, (_use, form) => {
|
|
if (!form) { return; }
|
|
|
|
document.title = `${form.formTitle}${getPageTitleSuffix(getGristConfig())}`;
|
|
}));
|
|
}
|
|
|
|
public buildDom() {
|
|
return cssPageContainer(
|
|
dom.domComputed(use => {
|
|
const error = use(this._model.error);
|
|
if (error) { return dom.create(FormErrorPage, error); }
|
|
|
|
const submitted = use(this._model.submitted);
|
|
if (submitted) { return dom.create(FormSuccessPage, this._model); }
|
|
|
|
return this._buildFormPageDom();
|
|
}),
|
|
);
|
|
}
|
|
|
|
private _buildFormPageDom() {
|
|
return dom.domComputed(use => {
|
|
const form = use(this._model.form);
|
|
const rootLayoutNode = use(this._model.formLayout);
|
|
if (!form || !rootLayoutNode) { return null; }
|
|
|
|
const formRenderer = FormRenderer.new(rootLayoutNode, {
|
|
fields: form.formFieldsById,
|
|
rootLayoutNode,
|
|
disabled: this._model.submitting,
|
|
error: this._error,
|
|
});
|
|
|
|
return dom('div',
|
|
cssForm(
|
|
cssFormBody(
|
|
cssFormContent(
|
|
dom.autoDispose(formRenderer),
|
|
formRenderer.render(),
|
|
handleSubmit(this._model.submitting,
|
|
(_formData, formElement) => this._handleFormSubmit(formElement),
|
|
() => this._handleFormSubmitSuccess(),
|
|
(e) => this._handleFormError(e),
|
|
),
|
|
),
|
|
),
|
|
cssFormFooter(
|
|
buildFormFooter(),
|
|
),
|
|
),
|
|
testId('page'),
|
|
);
|
|
});
|
|
}
|
|
|
|
private async _handleFormSubmit(formElement: HTMLFormElement) {
|
|
await this._model.submitForm(new TypedFormData(formElement));
|
|
}
|
|
|
|
private async _handleFormSubmitSuccess() {
|
|
const formLayout = this._model.formLayout.get();
|
|
if (!formLayout) { throw new Error('formLayout is not defined'); }
|
|
|
|
const {successURL} = formLayout;
|
|
if (successURL) {
|
|
try {
|
|
const url = new URL(successURL);
|
|
window.location.href = url.href;
|
|
return;
|
|
} catch {
|
|
// If the URL is invalid, just ignore it.
|
|
}
|
|
}
|
|
|
|
this._model.submitted.set(true);
|
|
}
|
|
|
|
private _handleFormError(e: unknown) {
|
|
this._error.set(t('There was an error submitting your form. Please try again.'));
|
|
if (!(e instanceof ApiError) || e.status >= 500) {
|
|
// If it doesn't look like a user error (i.e. a 4XX HTTP response), report it.
|
|
reportError(e as Error|string);
|
|
}
|
|
}
|
|
}
|
|
|
|
const cssPageContainer = styled('div', `
|
|
height: 100%;
|
|
width: 100%;
|
|
padding: 20px;
|
|
overflow: auto;
|
|
`);
|
|
|
|
const cssForm = styled('div', `
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
background-color: white;
|
|
border-radius: 3px;
|
|
max-width: 600px;
|
|
margin: 0px auto;
|
|
`);
|
|
|
|
const cssFormBody = styled('div', `
|
|
width: 100%;
|
|
`);
|
|
|
|
// TODO: break up and move to `FormRendererCss.ts`.
|
|
const cssFormContent = styled('form', `
|
|
color: ${colors.dark};
|
|
font-size: 15px;
|
|
line-height: 1.42857143;
|
|
|
|
& h1,
|
|
& h2,
|
|
& h3,
|
|
& h4,
|
|
& h5,
|
|
& h6 {
|
|
margin: 8px 0px 12px 0px;
|
|
font-weight: normal;
|
|
}
|
|
& h1 {
|
|
font-size: 24px;
|
|
}
|
|
& h2 {
|
|
font-size: 22px;
|
|
}
|
|
& h3 {
|
|
font-size: 16px;
|
|
}
|
|
& h4 {
|
|
font-size: 13px;
|
|
}
|
|
& h5 {
|
|
font-size: 11px;
|
|
}
|
|
& h6 {
|
|
font-size: 10px;
|
|
}
|
|
& p {
|
|
margin: 0px;
|
|
}
|
|
& strong {
|
|
font-weight: 600;
|
|
}
|
|
& hr {
|
|
border: 0px;
|
|
border-top: 1px solid ${colors.darkGrey};
|
|
margin: 4px 0px;
|
|
}
|
|
`);
|
|
|
|
const cssFormFooter = styled('div', `
|
|
padding: 8px 16px;
|
|
width: 100%;
|
|
`);
|