Skip to content

Scoped validation ​

Scoped validation in Regle is made to port Vuelidate's nested component validation.

Problems with Vuelidate's approach:

  • Performances
  • Not declarative
  • Usage (too magic for the user)
  • Type safety
  • Restricted to DOM
  • Have to play with $scope and $stopPropagation to avoid unwanted behaviour

Regle's solution solves all these problems

Collecting validation with useCollectScope and useScopedRegle ​

useScopedRegle ​

useScopedRegle is a clone of useRegle, but with the difference that every time it's used and updated, its state will be collected by the same scope created using createScopedUseRegle.

Every time it's called, an instance will be added for useCollectScope to collect.

It can be called from any component in the same Vue app — not restricted by DOM hierarchy. Both useScopedRegle and useCollectScope must run with a current Vue instance (typically setup). A call made outside an app is not collected, and is not stored process-wide so SSR requests cannot leak into each other.

useCollectScope ​

This composable allow you to retrieve every Regle instances created using the sibling composable useScopedRegle.

Children properties like $value and $errors will not be objects, and are converted into arrays instead.

You will also have access to every validation properties like $error, $invalid etc...

vue
<template>
  <div>
    <Child1 />
  </div>

  <Child2 />

  Collected errors: <pre>{{ r$.$errors }}</pre>
</template>

<script setup lang="ts">
import { useCollectScope } from '@regle/core';
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';

const { r$ } = useCollectScope();
</script>
vue
<template>
  <input v-model="r$.$value.firstName" placeholder="Type your firstname" />
  <ul>
    <li v-for="error of r$.$errors.firstName" :key="error">
      {{ error }}
    </li>
  </ul>
</template>

<script setup lang="ts">
import { required } from '@regle/rules';
import { useScopedRegle } from '@regle/core';

const { r$ } = useScopedRegle({ firstName: '' }, { firstName: { required } });
</script>
vue
<template>
  <input v-model="r$.$value.email" placeholder="Type your email" />
  <ul>
    <li v-for="error of r$.$errors.email" :key="error">
      {{ error }}
    </li>
  </ul>
</template>

<script setup lang="ts">
import { required, email } from '@regle/rules';
import { useScopedRegle } from '@regle/core';

const { r$ } = useScopedRegle({ email: '' }, { email: { required, email } });
</script>

Result:




Valid: false
Collected errors:
[
  {
    "firstName": []
  },
  {
    "email": []
  }
]

Collected values:
[
  {
    "firstName": ""
  },
  {
    "email": ""
  }
]

Multiple scopes ​

If you want to create your own separated scope, you can use createScopedUseRegle helper method.

It's advised to change the name of this composable to avoid conflicts or issues.

scoped-config.ts
ts
import { createScopedUseRegle } from '@regle/core';

export const { useScopedRegle, useCollectScope } = createScopedUseRegle();
export const { 
  useScopedRegle: useContactsRegle, 
  useCollectScope: useCollectContacts 
} = createScopedUseRegle();

Namespaces inside scopes ​

Each scope can collect a specific namespace. Giving a namespace name will collect only the children with the same namespace name.

The namespace can be reactive, so it will update every time it changes.

In this example, only the components using the same scope and namespace will be collected.

vue
<script setup lang="ts">
import { useCollectScope } from '@regle/core';
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';

const { r$ } = useCollectScope('contacts');
</script>
vue
<template>
  <input v-model="r$.$value.firstName" placeholder="Type your firstname" />
  <ul>
    <li v-for="error of r$.$errors.firstName" :key="error">
      {{ error }}
    </li>
  </ul>
</template>

<script setup lang="ts">
import { required } from '@regle/rules';
import { useScopedRegle } from '@regle/core';

const { r$ } = useScopedRegle(
  { firstName: '' }, 
  { firstName: { required } },
  { namespace: 'contacts' }
);
</script>
vue
<template>
  <input v-model="r$.$value.email" placeholder="Type your email" />
  <ul>
    <li v-for="error of r$.$errors.email" :key="error">
      {{ error }}
    </li>
  </ul>
</template>

<script setup lang="ts">
import { required, email } from '@regle/rules';
import { useScopedRegle } from '@regle/core';

const { r$ } = useScopedRegle({ email: '' }, { email: { required, email } });
</script>

You can also collect multiple namespaces at once by passing an array of namespace names to the useCollectScope function.

ts
const { r$ } = useCollectScope(['contacts', 'persons']);

Inject global config ​

If you have a global config already registered, simply pass it as a parameter to your createScopedUseRegle function.

scoped-config.ts
ts
import { 
createScopedUseRegle
,
defineRegleConfig
} from '@regle/core';
import {
required
,
withMessage
} from '@regle/rules';
const {
useRegle
} =
defineRegleConfig
({
rules
: () => ({
custom
:
withMessage
(
required
, 'Custom error'),
}), }); export const {
useScopedRegle
,
useCollectScope
} =
createScopedUseRegle
({
customUseRegle
:
useRegle
});
const {
r$
} =
useScopedRegle
({
name
: ''}, {
name
: {
cus
} })

Custom store for instances ​

By default collected instances are stored per Vue app.

You can provide your own store ref. That store is process-wide: on SSR, pass a per-request ref. A module-level ref() reintroduces the cross-request leak this API otherwise avoids.

ts
import { createScopedUseRegle, type ScopedInstancesRecordLike } from '@regle/core';

// Having a default 
const myCustomStore = ref<ScopedInstancesRecordLike>({});

const { useScopedRegle, useCollectScope } = createScopedUseRegle({customStore: myCustomStore});

Collect instances in a Record ​

By default collected instances are stored in a readonly array.

If you want to store your nested instances in a record it's possible with the asRecord option.

This will require every nested useScopeRegle to provide a parameter id.

ts
import { createScopedUseRegle } from '@regle/core';

export const { 
  useScopedRegle: useScopedRegleItem, 
  useCollectScope: useCollectScopeRecord 
} = createScopedUseRegle({ asRecord: true });
vue
<script setup lang="ts">
import { useCollectScopeRecord } from './scoped-config';
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';

const { r$ } = useCollectScopeRecord<{
    child1: {firstName: string},
    child2: {email: string},
  }>();

console.log(r$.$value.child1.firstName);
</script>
vue
<template>
  <input v-model="r$.$value.firstName" placeholder="Type your firstname" />
  <ul>
    <li v-for="error of r$.$errors.firstName" :key="error">
      {{ error }}
    </li>
  </ul>
</template>

<script setup lang="ts">
import { useScopedRegleItem } from './scoped-config';

const { r$ } = useScopedRegleItem(
  { firstName: '' }, 
  { firstName: { required } },
  { id: 'child1' }
);
</script>
vue
<template>
  <input v-model="r$.$value.email" placeholder="Type your email" />
  <ul>
    <li v-for="error of r$.$errors.email" :key="error">
      {{ error }}
    </li>
  </ul>
</template>

<script setup lang="ts">
import { useScopedRegleItem } from './scoped-config';

const { r$ } = useScopedRegleItem({ email: '' }, 
  { email: { required, email } }, 
  { id: 'child2' });
</script>

Manually dispose or register a scope entry ​

useScopedRegle also returns two methods: dispose and register.

You can then programmatically handle if your component is collected from inside.

vue
<script setup lang='ts'>
import { useCollectScope } from './scoped-config';

const { r$, dispose, register } = useScopedRegle();
</script>

Manual typing ​

WARNING

Use with care, only if you're 100% sure of what return type your collected types will have.

The order of the collected values can change depending on if they added/deleted. This is here for convenience but not advised.

ts
import { 
useCollectScope
} from '@regle/core';
const {
r$
} =
useCollectScope
<[{
foo
: string }]>();
const {
valid
,
data
} = await
r$
.
$validate
();

Testing ​

If you write unit test for a component using scoped validation, you may encounter problems with the tests failing.

To solve this, mount your component with the attachTo option set to the document element.

ts
import { mount } from '@vue/test-utils';
import ComponentUsingCollectScope from './ComponentUsingCollectScope.vue';

const wrapper = mount(ComponentUsingCollectScope, {
  attachTo: document.documentElement,
});

Released under the MIT License. Logo by Johannes Lacourly