vuelidify 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +91 -28
  2. package/package.json +9 -9
package/README.md CHANGED
@@ -7,10 +7,14 @@ Powerful and typed model-based validation for Vue 3
7
7
 
8
8
  [Examples](#examples)
9
9
 
10
- [Technical Details](#technical-details)
10
+ [Provided Validators](#provided-validators)
11
11
 
12
12
  [Custom Validators](#custom-validators)
13
13
 
14
+ [Throttle Functions](#throttle-functions)
15
+
16
+ [Technical Details](#technical-details)
17
+
14
18
  ---
15
19
 
16
20
  This library was inspired by Vuelidate and sought to solve some of its biggest problems. This library does NOT support Vue2, and does NOT support commonJS. Technology must move forward.
@@ -175,18 +179,18 @@ type BaseValidationReturn<F> = {
175
179
  ```
176
180
  Here is the breakdown of the parameters that are passed into validators
177
181
  ```ts
178
- type ValidatorParams<T,P,V,A> = {
182
+ type ValidatorParams<T,KModel,Args,Ancestors> = {
179
183
  // The value of the property being validated
180
184
  value: T,
181
185
  // The top-most ancestor being validated. The object that was passed to the composable.
182
- parent: P,
186
+ model: KModel,
183
187
  // The args that were specified in the composable configuration.
184
- args: V,
188
+ args: Args,
185
189
  // The type will be an ordered array of strongly typed objects.
186
190
  // Each index is an ancestor to what you're validating.
187
191
  // Index 0 will appear when you're 1 array deep, and index 1 will appear 2 arrays deep, etc.
188
192
  // Extremely useful for complex validation.
189
- arrayAncestors: A
193
+ arrayAncestors: Ancestors
190
194
  }
191
195
  ```
192
196
 
@@ -355,7 +359,7 @@ Sometimes your objects will contain objects and arrays.
355
359
  </script>
356
360
  ```
357
361
  ### Validation State
358
- All of the validation types present their results similarly. Just access `state` to get started! As long as you have TypeScript enabled in your workspace, you should have no problem understanding its layout. Here's a short example on what you can do with the state object.
362
+ In order to access the results of validation, access the `state` property returned from the composable. As long as you have TypeScript enabled in your workspace, you should have no problem understanding its layout. Validation state copies the layout of your model and puts state objects where values should be instead. Here's a short example on what you can do with the state object.
359
363
  ```ts
360
364
  const v$ = useValidation({
361
365
  // ... complex object validation provided earlier ...
@@ -365,25 +369,42 @@ const aAgeErrors: string[] | undefined = v$.state.a?.age?.$state?.errorMessages;
365
369
  // An array of validation state objects on each person of property b.
366
370
  const bErrors = v$.state.b?.$arrayState;
367
371
  ```
368
- Note, properties may show up in the intellisense, but they are undefinable *on purpose*. If validation rules are not provided for a property, its state object will not exist.
369
-
370
- ## Technical Details
371
- For those interested in the inner workings of the library without looking at the code:
372
-
373
- - Reactive validation is performed by a deep watcher on the provided model. This was done because of inter-property dependence. When a validator for one property relies on another property in the object, it needs to be reevaluated. This does come with the technical debt of running *every* reactive validator *every* time the model is changed. However, the problem is mediated by validator optimizations which is discussed later.
374
- - Lazy validation is only performed only when the `validate()` function is called. However, `validate()` will also invoke all reactive validators to guarantee all validation results are up-to-date with the model. Properties or the model itself may be valid before ever calling `validate()` if there were no lazy validators provided, and all reactive validators were true (or again none specified).
375
- - Async validators can be mixed with sync validators, so there is no way to distinguish them upon initialization. However, once they are invoked for the first time, it is possible to distinguish them. Optimizations can then be made on the sync and async validators to improve validation behavior and performance. Sync validators will be wrapped in a computed function which has the benefit of determining reactive dependencies and caching the result. This counteracts the downside of using a deep watcher discussed previously. Synchronous validators will not be needlessly reevaluated every time a character changes in an unrelated property because the computed determines it doesn't rely on it. Async validators will be optimized based on how long they take to return. If they return faster than 250ms, they will not be given any optimization; if they return in less than 500ms, they will be given a throttle of 250ms; if they return longer than that they will be given a buffer. Details of the throttles are below.
376
- - `throttleAsync` is an async throttler that preserves your function’s signature and always returns a Promise of its result. It executes immediately when idle; if called during the throttle interval, it buffers only the latest call and runs it once the interval elapses—earlier buffered calls resolve to `undefined`. This lets you schedule non-blocking, promise-based throttling without overlapping executions and guarantees you always call your function with the latest arguments. Note, you are unable to distinguish if your function returned `undefined` or `throttleAsync` returned `undefined`.
377
- - `bufferAsync` is another custom function exported by this library that provides a more aggressive throttling behavior than `throttleAsync`. `bufferAsync` preserves the original function’s signature and returns a promise resolving to its result. It only remembers the latest invocation while the provided function is still executing. Once the current execution completes, only the remembered call will be invoked — all intermediate calls will return `undefined`. This mechanism prevents overlapping executions and reduces redundant work, making it ideal for expensive async operations where only the most recent intent should be executed. Note, you are unable to distinguish if your function returned `undefined` or `bufferAsync` returned `undefined`.
378
- - Returning arrays of validators from within other validators is powerful but complex. Initially, we aimed to optimize these nested validators, but their dynamic nature--varying instances, order, and presence between iterations--made this unreliable. Since their results merge with all other validators, Vuelidify tracks and removes outdated results when the "parent" validator is invoked again and the same results are not returned.
379
- - This library uses `unknown` instead of `any` to align with Deno and strict TypeScript standards. While `Args` and `Ancestors` are logically `undefined` by default, using `undefined` as a type causes issues—`unknown` can't be assigned to `undefined`. This distinction is why some of Vuelidify’s types may seem unusual, especially when creating generic validators meant to work universally. Additionally, the default type of `Return` is `any` because it truly *can be* anything. If it was unknown, you would be unable to access it for anything. `Return` is the only part of Vuelidify that is not strongly-typed. Be sure to cast it to what you expect before using it.
372
+ Note, your properties may show up in the intellisense for `state`, but they are undefinable *on purpose*. If validation rules are not provided for a property, its state object will not exist.
373
+
374
+ # Provided Validators
375
+ Here are the validators that Vuelidify provides by default:
376
+
377
+ - ```ts
378
+ required()
379
+ ```
380
+ - ```ts
381
+ minLength(min: number)
382
+ ```
383
+ - ```ts
384
+ maxLength(max: number)
385
+ ```
386
+ - ```ts
387
+ minNumber(min: number)
388
+ ```
389
+ - ```ts
390
+ maxNumber(max: number)
391
+ ```
392
+ - ```ts
393
+ must(fn: (params) => boolean, errorMessage: string)
394
+ ```
395
+ - ```ts
396
+ validateIf(predicate: (params) => boolean | Promise<boolean>, validators: Validator[])
397
+ ```
398
+ - ```ts
399
+ isEmailSync()
400
+ ```
401
+
402
+ There aren't many validators provided by this library on purpose. If you feel a validator would be useful for everyone to have, give us feedback on our GitHub repository. However, we highly encourage you to understand how to make your own!
380
403
 
381
404
  # Custom Validators
382
- There aren't many validators provided by this library on purpose. Mostly because I would rather rely on feedback for useful validators. Feel free to give me feedback on the github repo.
383
-
384
- I highly encourage you to understand the types enough to create your own validators. It isn't too difficult, and you should be able to base your validator off of existing ones.
405
+ This section guides you to create your own generic validators. Validators were designed to be easy to create and easier to use.
385
406
 
386
- Here is a breakdown of one of the validators exported by this library (expanded to make comments more readable):
407
+ Here is a breakdown of one of the built-in validators (expanded to make comments more readable):
387
408
 
388
409
  ```ts
389
410
  // always provide a header comment to explain what the validator does!
@@ -397,24 +418,23 @@ export function isEmailSync<
397
418
  T extends string | undefined | null,
398
419
  // The type for the model parameter.
399
420
  // Generally you don't put constraints on this.
400
- P,
421
+ K,
401
422
  // The type for the args
402
423
  // You may want to put a constraint on this if you need access to a store, or some other external data.
403
424
  V,
404
425
  // The type for the custom return from the validator
405
426
  R,
406
- // The type for the arrayParents parameter.
427
+ // The type for the arrayAncestors parameter.
407
428
  // Generally you don't put constraints on this.
408
- // But you have to accept this generic in order to pass the type forward to not mess up outside types.
409
429
  A
410
430
  >(
411
- // Specify any parameters you need here. This is a function that returns a validator.
412
- ): SyncValidator<T, P, V, R, A> // Explicitly type the validator you'll be returning
431
+ // Specify any parameters you need here. This can be configuration (like a max length) or reactive variables.
432
+ ): SyncValidator<T, K, V, R, A> // Explicitly type the validator you'll be returning
413
433
  {
414
434
  // Return a validator function
415
435
  return (
416
436
  // Strongly type the expected params object to have intellisense
417
- params: ValidatorParams<T, P, V, A>
437
+ params: ValidatorParams<T, K, V, A>
418
438
  ) => {
419
439
  // you can do whatever you want a normal validator can in here.
420
440
  // Return undefined, an array of validators, or a validation result.
@@ -443,4 +463,47 @@ export function isEmailSync<T extends string | undefined | null>(): SyncValidato
443
463
 
444
464
  This validator is effectively: `SyncValidator<string | undefined | null, unknown, unknown, unknown, unknown>`
445
465
 
466
+ ## Throttle Functions
467
+ Vuelidify provides several throttling functions for limiting how often a function can be invoked. Internally, Vuelidify uses some of these internally to optimize async validators, but we figured they could be useful outside of just validation. Functions like `debounce` and `throttle` are common examples exported by lodash. However, lodash's implementations are often hard to use because they don't return control back to the caller (i.e. they don't return a promise). The functions Vuelidify provides strongly type themselves to the function you provide, and always return a promise when it makes sense to. Here is a list of the available throttling functions:
468
+
469
+ - ```bufferAsync``` ensures that the provided function has only one instance executing at a time. Calls to the buffered function will return a promise to execute when the current instance returns. Only the latest promise will execute the function next, all other promises will resolve to `IGNORE_RESULT` once the current instance returns. This function is very useful for only invoking resource-heavy functions while guaranteeing each execution uses the most up-to-date parameters.
470
+ - ```throttleBufferAsync``` behaves very similarly to `bufferAsync`, but instead of waiting for the current instance to return, it waits for a throttle duration to expire. Once the throttle expires the latest buffered promise will execute the function and all others will resolve to `IGNORE_RESULT`. This means multiple instances of the function could be running at the same time, depending on the throttle and how long the function actually takes to return.
471
+ - ```throttleAsync``` ensures a function can only be invoked once every throttle period. Does not use buffering. Returns two objects, a ref indicating if the function is in its throttle period and the throttled function. Useful for hard limiting invocation of a function (e.g. forgot password form submission). Does not guarantee the latest invocation will be executed because invocations during the throttle period return `IGNORE_RESULT`.
472
+ - ```trailingDebounceAsync``` ensures a function will only be invoked after a delay has passed since the last call. Guarantees the latest parameters will be executed. All calls prior to the latest will return `IGNORE_RESULT`.
473
+
474
+ `IGNORE_RESULT` is a constant unique symbol exported by Vuelidify that helps you to identify when invocations are ignored by a throttle function. This was done to make sure this unique state doesn't conflict with possible returns from your own functions.
475
+
476
+ ## Technical Details
477
+ For those interested in the inner workings of the library without looking at the code:
478
+
479
+ - Reactive validation is performed by a deep watcher on the provided model. This was done because of inter-property dependence. When a validator for one property relies on another property in the object, it needs to be reevaluated. This does come with the technical debt of running *every* reactive validator *every* time the model is changed. However, the problem is mediated by validator optimizations which is discussed later.
480
+
481
+ - Lazy validation is only performed only when the `validate()` function is called. However, `validate()` will also invoke all reactive validators to guarantee all validation results are up-to-date with the model. Properties or the model itself may be valid before ever calling `validate()` if there were no lazy validators provided, and all reactive validators were true (or again none specified).
482
+
483
+ - Validation results should appear as soon as possible. Synchronous validators shouldn’t wait for asynchronous ones to finish before showing errors. But things get complex when mixing lazy and reactive validators that run at different times or modify the same data. It’s even trickier when validators return other validators—sometimes inconsistently across runs. Vuelidify solves these problems with a robust validation system:
484
+ - Each validator gets a unique ID.
485
+
486
+ - All validators are run concurrently and their results are processed as they come back.
487
+
488
+ - Any child validators returned inherit the parent's ID with a unique suffix.
489
+
490
+ - All child validators are evaluated immediately and tracked within the origin validator.
491
+
492
+ - If a parent validator returns a different set of children in a future run, the ones that are no longer present are removed.
493
+
494
+ - Validation configs are objects which are created to store any necessary data to validate a value. The validation configs that are created are entirely dependent on the validation rules provided. These validation configs make heavy use of Vue's reactivity system to ensure references to your object and its nested properties stays up-to-date. These configs are not exposed to developers directly, but `state` and values like `isValidating` are determined using these configs. The use of these configs greatly improve readability and maintainability of Vuelidify.
495
+
496
+ - Validation configs are usually created as soon as the composable is invoked, but in the case of arrays, they must be created dynamically with the content of the array. There's some nuance to using the `$each` validation rule to validate arrays. Validation of arrays works by creating a matching array of validation configs for each element of the array. In the case of arrays of primitives, it is impossible to assign IDs to each element such that they can map to a validation config which was created. On the other hand, each object in an array can be modified with an ID mapping an object to a validation config. This is an important feature, because the validation state for an array is meant to match 1:1 with the array validated. However, what happens when the elements shift around? <br/>
497
+ When the indexes of objects are modified, Vuelidify reactively maps the objects to their validation config and returns the correctly ordered state. This is important specifically for *lazy* validators--which likely won't validate every time an index changes. So if error messages from lazy validation are on an object whose index moves, they need to follow that object around. This does not behave correctly with primitives; basically, only the index is used to map validation configs to values. **Make sure to NOT use lazy validation on primitives in arrays which can change indexes.**
498
+
499
+ - Both lazy and reactive validations track their own iteration ID. Before updating anything, iterations check if they're still the most recent. This prevents outdated runs from making state changes like setting the `isValidating` ref to false when the latest validation is actually still validating.
500
+
501
+ - When results are returned from validation, Vuelidify updates existing entries instead of replacing them. This avoids flickering or reordering in the results array—especially useful if you want to animate error messages smoothly.
502
+
503
+ - Async validators can be mixed with sync validators, so there is no way to distinguish them upon initialization. However, once they are invoked for the first time, it is possible to distinguish them. Optimizations can then be made on the synchronous validators to improve validation behavior and performance. Sync validators will be wrapped in a computed function which has the benefit of determining reactive dependencies and caching the result. This counteracts the downside of using a deep watcher discussed previously. Synchronous validators will not be needlessly reevaluated every time a character changes in an unrelated property because the computed determines its own reactive dependencies.
504
+
505
+ - **As of v2.1, all validators are guaranteed to run exclusively.** No validator will execute concurrently with itself. This removed the need for manual optimization of async-validators in earlier versions because all validators now essentially have a `bufferAsync` on them. This also has the added benefit of allowing developers to have piece of mind that their synchronous and asynchronous validators won't have weird race conditions because of concurrency. While a validator is being ran and its return is processed, all validation attempts are queued to happen after it returns. Before the buffered validations run, they check if they are the latest iteration, and if they aren't they return immediately. This means no matter how many times validation is invoked, only the latest state is validated. This decision for exclusivity was made because of a bug that arose around the `validateIf` validator, where race-conditions were happening with modifying the internal state of the validator to keep track of child validators it returned.
506
+
507
+ - This library uses `unknown` a lot instead of `any` to align with Deno and strict TypeScript standards. While the common generics, `Args` and `Ancestors` are logically `undefined` by default, using `undefined` as a type causes issues—`unknown` can't be assigned to `undefined`. This distinction is why some of Vuelidify’s types may seem unusual, especially when creating generic validators meant to work universally. Additionally, the default type of `Return` is `any` because it truly *can be* anything; if it was unknown, you wouldn't be able to access it from state. There are only a handful of spots in Vuelidify where any is used (e.g. throttle functions).
508
+
446
509
  Feel free to post issues you may have with the package on the git repo! Happy validating!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vuelidify",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "author": "Daniel Walbolt",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",
@@ -19,20 +19,20 @@
19
19
  "url": "https://github.com/Daniel-Walbolt/vue-final-form"
20
20
  },
21
21
  "dependencies": {
22
- "vue": "^3.4.38"
22
+ "vue": "^3.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@tsconfig/node20": "^20.1.4",
26
- "@types/node": "^20.16.2",
27
- "@vitejs/plugin-vue": "^5.1.3",
25
+ "@tsconfig/node20": "^20.1.5",
26
+ "@types/node": "^20.17.46",
27
+ "@vitejs/plugin-vue": "^5.2.4",
28
28
  "@vue/eslint-config-typescript": "^13.0.0",
29
29
  "@vue/tsconfig": "^0.5.1",
30
- "eslint": "^8.57.0",
31
- "eslint-plugin-vue": "^9.27.0",
32
- "npm-run-all2": "^6.2.2",
30
+ "eslint": "^8.57.1",
31
+ "eslint-plugin-vue": "^9.33.0",
32
+ "npm-run-all2": "^6.2.6",
33
33
  "tsup": "^6.7.0",
34
34
  "typescript": "~5.4.5",
35
- "vue-tsc": "^2.1.2"
35
+ "vue-tsc": "^2.2.10"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsup src/index.ts --format esm --dts --minify",