Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: improve form validation #962

Merged
merged 4 commits into from
Jan 9, 2025
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
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
Loading