Skip to content

Commit 78faf53

Browse files
authored
Merge pull request #962 from Trendyol/form-handling
refactor: improve form validation
2 parents d8ed355 + 3f37995 commit 78faf53

File tree

11 files changed

+258
-22
lines changed

11 files changed

+258
-22
lines changed

playground/template.html

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<!DOCTYPE html>
2-
<html lang="en">
2+
<html lang="tr">
33
<head>
44
<meta charset="UTF-8" />
55
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
66
<title>Baklava Playground</title>
77
<link href="./dist/themes/default.css" rel="stylesheet" />
8+
<script src="./dist/localization.js" type="module"></script>
89
<script src="./dist/baklava.js" type="module"></script>
910
<script>
1011
// Live reload
@@ -27,11 +28,15 @@
2728
<body>
2829
<h1>Baklava Playground</h1>
2930

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

3533
<bl-button>Baklava is ready</bl-button>
3634
</body>
35+
36+
<script>
37+
const sa = document.querySelector("#sa");
38+
sa.updateComplete.then(() => {
39+
sa.forceCustomError();
40+
})
41+
</script>
3742
</html>

src/components/input/bl-input.stories.mdx

+31-8
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ import { extraPadding } from '../../utilities/chromatic-decorators';
7979
},
8080
helpText: {
8181
control: 'text'
82-
}
82+
},
83+
error: {
84+
control: 'text',
85+
},
8386
}}
8487
/>
8588

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

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

252-
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.
256+
### Custom Error Text
257+
258+
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.
259+
260+
```html
261+
<bl-input id="input" required />
262+
263+
<script>
264+
const blInput = document.getElementById("input");
265+
blInput.addEventListener("bl-input", (e) => {
266+
if(e.target.validity.valueMissing){
267+
e.target.setCustomValidity("Custom Error Text");
268+
}else{
269+
e.target.setCustomValidity("");
270+
}
271+
});
272+
</script>
273+
```
274+
275+
### Custom Validation
276+
277+
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.*
278+
279+
<bl-alert icon variant="warning">When you use this attribute, the `dirty` prop will instantly become true.</bl-alert>
253280

254281
<Canvas>
255-
<Story name="Custom Error Message"
256-
args={{ type: 'text', label: 'User Name', required: true, customInvalidText: 'This field is mandatory' }}
282+
<Story name="Custom Validation"
283+
args={{ type: 'text', label: 'User Name', error: 'I am custom validation' }}
257284
>
258285
{SingleInputTemplate.bind({})}
259286
</Story>
260287
</Canvas>
261288

262-
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
263-
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
264-
you can run your custom validation logic outside of the basic validation rules we provide with validation attributes.
265-
266289
## Input Sizes
267290

268291
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.

src/components/input/bl-input.test.ts

+37
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,43 @@ describe("bl-input", () => {
204204
expect(errorMessageElement).to.visible;
205205
});
206206

207+
it("should show custom error", async () => {
208+
const errorMessage = "This field is mandatory";
209+
const el = await fixture<BlInput>(
210+
html`<bl-input error="${errorMessage}"></bl-input>`
211+
);
212+
213+
await elementUpdated(el);
214+
215+
const errorMessageElement = <HTMLParagraphElement>(
216+
el.shadowRoot?.querySelector(".invalid-text")
217+
);
218+
219+
expect(el.validity.valid).to.be.false;
220+
221+
expect(errorMessageElement).to.exist;
222+
expect(errorMessageElement?.innerText).to.equal(errorMessage);
223+
});
224+
225+
it("should show custom invalid text", async () => {
226+
const invalidText = "This field is mandatory";
227+
const el = await fixture<BlInput>(html`<bl-input required></bl-input>`);
228+
229+
el.setCustomValidity(invalidText);
230+
el.setValue(el.value);
231+
el.reportValidity();
232+
233+
await elementUpdated(el);
234+
235+
expect(el.validity.valid).to.be.false;
236+
const errorMessageElement = <HTMLParagraphElement>(
237+
el.shadowRoot?.querySelector(".invalid-text")
238+
);
239+
240+
expect(errorMessageElement).to.visible;
241+
expect(errorMessageElement?.innerText).to.equal(invalidText);
242+
});
243+
207244
it("should set custom error state with forceCustomError method", async () => {
208245
const el = await fixture<BlInput>(html`<bl-input></bl-input>`);
209246

src/components/input/bl-input.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from "lit/decorators.js";
33
import { classMap } from "lit/directives/class-map.js";
44
import { ifDefined } from "lit/directives/if-defined.js";
55
import { live } from "lit/directives/live.js";
6+
import { localized, msg } from "@lit/localize";
67
import { FormControlMixin } from "@open-wc/form-control";
78
import { submit } from "@open-wc/form-helpers";
89
import "element-internals-polyfill";
@@ -45,6 +46,7 @@ export type InputSize = "small" | "medium" | "large";
4546
* @cssproperty [--bl-input-padding-end] Sets the padding end
4647
*/
4748
@customElement("bl-input")
49+
@localized()
4850
export default class BlInput extends FormControlMixin(LitElement) {
4951
static get styles(): CSSResultGroup {
5052
return [style];
@@ -184,17 +186,24 @@ export default class BlInput extends FormControlMixin(LitElement) {
184186

185187
/**
186188
* Overrides error message. This message will override default error messages
189+
* @deprecated use setCustomValidity instead
187190
*/
188191
@property({ type: String, attribute: "invalid-text", reflect: true })
189192
set customInvalidText(value: string) {
190193
this._customInvalidText = value;
191194
this.setValue(this.value);
192195
}
193196

197+
/**
198+
* @deprecated
199+
*/
194200
get customInvalidText(): string {
195201
return this._customInvalidText;
196202
}
197203

204+
@property({ reflect: true, type: String })
205+
error: string;
206+
198207
private _customInvalidText: string;
199208

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

273+
/**
274+
* Sets a custom validity on the form element.
275+
* @param message
276+
*/
277+
setCustomValidity(message: string) {
278+
this.validationTarget.setCustomValidity(message);
279+
}
280+
264281
/**
265282
* Force to set input as in invalid state.
283+
* @deprecated use error attribute instead
266284
*/
267285
async forceCustomError() {
268286
await this.updateComplete;
269-
this.validationTarget.setCustomValidity(this.customInvalidText || "An error occurred");
287+
this.setCustomValidity(
288+
this.customInvalidText ||
289+
msg("An error occurred", { desc: "bl-input: default custom error message" })
290+
);
270291
this.setValue(this.value);
271292
this.reportValidity();
272293
}
273294

274295
/**
275296
* Clear forced invalid state
297+
* @deprecated use error attribute instead
276298
*/
277299
async clearCustomError() {
278300
await this.updateComplete;
279-
this.validationTarget.setCustomValidity("");
301+
this.setCustomValidity("");
280302
this.setValue(this.value);
281303
this.reportValidity();
282304
}
@@ -291,6 +313,7 @@ export default class BlInput extends FormControlMixin(LitElement) {
291313
const value = (event.target as HTMLInputElement).value;
292314

293315
this.value = value;
316+
this.setValue(this.value);
294317
this.onInput(value);
295318
}
296319

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

300323
this.dirty = true;
301324
this.value = value;
325+
this.setValue(this.value);
302326
this.onChange(value);
303327
}
304328

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

316340
this.requestUpdate();
317341
}
342+
343+
if (changedProperties.has("error") && this.error && !this.dirty) {
344+
this.reportValidity();
345+
}
318346
}
319347

320348
private inputId = Math.random().toString(36).substring(2);

src/components/textarea/bl-textarea.stories.mdx

+37
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ import { extraPadding } from '../../utilities/chromatic-decorators';
5050
},
5151
customInvalidText: {
5252
control: 'text'
53+
},
54+
error: {
55+
control: 'text'
5356
}
5457
}}
5558
/>
@@ -70,6 +73,7 @@ export const TextareaTemplate = (args) => html`
7073
max-rows='${ifDefined(args.maxRows)}'
7174
expand='${ifDefined(args.expand)}'
7275
size='${ifDefined(args.size)}'
76+
error='${ifDefined(args.error)}'
7377
character-counter='${ifDefined(args.characterCounter)}'></bl-textarea>`
7478

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

234+
### Custom Error Text
235+
236+
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.
237+
238+
```html
239+
<bl-textarea id="textarea" required />
240+
241+
<script>
242+
const blTextarea = document.getElementById("textarea");
243+
blTextarea.addEventListener("bl-input", (e) => {
244+
if(e.target.validity.valueMissing){
245+
e.target.setCustomValidity("Custom Error Text");
246+
}else{
247+
e.target.setCustomValidity("");
248+
}
249+
});
250+
</script>
251+
```
252+
253+
### Custom Validation
254+
255+
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.*
256+
257+
<bl-alert icon variant="warning">When you use this attribute, the `dirty` prop will instantly become true.</bl-alert>
258+
259+
<Canvas>
260+
<Story name="Custom Validation"
261+
args={{ type: 'text', label: 'User Name', error: 'I am custom validation' }}
262+
>
263+
{TextareaTemplate.bind({})}
264+
</Story>
265+
</Canvas>
266+
230267
## Using within a form
231268

232269
Textarea component uses [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) to associate with it's parent form automatically.

src/components/textarea/bl-textarea.test.ts

+37
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,43 @@ describe("bl-textarea", () => {
163163
expect(errorMsgElement).to.exist;
164164
expect(errorMsgElement.innerText).to.equal(customErrorMsg);
165165
});
166+
167+
it("should show custom error", async () => {
168+
const errorMessage = "This field is mandatory";
169+
const el = await fixture<BlTextarea>(
170+
html`<bl-textarea error="${errorMessage}"></bl-textarea>`
171+
);
172+
173+
await elementUpdated(el);
174+
175+
const errorMessageElement = <HTMLParagraphElement>(
176+
el.shadowRoot?.querySelector(".invalid-text")
177+
);
178+
179+
expect(el.validity.valid).to.be.false;
180+
181+
expect(errorMessageElement).to.exist;
182+
expect(errorMessageElement?.innerText).to.equal(errorMessage);
183+
});
184+
185+
it("should show custom invalid text", async () => {
186+
const invalidText = "This field is mandatory";
187+
const el = await fixture<BlTextarea>(html`<bl-textarea required></bl-textarea>`);
188+
189+
el.setCustomValidity(invalidText);
190+
el.setValue(el.value);
191+
el.reportValidity();
192+
193+
await elementUpdated(el);
194+
195+
expect(el.validity.valid).to.be.false;
196+
const errorMessageElement = <HTMLParagraphElement>(
197+
el.shadowRoot?.querySelector(".invalid-text")
198+
);
199+
200+
expect(errorMessageElement).to.visible;
201+
expect(errorMessageElement?.innerText).to.equal(invalidText);
202+
});
166203
});
167204

168205
describe("events", () => {

src/components/textarea/bl-textarea.ts

+17
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export default class BlTextarea extends FormControlMixin(LitElement) {
2727
@query("textarea")
2828
validationTarget: HTMLTextAreaElement;
2929

30+
@property({ reflect: true, type: String })
31+
error: string;
32+
3033
/**
3134
* Name of textarea
3235
*/
@@ -175,6 +178,7 @@ export default class BlTextarea extends FormControlMixin(LitElement) {
175178
const value = (event.target as HTMLTextAreaElement).value;
176179

177180
this.value = value;
181+
this.setValue(this.value);
178182
this.onInput(value);
179183
}
180184

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

184188
this.dirty = true;
185189
this.value = value;
190+
this.setValue(this.value);
186191
this.onChange(value);
187192
}
188193

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

204209
this.requestUpdate();
205210
}
211+
212+
if (changedProperties.has("error") && this.error && !this.dirty) {
213+
this.reportValidity();
214+
}
215+
}
216+
217+
/**
218+
* Sets a custom validity on the form element.
219+
* @param message
220+
*/
221+
setCustomValidity(message: string) {
222+
this.validationTarget.setCustomValidity(message);
206223
}
207224

208225
reportValidity() {

0 commit comments

Comments
 (0)