Skip to content
This repository was archived by the owner on Sep 16, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/forms/field-name.definer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { InGroup } from '@frontmeans/input-aspects';
import { afterAll, consumeEvents } from '@proc7ts/fun-events';
import { Class, Supply } from '@proc7ts/primitives';
import { ComponentClass } from '@wesib/wesib';
import { ComponentShare__symbol, ComponentShareRef } from '../share';
import { componentShareLocator, ComponentShareLocator } from '../share';
import { Field } from './field';
import { Field$nameByKey } from './field.impl';
import { Form } from './form';
Expand Down Expand Up @@ -62,7 +62,7 @@ function FormUnitName<
return ({
key,
share,
formShare,
locateForm: defaultForm,
name: defaultName,
}) => {

Expand All @@ -84,15 +84,15 @@ function FormUnitName<
fieldName = autoName;
}

const fieldFormShare = (def.form || formShare || FormShare)[ComponentShare__symbol];
const locateForm = componentShareLocator(def.form || defaultForm, { share: FormShare });

return {
componentDef: {
setup(setup) {
setup.whenComponent(context => {
afterAll({
unit: context.get(share),
form: fieldFormShare.valueFor(context),
form: locateForm(context),
}).do(
consumeEvents(({ unit: [field], form: [form] }): Supply | undefined => {
if (!form || !field) {
Expand Down Expand Up @@ -123,11 +123,11 @@ export interface FieldNameDef {
/**
* A form to add the field to.
*
* This is a reference to the form share.
* This is a shared form locator.
*
* Either {@link SharedFieldDef.form predefined}, or {@link FieldShare default} form share is used when omitted.
*/
readonly form?: ComponentShareRef<Form>;
readonly form?: ComponentShareLocator<Form>;

/**
* Field name.
Expand Down
1 change: 1 addition & 0 deletions src/forms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './form.share';
export * from './form-preset';
export * from './form-unit';
export * from './presets';
export * from './on-submit.decorator';
export * from './shared-field.decorator';
export * from './shared-form.decorator';
export * from './shared-form-unit.decorator';
98 changes: 98 additions & 0 deletions src/forms/on-submit.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { inFormElement, InGroup, inGroup } from '@frontmeans/input-aspects';
import { afterThe } from '@proc7ts/fun-events';
import { noop } from '@proc7ts/primitives';
import { Component, ComponentMount } from '@wesib/wesib';
import { testDefinition } from '../spec/test-element';
import { Form } from './form';
import { OnSubmit, OnSubmitDef } from './on-submit.decorator';
import { SharedForm, SharedFormDef } from './shared-form.decorator';

describe('forms', () => {
describe('@OnSubmit', () => {

interface TestData {
property?: string;
}

let element: Element;
let formElement: HTMLFormElement;

beforeEach(() => {
element = document.body.appendChild(document.createElement('custom-element'));
formElement = element.appendChild(document.createElement('form'));
});
afterEach(() => {
element.remove();
});

it('calls decorated method on submit', async () => {

const onSubmit = jest.fn();

await bootstrap(onSubmit);

formElement.requestSubmit();
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
control: expect.any(InGroup),
element: expect.objectContaining({ element: expect.objectContaining({ tagName: 'FORM' }) }),
}),
expect.objectContaining({
type: 'submit',
}),
);
});
it('cancels default event handler by default', async () => {
await bootstrap(noop);

const submit = new Event('submit', { cancelable: true });

expect(formElement.dispatchEvent(submit)).toBe(false);
});
it('does not cancel default event handler when `cancel` is `false`', async () => {
await bootstrap(noop, { cancel: false });

const submit = new Event('submit', { cancelable: true });

expect(formElement.dispatchEvent(submit)).toBe(true);
});
it('does not submit when there is no form', async () => {

const defaultSubmit = jest.fn(e => e.preventDefault());

formElement.addEventListener('submit', defaultSubmit);

const onSubmit = jest.fn();

await bootstrap(onSubmit, { form: () => afterThe() });
formElement.requestSubmit();
expect(defaultSubmit).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'submit' }));
expect(onSubmit).not.toHaveBeenCalled();
});

async function bootstrap(
onSubmit: (form: Form<TestData>, event: Event) => void,
def?: OnSubmitDef,
formDef?: SharedFormDef<Form<TestData>>,
): Promise<ComponentMount> {

@Component('custom-element')
class TestElement {

@SharedForm(formDef)
form = Form.by<TestData>(
opts => inGroup({}, opts),
opts => inFormElement(formElement, opts),
);

@OnSubmit(def)
readonly onSubmit = onSubmit;

}

const defContext = await testDefinition(TestElement);

return defContext.mountTo(element);
}
});
});
85 changes: 85 additions & 0 deletions src/forms/on-submit.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { handleDomEvents } from '@frontmeans/dom-events';
import { consumeEvents } from '@proc7ts/fun-events';
import { Class } from '@proc7ts/primitives';
import { ComponentClass, ComponentContext, ComponentProperty, ComponentPropertyDecorator } from '@wesib/wesib';
import { componentShareLocator, ComponentShareLocator } from '../share';
import { Form } from './form';
import { FormShare } from './form.share';

/**
* Creates a decorator for component method to call on input form submit.
*
* The decorated method accepts a {@link Form form} to submit and submit event as parameters.
*
* @typeParam TModel - Submitted model type.
* @typeParam TElt - A type of HTML form element.
* @typeParam TClass - A type of decorated component class.
* @param def - Submit handler definition.
*
* @returns New component property decorator.
*/
export function OnSubmit<TModel = any, TElt extends HTMLElement = HTMLElement, T extends ComponentClass = Class>(
def: OnSubmitDef<TModel, TElt> = {},
): ComponentPropertyDecorator<(form: Form<TModel, TElt>, event: Event) => void, T> {

const { form: formRef = FormShare, cancel = true } = def;
const locateForm = componentShareLocator(formRef, { share: FormShare, self: true });

return ComponentProperty(({ get }) => ({
componentDef: {
define(defContext) {
defContext.whenComponent(context => {
context.whenConnected(() => {

const { component } = context;

locateForm(context).do(
consumeEvents((form?: Form<TModel, TElt>, _sharer?: ComponentContext) => {
if (!form) {
return;
}

let onSubmit = form.element.events.on('submit');

if (cancel) {
onSubmit = onSubmit.do(
handleDomEvents(false),
);
}

return onSubmit(
event => get(component).call(component, form, event),
);
}),
).needs(context);
});
});
},
},
}));
}

/**
* Form submit handler definition.
*
* Configures {@link OnSubmit @OnSubmit} component property decorator.
*/
export interface OnSubmitDef<TModel = any, TElt extends HTMLElement = HTMLElement> {

/**
* A form to submit.
*
* This is a shared form locator.
*
* A {@link FieldShare default} form share is used when omitted.
*/
readonly form?: ComponentShareLocator<Form<TModel, TElt>>;

/**
* Whether to cancel default submit handler.
*
* `true` by default.
*/
readonly cancel?: boolean;

}
25 changes: 16 additions & 9 deletions src/forms/shared-field.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Class } from '@proc7ts/primitives';
import { ComponentClass } from '@wesib/wesib';
import { ComponentShare, ComponentShare__symbol, ComponentShareDecorator, ComponentShareRef, Shared } from '../share';
import {
ComponentShare,
ComponentShareDecorator,
componentShareLocator,
ComponentShareLocator,
ComponentShareRef,
Shared,
} from '../share';
import { Field } from './field';
import { FieldName } from './field-name.definer';
import { Field$name } from './field.impl';
Expand Down Expand Up @@ -71,17 +78,17 @@ export function SharedField<

const {
share = FieldShare as ComponentShareRef<any> as ComponentShareRef<TField>,
form: formShareRef = FormShare,
form: formLocator,
} = def;
const formShare: ComponentShare<Form<any, any>> = formShareRef[ComponentShare__symbol];
const locateForm = componentShareLocator(formLocator, { share: FormShare });

return SharedFormUnit<TField, TValue, Field.Controls<TValue>, TClass>(
share,
...definers.map(definer => (
descriptor: Shared.Descriptor<TField, TClass>,
) => definer({
...descriptor,
formShare,
locateForm,
name: Field$name(descriptor.key, fieldName),
})),
);
Expand All @@ -103,11 +110,11 @@ export interface SharedFieldDef<TField extends Field<TValue>, TValue = Field.Val
/**
* A form to add the field to.
*
* This is a reference to the form share.
* This is shared form locator.
*
* The {@link FieldShare default form share} is used when omitted.
*/
readonly form?: ComponentShareRef<Form>;
readonly form?: ComponentShareLocator<Form>;

/**
* Field name.
Expand Down Expand Up @@ -145,12 +152,12 @@ export namespace SharedField {
readonly share: ComponentShare<TField>;

/**
* Predefined share of the form to add the field to, or `undefined` when unknown.
* Predefined locator function of the form to add the field to.
*/
readonly formShare: ComponentShare<Form<any, any>>;
readonly locateForm: ComponentShareLocator.Fn<Form<any, any>>;

/**
* Predefined field name, or `null`/`undefined` when the field is not to be added to the {@link formShare form}.
* Predefined field name, or `null`/`undefined` when the field is not to be added to the {@link locateForm form}.
*/
readonly name: string | null;

Expand Down
8 changes: 4 additions & 4 deletions src/forms/shared-form-unit.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Class } from '@proc7ts/primitives';
import { ComponentClass } from '@wesib/wesib';
import { ComponentShare, ComponentShareDecorator, ComponentShareRef, Shared } from '../share';
import { ComponentShareDecorator, ComponentShareLocator, ComponentShareRef, Shared } from '../share';
import { Form } from './form';
import { FormUnit } from './form-unit';

Expand Down Expand Up @@ -47,12 +47,12 @@ export namespace SharedFormUnit {
extends Shared.Descriptor<TUnit, TClass> {

/**
* Predefined share of the form to add the unit to, or `undefined` when unknown.
* Predefined locator function of the form to add the unit to, or `undefined` when unknown.
*/
readonly formShare?: ComponentShare<Form<any, any>>;
readonly locateForm?: ComponentShareLocator.Fn<Form<any, any>>;

/**
* Predefined unit name, or `null`/`undefined` when the unit is not to be added to the {@link formShare form}.
* Predefined unit name, or `null`/`undefined` when the unit is not to be added to the {@link locateForm form}.
*/
readonly name?: string | null;

Expand Down
Loading