Skip to content

Commit

Permalink
feat: refactor for form machine, component (#140)
Browse files Browse the repository at this point in the history
* fix: update types for form machine WIP

* fix: remove default styling from form-element WIP

* test: update tests

* chore: replace observable with promise in FormValidator

* fix: fixed demo validator

* chore: removed unused imports

* chore: use hydrate for form-elements WIP

* test: bump coverage
  • Loading branch information
lem-onade authored Jan 18, 2022
1 parent 138cb6e commit e234a32
Show file tree
Hide file tree
Showing 14 changed files with 472 additions and 292 deletions.
71 changes: 70 additions & 1 deletion packages/dgt-components/demo/demo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,68 @@ import { css, html, unsafeCSS } from 'lit-element';
import { RxLitElement } from 'rx-lit';
import { Theme } from '@digita-ai/dgt-theme';
import { CheckboxComponent } from '../lib/components/checkbox/checkbox.component';
import { createMachine, interpret, Interpreter, StateMachine } from 'xstate';
import { FormContext, FormStateSchema, FormState, formMachine } from '../lib/components/forms/form.machine';
import { FormEvent, FormUpdatedEvent } from '../lib/components/forms/form.events';
import { FormValidator } from '../lib/components/forms/form-validator';
import { FormElementComponent } from '../lib/components/forms/form-element.component';
import { define } from '../lib/util/define';
import { hydrate } from '../lib/util/hydrate';


const emailValidator: FormValidator<{ email: string }> = async (context, event) => {

if (!context.data) return [];

const { email } = context.data;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const updatedField = (event as FormUpdatedEvent).field;

// email checks
if (updatedField === 'email') {

if (!email || email.length < 1) {

return [ { message: 'This field is required', field: 'email' } ];

}

const emailRegex = /^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/u;

if (!emailRegex.test(email)) {

return [ { message: 'Please enter a valid e-mail', field: 'email' } ];

}

}

return [];

};

export class DemoComponent extends RxLitElement {

// eslint-disable-next-line max-len
private formMachine: StateMachine<FormContext<{ email: string }>, FormStateSchema<{ email: string }>, FormEvent, FormState<{ email: string }>>;
// eslint-disable-next-line max-len
private formActor: Interpreter<FormContext<{ email: string }>, FormStateSchema<{ email: string }>, FormEvent, FormState<{ email: string }>>;

constructor() {
super();
customElements.define('checkbox-component', CheckboxComponent);

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.formMachine = createMachine<FormContext<{ email: string }>, FormEvent, FormState<{ email: string }>>(formMachine<{ email: string }>(emailValidator)).withContext({
data: { email: '' },
original: { email: '' },
});

// eslint-disable-next-line no-console,@typescript-eslint/no-unsafe-assignment
this.formActor = interpret(this.formMachine, { devTools: true }).onTransition((s) => console.log(s.value));
this.formActor.start();

define('checkbox-component', CheckboxComponent);
define('form-element', hydrate(FormElementComponent)(this.formActor));

}

Expand All @@ -28,12 +84,21 @@ export class DemoComponent extends RxLitElement {
render() {

return html`
<h1>checkbox component</h1>
<form>
<checkbox-component @change="${this.onCheckboxClicked}">I agree</checkbox-component>
<checkbox-component @change="${this.onCheckboxClicked}">I consent</checkbox-component>
<checkbox-component @change="${this.onCheckboxClicked}">I would like to receive promotional e-mails</checkbox-component>
<button disabled @click="${this.onButtonClicked}">Continue</button>
</form>
<h1>form element component</h1>
<form>
<input placeholder="non form-element input field">
<form-element field="email">
<input slot="input" type="email" name="email" id="email" placeholder="enter e-mail address">
</form-element>
<button>Continue</button>
</form>
`;

}
Expand All @@ -53,6 +118,10 @@ export class DemoComponent extends RxLitElement {
padding: var(--gap-small);
background-color: white;
}
input {
width: 100%;
box-sizing: border-box;
}
`,
];

Expand Down
2 changes: 0 additions & 2 deletions packages/dgt-components/demo/demo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Parser, Store } from 'n3';
import { addListener, ComponentEventTypes, ComponentReadEvent, ComponentResponseEvent, ComponentWriteEvent } from '@digita-ai/semcom-sdk';
import {FormElementComponent} from '../lib/components/forms/form-element.component';
import {CardComponent} from '../lib/components/cards/card.component';
import { DemoAuthenticateComponent } from './demo-authenticate.component';
import { ListItemComponent } from '../lib/components/list-item/list-item.component';
Expand All @@ -9,7 +8,6 @@ import { DemoComponent } from './demo.component';


customElements.define('demo-auth', DemoAuthenticateComponent);
customElements.define('nde-form-element', FormElementComponent);
customElements.define('nde-card', CardComponent);
customElements.define('list-item', ListItemComponent);
customElements.define('demo-component', DemoComponent);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Purpose } from '../../models/purpose.model';
import { define } from '../../util/define';
import { ConsentRequestComponent } from './consent-request.component';

const purpose: Purpose = {
uri: 'https://purpose.uri/',
description: 'test description',
predicates: [ 'https://schema.org/name' ],
icon: 'https://icon.uri/',
};

describe('ConsentRequestComponent', () => {

let component: ConsentRequestComponent;

beforeEach(() => {

define('consent-request', ConsentRequestComponent);

component = window.document.createElement('consent-request') as ConsentRequestComponent;
component.purpose = purpose;

});

it('should instantiate', async () => {

window.document.body.appendChild(component);
await component.updateComplete;

expect(component).toBeTruthy();
expect(component).toBeInstanceOf(ConsentRequestComponent);

});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Purpose } from '../../models/purpose.model';
import { define } from '../../util/define';
import { ConsentResultComponent } from './consent-result.component';

const purpose: Purpose = {
uri: 'https://purpose.uri/',
description: 'test description',
predicates: [ 'https://schema.org/name' ],
icon: 'https://icon.uri/',
};

describe('ConsentResultComponent', () => {

let component: ConsentResultComponent;

beforeEach(() => {

define('consent-result', ConsentResultComponent);

component = window.document.createElement('consent-result') as ConsentResultComponent;
component.purpose = purpose;

});

it('should instantiate', async () => {

window.document.body.appendChild(component);
await component.updateComplete;

expect(component).toBeTruthy();
expect(component).toBeInstanceOf(ConsentResultComponent);

});

});
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ArgumentError } from '@digita-ai/dgt-utils';
import { Observable, of } from 'rxjs';
import { interpret, Interpreter, StateSchema } from 'xstate';
import { State } from '../state/state';
import { createMachine, interpret, Interpreter } from 'xstate';
import { define } from '../../util/define';
import { hydrate } from '../../util/hydrate';
import { FormElementComponent } from './form-element.component';
import { FormValidatorResult } from './form-validator-result';
import { FormEvent, FormEvents } from './form.events';
import { FormContext, formMachine, FormStates } from './form.machine';
import { FormEvent, FormEvents, FormSubmittedEvent } from './form.events';
import { FormContext, formMachine, FormState, FormStateSchema, FormSubmissionStates } from './form.machine';

interface TData {
name: string;
Expand All @@ -16,35 +16,28 @@ interface TData {
describe('FormElementComponent', () => {

let component: FormElementComponent<TData>;

let machine: Interpreter<
FormContext<TData>,
StateSchema<FormContext<TData>>,
FormEvent,
State<FormStates, FormContext<TData>>
>;

let input;
let machine: Interpreter<FormContext<TData>, FormStateSchema<TData>, FormEvent, FormState<TData>>;
let input: HTMLInputElement;

beforeEach(() => {

machine = interpret(
formMachine<TData>(
(context: FormContext<TData>, event: FormEvent): Observable<FormValidatorResult[]> => of([
createMachine<FormContext<TData>, FormEvent, FormState<TData>>(formMachine<TData>(
async (context: FormContext<TData>) => [
...context.data && context.data.name ? [] : [ { field: 'name', message: 'demo-form.name.required' } ],
...context.data && context.data.uri ? [] : [ { field: 'uri', message: 'demo-form.uri.required' } ],
]),
)
],
))
.withContext({
data: { uri: '', name: 'Test', description: 'description' },
original: { uri: '', name: 'Test', description: 'description' },
validation: [],
}),
);

component = window.document.createElement('nde-form-element') as FormElementComponent<TData>;
define('form-element', hydrate(FormElementComponent)(machine));
component = window.document.createElement('form-element') as FormElementComponent<TData>;

component.actor = machine;
component.field = 'name';
component.data = { uri: '', name: 'Test', description: 'description' };

Expand Down Expand Up @@ -94,7 +87,7 @@ describe('FormElementComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

expect((window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.field slot').assignedElements()[0] as HTMLInputElement).value).toBe('Test');
expect((window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.field slot').assignedElements()[0] as HTMLInputElement).value).toBe('Test');

});

Expand Down Expand Up @@ -132,7 +125,7 @@ describe('FormElementComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

// const input = window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.input slot').assignedElements()[0] as HTMLInputElement;
// const input = window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.input slot').assignedElements()[0] as HTMLInputElement;

input.value = 'Lorem';
input.dispatchEvent(new Event('input'));
Expand All @@ -146,8 +139,8 @@ describe('FormElementComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelectorAll<HTMLSlotElement>('.results .result').length).toBe(1);
expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelectorAll<HTMLSlotElement>('.help[hidden]').length).toBe(1);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelectorAll<HTMLSlotElement>('.results .result').length).toBe(1);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelectorAll<HTMLSlotElement>('.help[hidden]').length).toBe(1);

});

Expand All @@ -156,10 +149,10 @@ describe('FormElementComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.help slot').assignedElements().length).toBe(1);
expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.label slot').assignedElements().length).toBe(1);
expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.icon slot').assignedElements().length).toBe(1);
expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.action slot').assignedElements().length).toBe(1);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.help slot').assignedElements().length).toBe(1);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.label slot').assignedElements().length).toBe(1);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.icon slot').assignedElements().length).toBe(1);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.action slot').assignedElements().length).toBe(1);

});

Expand All @@ -170,7 +163,7 @@ describe('FormElementComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelectorAll<HTMLDivElement>('.icon .loading').length).toEqual(1);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelectorAll<HTMLDivElement>('.icon .loading').length).toEqual(1);

});

Expand All @@ -181,7 +174,7 @@ describe('FormElementComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelectorAll<HTMLDivElement>('.icon .loading').length).toEqual(0);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelectorAll<HTMLDivElement>('.icon .loading').length).toEqual(0);

});

Expand All @@ -192,7 +185,7 @@ describe('FormElementComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelectorAll<HTMLDivElement>('.icon slot[name="icon"]').length).toEqual(1);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelectorAll<HTMLDivElement>('.icon slot[name="icon"]').length).toEqual(1);

});

Expand All @@ -203,18 +196,28 @@ describe('FormElementComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

expect(window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelectorAll<HTMLDivElement>('.icon slot[name="icon"]').length).toEqual(0);
expect(window.document.body.getElementsByTagName('form-element')[0].shadowRoot.querySelectorAll<HTMLDivElement>('.icon slot[name="icon"]').length).toEqual(0);

});

it('should disable input when locked', async () => {

component.lockInput = true;
machine.send(new FormSubmittedEvent());

window.document.body.appendChild(component);
await component.updateComplete;

expect(input.disabled).toBeTruthy();
machine.onTransition((state) => {

if (state.matches(FormSubmissionStates.SUBMITTED)) {

expect(input.disabled).toBeTruthy();

}

});

machine.start();

});

Expand All @@ -232,12 +235,17 @@ describe('FormElementComponent', () => {
describe('bindActorToInput', () => {

const slot: HTMLSlotElement = {
...window.document.createElement('input'),
...window.document.createElement('input') as any,
assignedElements: jest.fn(),
assignedNodes: jest.fn(),
};

const actor = interpret(formMachine<any>((context, event): any => of([])));
const actor = interpret(
createMachine<FormContext<TData>, FormEvent, FormState<TData>>(formMachine<TData>(
async () => []
))
);

const data = { name: '', description: '', uri: '' };

it('should throw when slot in undefined', async() => {
Expand Down Expand Up @@ -275,4 +283,3 @@ describe('FormElementComponent', () => {
});

});

Loading

0 comments on commit e234a32

Please sign in to comment.