gristlabs_grist-core/app/client/ui/FormPage.ts
George Gevoian 86062a8c28 (core) New Grist Forms styling and field options
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
2024-04-11 08:17:42 -07:00

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%;
`);