Skip to content

Commit

Permalink
Merge pull request #962 from Trendyol/form-handling
Browse files Browse the repository at this point in the history
refactor: improve form validation
  • Loading branch information
Enes5519 authored Jan 9, 2025
2 parents d8ed355 + 3f37995 commit 78faf53
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 22 deletions.
15 changes: 10 additions & 5 deletions playground/template.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<html lang="tr">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Baklava Playground</title>
<link href="./dist/themes/default.css" rel="stylesheet" />
<script src="./dist/localization.js" type="module"></script>
<script src="./dist/baklava.js" type="module"></script>
<script>
// Live reload
Expand All @@ -27,11 +28,15 @@
<body>
<h1>Baklava Playground</h1>

<p>
Copy this file as playground/index.html and try your work here by running
<code>npm run serve</code>.
</p>
<bl-input id="sa" />

<bl-button>Baklava is ready</bl-button>
</body>

<script>
const sa = document.querySelector("#sa");
sa.updateComplete.then(() => {
sa.forceCustomError();
})
</script>
</html>
39 changes: 31 additions & 8 deletions src/components/input/bl-input.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ import { extraPadding } from '../../utilities/chromatic-decorators';
},
helpText: {
control: 'text'
}
},
error: {
control: 'text',
},
}}
/>

Expand All @@ -106,6 +109,7 @@ export const SingleInputTemplate = (args) => html`<bl-input
step='${ifDefined(args.step)}'
icon='${ifDefined(args.icon)}'
size='${ifDefined(args.size)}'
error='${ifDefined(args.error)}'
>${args.slot?.()}</bl-input>`

export const SizeVariantsTemplate = args => html`
Expand Down Expand Up @@ -249,20 +253,39 @@ Input validation will run after user enters a value and go out from the input. I
</Story>
</Canvas>

Validation error messages are used from default browser error messages by default. If you want you can override error message by setting `invalid-text` attribute.
### Custom Error Text

Validation error messages are used from default browser error messages by default. If you want to override, you can do it in a native-like structure as below.

```html
<bl-input id="input" required />

<script>
const blInput = document.getElementById("input");
blInput.addEventListener("bl-input", (e) => {
if(e.target.validity.valueMissing){
e.target.setCustomValidity("Custom Error Text");
}else{
e.target.setCustomValidity("");
}
});
</script>
```

### Custom Validation

If you want to use a different validation than all validations, you can do this with the `error` attribute. *Native validators will always be superior to custom errors.*

<bl-alert icon variant="warning">When you use this attribute, the `dirty` prop will instantly become true.</bl-alert>

<Canvas>
<Story name="Custom Error Message"
args={{ type: 'text', label: 'User Name', required: true, customInvalidText: 'This field is mandatory' }}
<Story name="Custom Validation"
args={{ type: 'text', label: 'User Name', error: 'I am custom validation' }}
>
{SingleInputTemplate.bind({})}
</Story>
</Canvas>

You can also set input validation as invalid by calling `forceCustomError()` method of the input. In this case input will be in invalid state and will report
its validity. Error message can be set with `invalid-text`. To clear this error state you would call `clearCustomError()` method. With the help of these 2 methods
you can run your custom validation logic outside of the basic validation rules we provide with validation attributes.

## Input Sizes

Inputs have 3 size options: `large`, `medium` and `small`. `medium` size is default and if you want to show a large or small input you can set `size` attribute.
Expand Down
37 changes: 37 additions & 0 deletions src/components/input/bl-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,43 @@ describe("bl-input", () => {
expect(errorMessageElement).to.visible;
});

it("should show custom error", async () => {
const errorMessage = "This field is mandatory";
const el = await fixture<BlInput>(
html`<bl-input error="${errorMessage}"></bl-input>`
);

await elementUpdated(el);

const errorMessageElement = <HTMLParagraphElement>(
el.shadowRoot?.querySelector(".invalid-text")
);

expect(el.validity.valid).to.be.false;

expect(errorMessageElement).to.exist;
expect(errorMessageElement?.innerText).to.equal(errorMessage);
});

it("should show custom invalid text", async () => {
const invalidText = "This field is mandatory";
const el = await fixture<BlInput>(html`<bl-input required></bl-input>`);

el.setCustomValidity(invalidText);
el.setValue(el.value);
el.reportValidity();

await elementUpdated(el);

expect(el.validity.valid).to.be.false;
const errorMessageElement = <HTMLParagraphElement>(
el.shadowRoot?.querySelector(".invalid-text")
);

expect(errorMessageElement).to.visible;
expect(errorMessageElement?.innerText).to.equal(invalidText);
});

it("should set custom error state with forceCustomError method", async () => {
const el = await fixture<BlInput>(html`<bl-input></bl-input>`);

Expand Down
32 changes: 30 additions & 2 deletions src/components/input/bl-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { customElement, property, query, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { live } from "lit/directives/live.js";
import { localized, msg } from "@lit/localize";
import { FormControlMixin } from "@open-wc/form-control";
import { submit } from "@open-wc/form-helpers";
import "element-internals-polyfill";
Expand Down Expand Up @@ -45,6 +46,7 @@ export type InputSize = "small" | "medium" | "large";
* @cssproperty [--bl-input-padding-end] Sets the padding end
*/
@customElement("bl-input")
@localized()
export default class BlInput extends FormControlMixin(LitElement) {
static get styles(): CSSResultGroup {
return [style];
Expand Down Expand Up @@ -184,17 +186,24 @@ export default class BlInput extends FormControlMixin(LitElement) {

/**
* Overrides error message. This message will override default error messages
* @deprecated use setCustomValidity instead
*/
@property({ type: String, attribute: "invalid-text", reflect: true })
set customInvalidText(value: string) {
this._customInvalidText = value;
this.setValue(this.value);
}

/**
* @deprecated
*/
get customInvalidText(): string {
return this._customInvalidText;
}

@property({ reflect: true, type: String })
error: string;

private _customInvalidText: string;

/**
Expand Down Expand Up @@ -261,22 +270,35 @@ export default class BlInput extends FormControlMixin(LitElement) {
return this.customInvalidText || this.validationTarget?.validationMessage;
}

/**
* Sets a custom validity on the form element.
* @param message
*/
setCustomValidity(message: string) {
this.validationTarget.setCustomValidity(message);
}

/**
* Force to set input as in invalid state.
* @deprecated use error attribute instead
*/
async forceCustomError() {
await this.updateComplete;
this.validationTarget.setCustomValidity(this.customInvalidText || "An error occurred");
this.setCustomValidity(
this.customInvalidText ||
msg("An error occurred", { desc: "bl-input: default custom error message" })
);
this.setValue(this.value);
this.reportValidity();
}

/**
* Clear forced invalid state
* @deprecated use error attribute instead
*/
async clearCustomError() {
await this.updateComplete;
this.validationTarget.setCustomValidity("");
this.setCustomValidity("");
this.setValue(this.value);
this.reportValidity();
}
Expand All @@ -291,6 +313,7 @@ export default class BlInput extends FormControlMixin(LitElement) {
const value = (event.target as HTMLInputElement).value;

this.value = value;
this.setValue(this.value);
this.onInput(value);
}

Expand All @@ -299,6 +322,7 @@ export default class BlInput extends FormControlMixin(LitElement) {

this.dirty = true;
this.value = value;
this.setValue(this.value);
this.onChange(value);
}

Expand All @@ -315,6 +339,10 @@ export default class BlInput extends FormControlMixin(LitElement) {

this.requestUpdate();
}

if (changedProperties.has("error") && this.error && !this.dirty) {
this.reportValidity();
}
}

private inputId = Math.random().toString(36).substring(2);
Expand Down
37 changes: 37 additions & 0 deletions src/components/textarea/bl-textarea.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ import { extraPadding } from '../../utilities/chromatic-decorators';
},
customInvalidText: {
control: 'text'
},
error: {
control: 'text'
}
}}
/>
Expand All @@ -70,6 +73,7 @@ export const TextareaTemplate = (args) => html`
max-rows='${ifDefined(args.maxRows)}'
expand='${ifDefined(args.expand)}'
size='${ifDefined(args.size)}'
error='${ifDefined(args.error)}'
character-counter='${ifDefined(args.characterCounter)}'></bl-textarea>`

# Textarea
Expand Down Expand Up @@ -227,6 +231,39 @@ Containing form submit also triggers validation. By default it uses browsers nat
</Story>
</Canvas>

### Custom Error Text

Validation error messages are used from default browser error messages by default. If you want to override, you can do it in a native-like structure as below.

```html
<bl-textarea id="textarea" required />

<script>
const blTextarea = document.getElementById("textarea");
blTextarea.addEventListener("bl-input", (e) => {
if(e.target.validity.valueMissing){
e.target.setCustomValidity("Custom Error Text");
}else{
e.target.setCustomValidity("");
}
});
</script>
```

### Custom Validation

If you want to use a different validation than all validations, you can do this with the `error` attribute. *Native validators will always be superior to custom errors.*

<bl-alert icon variant="warning">When you use this attribute, the `dirty` prop will instantly become true.</bl-alert>

<Canvas>
<Story name="Custom Validation"
args={{ type: 'text', label: 'User Name', error: 'I am custom validation' }}
>
{TextareaTemplate.bind({})}
</Story>
</Canvas>

## Using within a form

Textarea component uses [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) to associate with it's parent form automatically.
Expand Down
37 changes: 37 additions & 0 deletions src/components/textarea/bl-textarea.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,43 @@ describe("bl-textarea", () => {
expect(errorMsgElement).to.exist;
expect(errorMsgElement.innerText).to.equal(customErrorMsg);
});

it("should show custom error", async () => {
const errorMessage = "This field is mandatory";
const el = await fixture<BlTextarea>(
html`<bl-textarea error="${errorMessage}"></bl-textarea>`
);

await elementUpdated(el);

const errorMessageElement = <HTMLParagraphElement>(
el.shadowRoot?.querySelector(".invalid-text")
);

expect(el.validity.valid).to.be.false;

expect(errorMessageElement).to.exist;
expect(errorMessageElement?.innerText).to.equal(errorMessage);
});

it("should show custom invalid text", async () => {
const invalidText = "This field is mandatory";
const el = await fixture<BlTextarea>(html`<bl-textarea required></bl-textarea>`);

el.setCustomValidity(invalidText);
el.setValue(el.value);
el.reportValidity();

await elementUpdated(el);

expect(el.validity.valid).to.be.false;
const errorMessageElement = <HTMLParagraphElement>(
el.shadowRoot?.querySelector(".invalid-text")
);

expect(errorMessageElement).to.visible;
expect(errorMessageElement?.innerText).to.equal(invalidText);
});
});

describe("events", () => {
Expand Down
17 changes: 17 additions & 0 deletions src/components/textarea/bl-textarea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export default class BlTextarea extends FormControlMixin(LitElement) {
@query("textarea")
validationTarget: HTMLTextAreaElement;

@property({ reflect: true, type: String })
error: string;

/**
* Name of textarea
*/
Expand Down Expand Up @@ -175,6 +178,7 @@ export default class BlTextarea extends FormControlMixin(LitElement) {
const value = (event.target as HTMLTextAreaElement).value;

this.value = value;
this.setValue(this.value);
this.onInput(value);
}

Expand All @@ -183,6 +187,7 @@ export default class BlTextarea extends FormControlMixin(LitElement) {

this.dirty = true;
this.value = value;
this.setValue(this.value);
this.onChange(value);
}

Expand All @@ -203,6 +208,18 @@ export default class BlTextarea extends FormControlMixin(LitElement) {

this.requestUpdate();
}

if (changedProperties.has("error") && this.error && !this.dirty) {
this.reportValidity();
}
}

/**
* Sets a custom validity on the form element.
* @param message
*/
setCustomValidity(message: string) {
this.validationTarget.setCustomValidity(message);
}

reportValidity() {
Expand Down
Loading

0 comments on commit 78faf53

Please sign in to comment.