Add Input Mask Validation and Support in Form Requests and Components#776
Add Input Mask Validation and Support in Form Requests and Components#776chiragchhatrala wants to merge 18 commits intomainOpnForm/OpnForm:mainfrom
Conversation
- Introduced a new `InputMaskRule` for validating input masks in form requests, enhancing data integrity for text fields. - Updated `AnswerFormRequest` and `UserFormRequest` to include input mask validation rules, allowing for flexible input formats. - Enhanced `TextInput` component to support input masks, improving user experience with formatted input fields. - Added `useInputMask` composable for managing input mask logic, ensuring consistent behavior across components. - Updated `FieldOptions` to allow configuration of input masks for text fields, providing users with more customization options. These changes aim to improve form handling and user input validation, ensuring that data adheres to specified formats while enhancing the overall user experience in form interactions.
WalkthroughThis update introduces input mask support for text fields across both backend and frontend. It adds a custom input mask validation rule, updates form requests to handle and validate input masks, implements a composable for mask logic, and integrates a new masked input component into the form system with UI support for specifying and previewing input masks. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant FormUI
participant MaskInput
participant useInputMask
participant Backend
User->>FormUI: Opens form with text field (with input_mask)
FormUI->>MaskInput: Renders with mask prop
MaskInput->>useInputMask: Parses mask, formats input
User->>MaskInput: Types input
MaskInput->>useInputMask: Formats and validates input
MaskInput->>FormUI: Emits masked/unmasked value
FormUI->>Backend: Submits value
Backend->>InputMaskRule: Validates value against mask
InputMaskRule-->>Backend: Pass/Fail
Backend-->>FormUI: Returns validation result
FormUI-->>User: Displays success or error
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 6
🔭 Outside diff range comments (1)
client/components/forms/TextInput.vue (1)
64-67:⚠️ Potential issueAdd missing imports for Vue composition API and useInputMask.
Several Vue composition API functions and the
useInputMaskcomposable are used but not imported.import {inputProps, useFormInput} from "./useFormInput.js" import InputWrapper from "./components/InputWrapper.vue" +import { ref, computed, watch, nextTick } from 'vue' +import { useInputMask } from '../../composables/useInputMask.js'
🧹 Nitpick comments (2)
api/app/Http/Requests/UserFormRequest.php (1)
162-162: Input mask validation rule looks correct.The regex pattern appropriately restricts input mask characters to commonly used mask symbols. The
sometimes|nullablerules correctly make this optional.Consider adding a comment explaining the regex pattern for future maintainability:
+ // Input mask validation: 9=digit, a=letter, *=alphanumeric, ().-? and spaces allowed 'properties.*.input_mask' => 'sometimes|nullable|string|regex:/^[9a*().\\-?\\s]*$/',client/components/forms/TextInput.vue (1)
126-136: Reconsider the auto-clear behavior for better UX.Automatically clearing the entire input when the user backspaces might be frustrating. Consider keeping the partial input to allow users to correct mistakes without starting over.
// Auto-clear if incomplete and user is backspacing/clearing if (!isComplete(formatted) && inputValue.length > formatted.length) { - maskedValue.value = '' - compVal.value = '' - nextTick(() => { - if (inputRef.value) { - inputRef.value.value = '' - } - }) - return + // Keep the formatted partial input instead of clearing + maskedValue.value = formatted + compVal.value = formatted }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
api/app/Http/Requests/AnswerFormRequest.php(2 hunks)api/app/Http/Requests/UserFormRequest.php(1 hunks)api/app/Rules/InputMaskRule.php(1 hunks)api/tests/Unit/Rules/InputMaskRuleTest.php(1 hunks)client/components/forms/TextInput.vue(4 hunks)client/components/open/forms/OpenFormField.vue(1 hunks)client/components/open/forms/fields/components/FieldOptions.vue(1 hunks)client/composables/useInputMask.js(1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
client/composables/useInputMask.js
[error] 68-68: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🪛 GitHub Check: Run client linters
client/components/forms/TextInput.vue
[failure] 87-87:
'inputWrapperProps' is assigned a value but never used
⏰ Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: Run Feature & Unit tests (PHP 8.2 - mysql)
- GitHub Check: Build the Nuxt app
- GitHub Check: Run Feature & Unit tests (PHP 8.2 - pgsql)
🔇 Additional comments (8)
client/components/open/forms/OpenFormField.vue (1)
321-323: LGTM - Clean integration of input mask property.The conditional logic correctly passes the input mask from field configuration to the input component properties. The implementation is straightforward and integrates well with the existing property handling pattern.
client/components/open/forms/fields/components/FieldOptions.vue (1)
225-233: Well-designed UI for input mask configuration.The conditional rendering (
!field.multi_lines) appropriately restricts input masks to single-line text fields. The placeholder and help text provide excellent user guidance with clear format explanation and practical examples.api/app/Http/Requests/AnswerFormRequest.php (2)
7-7: Correct import of InputMaskRule.The import statement properly adds the InputMaskRule class for use in validation.
195-199: Solid conditional validation implementation.The logic correctly applies input mask validation only when an input mask is specified, with appropriate fallback to basic string validation. The defensive check for both existence and truthiness of the input_mask property is good practice.
api/tests/Unit/Rules/InputMaskRuleTest.php (3)
5-25: LGTM! Well-structured test for phone number mask validation.The test properly validates both passing and failing cases for phone number format masks.
27-42: LGTM! Good coverage for numeric-only mask validation.The test correctly validates that the mask accepts only numeric values and rejects alphanumeric inputs.
44-78: LGTM! Comprehensive test coverage for mixed character masks.Excellent coverage of edge cases including missing characters, extra characters, wrong character types, and missing literals. The test thoroughly validates the mask parsing and validation logic.
api/app/Rules/InputMaskRule.php (1)
12-27: LGTM! Well-structured validation flow.Good practice to validate the mask pattern before using it to validate the value. The error messages are clear and informative.
| private function parseMask(string $mask): array | ||
| { | ||
| $tokens = []; | ||
| $optional = false; | ||
| $patterns = [ | ||
| '9' => '/[0-9]/', | ||
| 'a' => '/[a-zA-Z]/', | ||
| '*' => '/[a-zA-Z0-9]/' | ||
| ]; | ||
|
|
||
| for ($i = 0; $i < strlen($mask); $i++) { | ||
| $char = $mask[$i]; | ||
|
|
||
| if ($char === '?') { | ||
| $optional = true; | ||
| continue; | ||
| } | ||
|
|
||
| $tokens[] = [ | ||
| 'char' => $char, | ||
| 'regex' => $patterns[$char] ?? null, | ||
| 'literal' => !isset($patterns[$char]), | ||
| 'optional' => $optional | ||
| ]; | ||
| } | ||
|
|
||
| return $tokens; | ||
| } |
There was a problem hiding this comment.
Fix the optional flag handling in mask parsing.
The optional flag is never reset after being set, making all subsequent tokens optional after the first '?'. This would incorrectly parse masks like "999?-999" where only the third digit should be optional.
private function parseMask(string $mask): array
{
$tokens = [];
- $optional = false;
$patterns = [
'9' => '/[0-9]/',
'a' => '/[a-zA-Z]/',
'*' => '/[a-zA-Z0-9]/'
];
for ($i = 0; $i < strlen($mask); $i++) {
$char = $mask[$i];
if ($char === '?') {
- $optional = true;
+ // Mark the previous token as optional
+ if (count($tokens) > 0) {
+ $tokens[count($tokens) - 1]['optional'] = true;
+ }
continue;
}
$tokens[] = [
'char' => $char,
'regex' => $patterns[$char] ?? null,
'literal' => !isset($patterns[$char]),
- 'optional' => $optional
+ 'optional' => false
];
}
return $tokens;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private function parseMask(string $mask): array | |
| { | |
| $tokens = []; | |
| $optional = false; | |
| $patterns = [ | |
| '9' => '/[0-9]/', | |
| 'a' => '/[a-zA-Z]/', | |
| '*' => '/[a-zA-Z0-9]/' | |
| ]; | |
| for ($i = 0; $i < strlen($mask); $i++) { | |
| $char = $mask[$i]; | |
| if ($char === '?') { | |
| $optional = true; | |
| continue; | |
| } | |
| $tokens[] = [ | |
| 'char' => $char, | |
| 'regex' => $patterns[$char] ?? null, | |
| 'literal' => !isset($patterns[$char]), | |
| 'optional' => $optional | |
| ]; | |
| } | |
| return $tokens; | |
| } | |
| private function parseMask(string $mask): array | |
| { | |
| $tokens = []; | |
| $patterns = [ | |
| '9' => '/[0-9]/', | |
| 'a' => '/[a-zA-Z]/', | |
| '*' => '/[a-zA-Z0-9]/' | |
| ]; | |
| for ($i = 0; $i < strlen($mask); $i++) { | |
| $char = $mask[$i]; | |
| if ($char === '?') { | |
| // Mark the previous token as optional | |
| if (count($tokens) > 0) { | |
| $tokens[count($tokens) - 1]['optional'] = true; | |
| } | |
| continue; | |
| } | |
| $tokens[] = [ | |
| 'char' => $char, | |
| 'regex' => $patterns[$char] ?? null, | |
| 'literal' => !isset($patterns[$char]), | |
| 'optional' => false | |
| ]; | |
| } | |
| return $tokens; | |
| } |
🤖 Prompt for AI Agents
In api/app/Rules/InputMaskRule.php between lines 82 and 109, the optional flag
is set to true when a '?' character is encountered but never reset, causing all
following tokens to be marked optional incorrectly. To fix this, modify the
logic so that the optional flag applies only to the token immediately following
the '?'. After processing that token, reset the optional flag back to false
before continuing to parse the rest of the mask.
| private function validateMaskPattern(string $mask): bool | ||
| { | ||
| return preg_match('/^[9a*().\\-?\\s]*$/', $mask); | ||
| } |
There was a problem hiding this comment.
Remove backslash from the mask pattern regex.
The regex allows backslashes for escaping, but the parseMask method doesn't handle escaped characters. This could lead to unexpected behavior.
private function validateMaskPattern(string $mask): bool
{
- return preg_match('/^[9a*().\\-?\\s]*$/', $mask);
+ return preg_match('/^[9a*().\-?\s]*$/', $mask);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private function validateMaskPattern(string $mask): bool | |
| { | |
| return preg_match('/^[9a*().\\-?\\s]*$/', $mask); | |
| } | |
| private function validateMaskPattern(string $mask): bool | |
| { | |
| return preg_match('/^[9a*().\-?\s]*$/', $mask); | |
| } |
🤖 Prompt for AI Agents
In api/app/Rules/InputMaskRule.php around lines 29 to 32, the regex pattern in
validateMaskPattern currently includes a backslash character which allows
escaping, but since parseMask does not handle escaped characters, this can cause
unexpected behavior. Remove the backslash from the regex pattern so that
backslashes are not allowed in the mask string, ensuring consistency with
parseMask's handling.
| const parseMask = (maskValue) => { | ||
| if (!maskValue) return [] | ||
|
|
||
| const tokens = [] | ||
| let optional = false | ||
|
|
||
| for (let i = 0; i < maskValue.length; i++) { | ||
| const char = maskValue[i] | ||
| if (char === '?') { | ||
| optional = true | ||
| continue | ||
| } | ||
|
|
||
| tokens.push({ | ||
| char, | ||
| regex: maskTokens[char] || null, | ||
| literal: !maskTokens[char], | ||
| optional | ||
| }) | ||
| } | ||
| return tokens | ||
| } |
There was a problem hiding this comment.
Fix the optional flag handling to match the backend implementation.
The optional flag is never reset, making all tokens after a '?' optional. This should mark only the preceding token as optional.
const parseMask = (maskValue) => {
if (!maskValue) return []
const tokens = []
- let optional = false
for (let i = 0; i < maskValue.length; i++) {
const char = maskValue[i]
if (char === '?') {
- optional = true
+ // Mark the previous token as optional
+ if (tokens.length > 0) {
+ tokens[tokens.length - 1].optional = true
+ }
continue
}
tokens.push({
char,
regex: maskTokens[char] || null,
literal: !maskTokens[char],
- optional
+ optional: false
})
}
return tokens
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parseMask = (maskValue) => { | |
| if (!maskValue) return [] | |
| const tokens = [] | |
| let optional = false | |
| for (let i = 0; i < maskValue.length; i++) { | |
| const char = maskValue[i] | |
| if (char === '?') { | |
| optional = true | |
| continue | |
| } | |
| tokens.push({ | |
| char, | |
| regex: maskTokens[char] || null, | |
| literal: !maskTokens[char], | |
| optional | |
| }) | |
| } | |
| return tokens | |
| } | |
| const parseMask = (maskValue) => { | |
| if (!maskValue) return [] | |
| const tokens = [] | |
| for (let i = 0; i < maskValue.length; i++) { | |
| const char = maskValue[i] | |
| if (char === '?') { | |
| // Mark the previous token as optional | |
| if (tokens.length > 0) { | |
| tokens[tokens.length - 1].optional = true | |
| } | |
| continue | |
| } | |
| tokens.push({ | |
| char, | |
| regex: maskTokens[char] || null, | |
| literal: !maskTokens[char], | |
| optional: false | |
| }) | |
| } | |
| return tokens | |
| } |
🤖 Prompt for AI Agents
In client/composables/useInputMask.js between lines 13 and 34, the optional flag
is set to true when encountering a '?' but never reset, causing all subsequent
tokens to be marked optional incorrectly. Modify the logic so that the optional
flag applies only to the token immediately before the '?'. This means you should
mark the previous token as optional when you see '?', and not set a persistent
optional flag for following tokens.
| const getUnmaskedValue = (value) => { | ||
| if (!value) return value | ||
| return value.replace(/[^\w]/g, '') | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Update getUnmaskedValue to correctly extract input characters.
The function should extract only the actual input characters (digits and letters), not remove all non-word characters.
const getUnmaskedValue = (value) => {
if (!value) return value
- return value.replace(/[^\w]/g, '')
+ // Extract only alphanumeric characters that represent actual input
+ return value.replace(/[^0-9a-zA-Z]/g, '')
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const getUnmaskedValue = (value) => { | |
| if (!value) return value | |
| return value.replace(/[^\w]/g, '') | |
| } | |
| const getUnmaskedValue = (value) => { | |
| if (!value) return value | |
| // Extract only alphanumeric characters that represent actual input | |
| return value.replace(/[^0-9a-zA-Z]/g, '') | |
| } |
🤖 Prompt for AI Agents
In client/composables/useInputMask.js around lines 79 to 82, the
getUnmaskedValue function currently removes all non-word characters but does not
correctly extract only the actual input characters (digits and letters). Update
the function to explicitly extract and return only digits and letters from the
input value, ensuring it filters out any other characters without simply
removing non-word characters.
| const formatValue = (value) => { | ||
| if (!mask.value || !value) return value | ||
|
|
||
| const tokens = parsedMask.value | ||
| const cleanValue = value.replace(/[^\w]/g, '') | ||
| let formatted = '' | ||
| let valueIndex = 0 | ||
|
|
||
| for (const token of tokens) { | ||
| if (token.literal) { | ||
| formatted += token.char | ||
| continue | ||
| } | ||
|
|
||
| if (valueIndex >= cleanValue.length) { | ||
| if (token.optional) break | ||
| continue | ||
| } | ||
|
|
||
| if (token.regex && token.regex.test(cleanValue[valueIndex])) { | ||
| formatted += cleanValue[valueIndex] | ||
| valueIndex++ | ||
| } else if (!token.optional) { | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return formatted | ||
| } |
There was a problem hiding this comment.
Fix the input cleaning regex and apply optional chaining.
The current regex removes all non-word characters, including valid mask literals like parentheses and hyphens. Also, apply the static analysis suggestion for optional chaining.
const formatValue = (value) => {
if (!mask.value || !value) return value
const tokens = parsedMask.value
- const cleanValue = value.replace(/[^\w]/g, '')
+ // Only remove characters that aren't part of the mask pattern
+ let cleanValue = ''
+ for (const char of value) {
+ if (/[0-9a-zA-Z]/.test(char)) {
+ cleanValue += char
+ }
+ }
let formatted = ''
let valueIndex = 0
for (const token of tokens) {
if (token.literal) {
formatted += token.char
continue
}
if (valueIndex >= cleanValue.length) {
if (token.optional) break
continue
}
- if (token.regex && token.regex.test(cleanValue[valueIndex])) {
+ if (token.regex?.test(cleanValue[valueIndex])) {
formatted += cleanValue[valueIndex]
valueIndex++
} else if (!token.optional) {
break
}
}
return formatted
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const formatValue = (value) => { | |
| if (!mask.value || !value) return value | |
| const tokens = parsedMask.value | |
| const cleanValue = value.replace(/[^\w]/g, '') | |
| let formatted = '' | |
| let valueIndex = 0 | |
| for (const token of tokens) { | |
| if (token.literal) { | |
| formatted += token.char | |
| continue | |
| } | |
| if (valueIndex >= cleanValue.length) { | |
| if (token.optional) break | |
| continue | |
| } | |
| if (token.regex && token.regex.test(cleanValue[valueIndex])) { | |
| formatted += cleanValue[valueIndex] | |
| valueIndex++ | |
| } else if (!token.optional) { | |
| break | |
| } | |
| } | |
| return formatted | |
| } | |
| const formatValue = (value) => { | |
| if (!mask.value || !value) return value | |
| const tokens = parsedMask.value | |
| // Only remove characters that aren't part of the mask pattern | |
| let cleanValue = '' | |
| for (const char of value) { | |
| if (/[0-9a-zA-Z]/.test(char)) { | |
| cleanValue += char | |
| } | |
| } | |
| let formatted = '' | |
| let valueIndex = 0 | |
| for (const token of tokens) { | |
| if (token.literal) { | |
| formatted += token.char | |
| continue | |
| } | |
| if (valueIndex >= cleanValue.length) { | |
| if (token.optional) break | |
| continue | |
| } | |
| if (token.regex?.test(cleanValue[valueIndex])) { | |
| formatted += cleanValue[valueIndex] | |
| valueIndex++ | |
| } else if (!token.optional) { | |
| break | |
| } | |
| } | |
| return formatted | |
| } |
🧰 Tools
🪛 Biome (1.9.4)
[error] 68-68: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🤖 Prompt for AI Agents
In client/composables/useInputMask.js between lines 49 and 77, the regex used to
clean the input value removes all non-word characters, which incorrectly strips
valid mask literals such as parentheses and hyphens. Update the regex to only
remove characters that are not allowed by the mask, preserving valid literals.
Additionally, apply optional chaining where appropriate to safely access nested
properties without errors.
| const { formatValue, isComplete, getMaskPlaceholder, isValidMask } = useInputMask(() => props.mask) | ||
|
|
||
| const { compVal, inputWrapperProps } = useFormInput( | ||
| props, | ||
| context, | ||
| { | ||
| formPrefixKey: props.nativeType === "file" ? "file-" : null | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Remove duplicate useFormInput call and fix unused variable.
The useFormInput composable is called twice, causing duplicate initialization. The first call's inputWrapperProps is never used.
setup(props, context) {
const { formatValue, isComplete, getMaskPlaceholder, isValidMask } = useInputMask(() => props.mask)
- const { compVal, inputWrapperProps } = useFormInput(
- props,
- context,
- {
- formPrefixKey: props.nativeType === "file" ? "file-" : null
- },
- )
+ const formInputResult = useFormInput(
+ props,
+ context,
+ {
+ formPrefixKey: props.nativeType === "file" ? "file-" : null
+ },
+ )
+ const compVal = formInputResult.compVal
const maskedValue = ref('')
const inputRef = ref(null)
// ... rest of the setup code ...
return {
- ...useFormInput(
- props,
- context,
- {
- formPrefixKey: props.nativeType === "file" ? "file-" : null
- },
- ),
+ ...formInputResult,
onEnterPress,
onChange,
handleInput,
maskedValue,
effectivePlaceholder,
inputRef,
isValidMask,
displayValue
}Also applies to: 194-209
🧰 Tools
🪛 GitHub Check: Run client linters
[failure] 87-87:
'inputWrapperProps' is assigned a value but never used
🤖 Prompt for AI Agents
In client/components/forms/TextInput.vue around lines 85 to 93 and also 194 to
209, there are duplicate calls to the useFormInput composable causing redundant
initialization, and the inputWrapperProps from the first call is unused. Remove
the first useFormInput call entirely and keep only the second call where the
returned variables are actually used, ensuring no unused variables remain.
JhumanJ
left a comment
There was a problem hiding this comment.
Great but some improvements:
- In the field options:
- only allow the letters a and number 9
- let's add support for https://primevue.org/inputmask/#slotchar
- let's create a basic help page and add link to it
- let's refactor in a popover for cleaner options and better explain (let's wait for the migration to nuxt ui 3 and tailwind 4 beforehands)
- For textInput: i think we did too many changes which are making the component too complicated. Let's instead revert our changes there, and create a dedicated
MaskInput. For the user it's still a text input option but for the code it'll be cleaner.
Also in terms of feature let's improve:
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
client/components/forms/TextInput.vue (1)
64-66: Missing core Vue & composable imports – component will not compile
ref,computed,nextTick,watch, anduseInputMaskare all referenced but never imported, breaking the build at runtime.-import {inputProps, useFormInput} from "./useFormInput.js" -import InputWrapper from "./components/InputWrapper.vue" +import InputWrapper from "./components/InputWrapper.vue" +import { inputProps, useFormInput } from "./useFormInput.js" + +// 🛠 Required Vue reactivity utilities +import { ref, computed, nextTick, watch } from "vue" + +// 🛠 Composable providing mask logic +import { useInputMask } from "@/composables/useInputMask" // adjust path if necessary
♻️ Duplicate comments (1)
client/components/forms/TextInput.vue (1)
85-94:useFormInputis still executed twice – remove the first callThe earlier review already pointed this out. Instantiating
useFormInputtwice duplicates watchers & emits, wasting resources and risking state divergence.- const { compVal } = useFormInput( - props, - context, - { - formPrefixKey: props.nativeType === "file" ? "file-" : null - }, - ) + const formInputResult = useFormInput( + props, + context, + { formPrefixKey: props.nativeType === "file" ? "file-" : null }, + ) + const { compVal } = formInputResult-return { - ...useFormInput( - props, - context, - { - formPrefixKey: props.nativeType === "file" ? "file-" : null - }, - ), +return { + ...formInputResult,Also applies to: 194-200
🧹 Nitpick comments (1)
client/components/forms/TextInput.vue (1)
115-152: Cursor-maintenance logic is brittle and can be simplifiedManually resetting
inputRef.value.valueinsidenextTickmay cause flicker and breaks IME composition. Consider letting Vue control the DOM and rely onv-modelplus a@input="handleInput"listener:- nextTick(() => { - if (inputRef.value && inputRef.value.value !== formatted) { - const cursorPos = inputRef.value.selectionStart - inputRef.value.value = formatted - if (inputRef.value.setSelectionRange) { - inputRef.value.setSelectionRange(cursorPos, cursorPos) - } - } - }) + // Emit only the masked value; Vue will update the DOM via v-model.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
client/components/forms/TextInput.vue(4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (5)
- GitHub Check: Build the Nuxt app
- GitHub Check: Run code linters
- GitHub Check: Run Feature & Unit tests (PHP 8.2 - pgsql)
- GitHub Check: Run Feature & Unit tests (PHP 8.2 - mysql)
- GitHub Check: Run client linters
… it, not clear the whole input
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
client/components/forms/TextInput.vue (1)
64-67: Import the required utilities anduseInputMaskto prevent runtime failures
ref,computed,watch,nextTick, anduseInputMaskare referenced but never imported, which will break both the linter and the runtime bundle.-import {inputProps, useFormInput} from "./useFormInput.js" -import InputWrapper from "./components/InputWrapper.vue" +import { ref, computed, watch, nextTick } from "vue" +import { inputProps, useFormInput } from "./useFormInput.js" +import InputWrapper from "./components/InputWrapper.vue" +import useInputMask from "../../composables/useInputMask.js"
🧹 Nitpick comments (1)
client/components/forms/TextInput.vue (1)
98-113: Potential cursor-position glitches when using computed setter only
handleInputrelies oninputRef.value.selectionStart, but when the setter is triggered viav-model,selectionStartmay benullin some browsers (e.g. Safari) because the DOM update hasn’t flushed yet. Consider binding@input="handleInput"directly and passing the real event instead of fabricating one, to guarantee reliable cursor handling.This is optional but will save edge-case bug reports.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
client/components/forms/TextInput.vue(4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Build the Nuxt app
- GitHub Check: Run Feature & Unit tests (PHP 8.2 - mysql)
- GitHub Check: Run client linters
- GitHub Check: Run Feature & Unit tests (PHP 8.2 - pgsql)
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
client/components/forms/TextInput.vue (1)
64-66: Missing essential imports – component will crash at runtime
ref,computed,watch,nextTickanduseInputMaskare used but never imported.-import {inputProps, useFormInput} from "./useFormInput.js" -import InputWrapper from "./components/InputWrapper.vue" +import { ref, computed, watch, nextTick } from "vue" +import { useInputMask } from "@/composables/useInputMask.js" +import { inputProps, useFormInput } from "./useFormInput.js" +import InputWrapper from "./components/InputWrapper.vue"
♻️ Duplicate comments (4)
client/composables/useInputMask.js (3)
17-31:optionalflag still leaks – breaks all masks after the first?
Same issue flagged earlier: theoptionalboolean is set when encountering'?'but never reset, therefore every subsequent token is marked optional and the mask truncates at the first missing char.- let optional = false - for (let i = 0; i < maskValue.length; i++) { const char = maskValue[i] if (char === '?') { - optional = true + // mark *previous* token optional, don’t affect the rest + if (tokens.length > 0) { + tokens[tokens.length - 1].optional = true + } continue } tokens.push({ char, regex: maskTokens[char] || null, literal: !maskTokens[char], - optional + optional: false }) }
52-55: Over-aggressive clean-up regex removes valid alpha-numeric input
/[^\w]/gstrips underscores but also removes locale-specific letters and treats_as word-char. Replace with an explicit alphanumeric whitelist as discussed previously.- const cleanValue = value.replace(/[^\w]/g, '') + const cleanValue = value.replace(/[^0-9a-zA-Z]/g, '')
79-82:getUnmaskedValuereplicates the same regex problem- return value.replace(/[^\w]/g, '') + return value.replace(/[^0-9a-zA-Z]/g, '')client/components/forms/TextInput.vue (1)
85-96: DuplicateuseFormInputinvocation – double watchers & wasted reactivity
The first call is stored incompVal, the second (spread inreturn) recreates the whole composable. Call it once and reuse the result.- const { compVal } = useFormInput( + const formInput = useFormInput( props, context, { formPrefixKey: props.nativeType === "file" ? "file-" : null }, ) + const { compVal } = formInput…and later in
return:- ...useFormInput( - props, - context, - { - formPrefixKey: props.nativeType === "file" ? "file-" : null - }, - ), + ...formInput,
🧹 Nitpick comments (1)
client/components/forms/TextInput.vue (1)
135-143:nextTick,selectionStarthandling relies on nullableinputRef
Before accessingselectionStart, guard againstnulland ensure the element is still mounted to prevent TypeErrors during rapid unmount / remount cycles.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
client/components/forms/TextInput.vue(4 hunks)client/composables/useInputMask.js(1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
client/composables/useInputMask.js
[error] 68-68: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 126-126: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Build the Nuxt app
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
client/components/forms/MaskInput.vue (3)
68-70: Name / file mismatch may confuse consumersThe component is exported as
TextInput, yet the file is namedMaskInput.vue.
Either rename the file toTextInput.vueor updatename:toMaskInputfor clarity & easier debugging in devtools.
73-81: Prop typing edge-cases
autocompleteacceptsBoolean | Object, but<input autocomplete>only expects a string ("on"/"off"or datalist tokens). Passing a boolean/object will render"true"/"[object Object]"in the DOM.pattern&maskdefaults arenull; Vue will render the literal"null"attribute. Preferundefinedso the attribute is omitted.-autocomplete: {type: [Boolean, String, Object], default: null}, +autocomplete: { type: String, default: undefined }, ... -pattern: { type: String, default: null }, -mask: { type: String, default: null } +pattern: { type: String, default: undefined }, +mask: { type: String, default: undefined }
34-35: Redundant event prevention
@keydown.enter.preventalready stops the default form submission.
Callingevent.preventDefault()again inonEnterPressis unnecessary and slightly misleading—consider removing the method entirely and the corresponding listener, or drop the.preventmodifier.Also applies to: 202-205
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
client/components/forms/MaskInput.vue(1 hunks)client/components/open/forms/OpenFormField.vue(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- client/components/open/forms/OpenFormField.vue
⏰ Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: Run Feature & Unit tests (PHP 8.2 - mysql)
- GitHub Check: Run Feature & Unit tests (PHP 8.2 - pgsql)
- GitHub Check: Build the Nuxt app
🔇 Additional comments (1)
client/components/forms/MaskInput.vue (1)
115-146: Underscore stripping may reject legitimate input
handleInputunconditionally strips_before formatting. If the mask itself legitimately contains underscores (e.g., part numbers likeABC_DEF), the user can never enter them. Consider making the placeholder character configurable or skipping this replace when the mask includes literal underscores.
| <script> | ||
| import {inputProps, useFormInput} from "./useFormInput.js" | ||
| import InputWrapper from "./components/InputWrapper.vue" | ||
|
|
There was a problem hiding this comment.
Missing essential imports – component will fail to compile
ref, computed, watch, nextTick, and the very useInputMask composable used below are never imported.
Add them (and group with existing imports) or the script will throw at runtime.
-import {inputProps, useFormInput} from "./useFormInput.js"
+import { ref, computed, watch, nextTick } from "vue"
+import { inputProps, useFormInput } from "./useFormInput.js"
+import { useInputMask } from "../composables/useInputMask.js"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <script> | |
| import {inputProps, useFormInput} from "./useFormInput.js" | |
| import InputWrapper from "./components/InputWrapper.vue" | |
| <script> | |
| import { ref, computed, watch, nextTick } from "vue" | |
| import { inputProps, useFormInput } from "./useFormInput.js" | |
| import { useInputMask } from "../composables/useInputMask.js" | |
| import InputWrapper from "./components/InputWrapper.vue" |
🤖 Prompt for AI Agents
In client/components/forms/MaskInput.vue around lines 64 to 67, the script is
missing imports for ref, computed, watch, nextTick, and the useInputMask
composable, which are used later in the code. Add these imports from their
respective sources and group them with the existing imports at the top of the
script to prevent runtime errors and ensure the component compiles correctly.
- Implemented a new method to validate input mask patterns, ensuring only allowed characters (a, 9, *, and common punctuation) are accepted. - Updated the help text to clarify the format and provide examples for users, improving usability and guidance for input mask patterns.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
client/components/forms/heavy/MaskInput.vue (2)
201-204: Remove redundant return statement.The
return falseis redundant afterevent.preventDefault().const onEnterPress = (event) => { event.preventDefault() - return false }
69-69: Remove empty components object.The empty
componentsobject is unnecessary.- components: {},
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
client/components/forms/heavy/MaskInput.vue(1 hunks)client/components/open/forms/fields/components/FieldOptions.vue(3 hunks)client/composables/components/useComponentRegistry.js(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- client/composables/components/useComponentRegistry.js
🚧 Files skipped from review as they are similar to previous changes (1)
- client/components/open/forms/fields/components/FieldOptions.vue
🧰 Additional context used
📓 Path-based instructions (4)
client/**
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
client/**/*.vue
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
client/components/**/*
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
client/**/*.{js,vue}
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/back-end.mdc:0-0
Timestamp: 2025-06-30T15:05:17.582Z
Learning: Applies to api/**/*.php : Use Laravel's validation features for form and request validation.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/back-end.mdc:0-0
Timestamp: 2025-06-30T15:05:17.582Z
Learning: Applies to api/**/*.php : Implement proper request validation using Form Requests.
client/components/forms/heavy/MaskInput.vue (3)
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : For forms inputs, use the components in client/components/forms such as <TextInput> etc.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Wrap form inputs in @VForm.vue.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Use Nuxt UI components (https://ui.nuxt.com/components) and Tailwind for components and styling.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build the Nuxt app
- GitHub Check: Run client linters
| <input | ||
| ref="inputRef" | ||
| :id="id ? id : name" | ||
| v-model="displayValue" |
There was a problem hiding this comment.
Fix input event handling for mask formatting.
The displayValue setter calls handleInput with a synthetic event object, but handleInput is designed to work with native input events. This could cause issues with cursor positioning and formatting.
Replace v-model with explicit value binding and input handler:
- v-model="displayValue"
+ :value="displayValue"
+ @input="handleInput"Then update the displayValue setter to handle non-masked inputs only:
set(newValue) {
- if (props.mask && isValidMask.value) {
- handleInput({ target: { value: newValue } })
- } else {
+ if (!props.mask || !isValidMask.value) {
compVal.value = newValue
}
}Also applies to: 107-111
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue at line 10 and also lines
107-111, the current use of v-model on displayValue triggers the setter with a
synthetic event, but handleInput expects a native input event, causing cursor
and formatting issues. Replace v-model with explicit :value binding to
displayValue and an @input event listener that calls handleInput directly with
the native event. Then modify the displayValue setter to only update the
internal value for non-masked inputs without calling handleInput, ensuring
proper event handling and cursor behavior.
| <script> | ||
| import {inputProps, useFormInput} from "../useFormInput.js" | ||
|
|
||
| export default { | ||
| name: "MaskInput", | ||
| components: {}, | ||
|
|
||
| props: { | ||
| ...inputProps, | ||
| nativeType: {type: String, default: "text"}, | ||
| accept: {type: String, default: null}, | ||
| min: {type: Number, required: false, default: null}, | ||
| max: {type: Number, required: false, default: null}, | ||
| autocomplete: {type: [Boolean, String, Object], default: null}, | ||
| maxCharLimit: {type: Number, required: false, default: null}, | ||
| pattern: { type: String, default: null }, | ||
| mask: { type: String, default: null } | ||
| }, | ||
|
|
||
| setup(props, context) { | ||
| const { formatValue, isValidMask, getDisplayValue } = useInputMask(() => props.mask) | ||
|
|
||
| const { compVal } = useFormInput( | ||
| props, | ||
| context, | ||
| { | ||
| formPrefixKey: props.nativeType === "file" ? "file-" : null | ||
| }, | ||
| ) | ||
|
|
||
| const maskedValue = ref('') | ||
| const inputRef = ref(null) | ||
|
|
||
| const displayValue = computed({ | ||
| get() { | ||
| if (props.mask && isValidMask.value) { | ||
| return getDisplayValue(compVal.value) | ||
| } else { | ||
| return compVal.value | ||
| } | ||
| }, | ||
| set(newValue) { | ||
| if (props.mask && isValidMask.value) { | ||
| handleInput({ target: { value: newValue } }) | ||
| } else { | ||
| compVal.value = newValue | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| const handleInput = (event) => { | ||
| const inputValue = event.target.value | ||
|
|
||
| if (!props.mask || !isValidMask.value) { | ||
| // No mask or invalid mask - behave as normal input | ||
| compVal.value = inputValue | ||
| return | ||
| } | ||
|
|
||
| // Remove underscores from input value for processing | ||
| const cleanInputValue = inputValue.replace(/_/g, '') | ||
|
|
||
| // Get the previous formatted value to compare | ||
| const previousFormatted = maskedValue.value | ||
| const formatted = formatValue(cleanInputValue) | ||
|
|
||
| // If the formatted value is the same as before, it means the new character was invalid | ||
| // In this case, we should revert to the previous state | ||
| if (formatted === previousFormatted && cleanInputValue.length > previousFormatted.length) { | ||
| // Invalid character entered - revert the input | ||
| nextTick(() => { | ||
| if (inputRef.value) { | ||
| const cursorPos = inputRef.value.selectionStart - 1 | ||
| inputRef.value.value = getDisplayValue(previousFormatted) | ||
| // Set cursor position to where it was before the invalid character | ||
| if (inputRef.value.setSelectionRange && cursorPos >= 0) { | ||
| inputRef.value.setSelectionRange(cursorPos, cursorPos) | ||
| } | ||
| } | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| // Valid input - update the values | ||
| maskedValue.value = formatted | ||
| compVal.value = formatted | ||
|
|
||
| // Update input display value | ||
| nextTick(() => { | ||
| if (inputRef.value) { | ||
| const displayValue = getDisplayValue(formatted) | ||
| if (inputRef.value.value !== displayValue) { | ||
| const cursorPos = inputRef.value.selectionStart | ||
| inputRef.value.value = displayValue | ||
| // Maintain cursor position logic here | ||
| if (inputRef.value.setSelectionRange) { | ||
| inputRef.value.setSelectionRange(cursorPos, cursorPos) | ||
| } | ||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| const effectivePlaceholder = computed(() => { | ||
| if (props.placeholder) return props.placeholder | ||
| if (props.mask && isValidMask.value) return getDisplayValue('') | ||
| return null | ||
| }) | ||
|
|
||
| // Watch for mask changes (form editor support) | ||
| watch(() => props.mask, (newMask) => { | ||
| if (!newMask) { | ||
| maskedValue.value = compVal.value || '' | ||
| } else if (compVal.value) { | ||
| // Reformat existing value with new mask | ||
| maskedValue.value = formatValue(compVal.value) | ||
| } | ||
| }) | ||
|
|
||
| // Watch for compVal changes from parent | ||
| watch(compVal, (newVal) => { | ||
| if (props.mask && isValidMask.value && newVal) { | ||
| maskedValue.value = formatValue(newVal) | ||
| } else { | ||
| maskedValue.value = newVal || '' | ||
| } | ||
| }, { immediate: true }) | ||
|
|
||
|
|
||
| const onChange = (event) => { | ||
| if (props.nativeType !== "file") return | ||
|
|
||
| const file = event.target.files[0] | ||
|
|
||
| props.form[props.name] = file | ||
| } | ||
|
|
||
| const onEnterPress = (event) => { | ||
| event.preventDefault() | ||
| return false | ||
| } | ||
|
|
||
| return { | ||
| ...useFormInput( | ||
| props, | ||
| context, | ||
| { | ||
| formPrefixKey: props.nativeType === "file" ? "file-" : null | ||
| }, | ||
| ), | ||
| onEnterPress, | ||
| onChange, | ||
| handleInput, | ||
| maskedValue, | ||
| effectivePlaceholder, | ||
| inputRef, | ||
| isValidMask, | ||
| displayValue | ||
| } | ||
| }, | ||
| computed: { | ||
| charCount() { | ||
| return this.compVal ? this.compVal.length : 0 | ||
| }, | ||
| }, | ||
| } |
There was a problem hiding this comment.
Convert component to use Vue Composition API script setup style.
The component uses Vue Options API, which violates the project's coding guidelines. All Vue components should use the Composition API script setup style.
Refactor the component to use <script setup>:
-<script>
-import {inputProps, useFormInput} from "../useFormInput.js"
-
-export default {
- name: "MaskInput",
- components: {},
-
- props: {
- ...inputProps,
- nativeType: {type: String, default: "text"},
- // ... other props
- },
-
- setup(props, context) {
- // ... setup logic
- },
-
- computed: {
- charCount() {
- return this.compVal ? this.compVal.length : 0
- },
- },
-}
-</script>
+<script setup>
+import { computed, ref, watch, nextTick } from 'vue'
+import { inputProps, useFormInput } from "../useFormInput.js"
+import { useInputMask } from '@/composables/useInputMask'
+
+const props = defineProps({
+ ...inputProps,
+ nativeType: { type: String, default: "text" },
+ accept: { type: String, default: null },
+ min: { type: Number, required: false, default: null },
+ max: { type: Number, required: false, default: null },
+ autocomplete: { type: [Boolean, String, Object], default: null },
+ maxCharLimit: { type: Number, required: false, default: null },
+ pattern: { type: String, default: null },
+ mask: { type: String, default: null }
+})
+
+// Component logic here...
+const charCount = computed(() => compVal.value ? compVal.value.length : 0)
+</script>Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue from lines 64 to 229, the
component currently uses the Vue Options API, but the project requires using the
Composition API with the <script setup> syntax. Refactor the entire component by
removing the export default object and instead use the <script setup> block.
Move all props definitions to defineProps, replace data and computed properties
with refs and computed functions, and convert methods to regular functions
within the setup scope. Also, import and use composables directly inside the
setup block, and return only what is needed for the template. This will align
the component with the project's Composition API style guidelines.
| const onChange = (event) => { | ||
| if (props.nativeType !== "file") return | ||
|
|
||
| const file = event.target.files[0] | ||
|
|
||
| props.form[props.name] = file | ||
| } |
There was a problem hiding this comment.
Avoid direct mutation of props.
Directly mutating props.form[props.name] violates Vue's one-way data flow principle.
Use the form input composable's methods to update the value:
const onChange = (event) => {
if (props.nativeType !== "file") return
const file = event.target.files[0]
-
- props.form[props.name] = file
+ compVal.value = file
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const onChange = (event) => { | |
| if (props.nativeType !== "file") return | |
| const file = event.target.files[0] | |
| props.form[props.name] = file | |
| } | |
| const onChange = (event) => { | |
| if (props.nativeType !== "file") return | |
| const file = event.target.files[0] | |
| compVal.value = file | |
| } |
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue around lines 193 to 199, the code
directly mutates props.form[props.name], which violates Vue's one-way data flow
principle. Instead of assigning the file directly to props.form[props.name], use
the provided form input composable's method to update the value properly,
ensuring the component respects Vue's reactive data flow and avoids direct prop
mutation.
| </template> | ||
|
|
||
| <script> | ||
| import {inputProps, useFormInput} from "../useFormInput.js" |
There was a problem hiding this comment.
Add missing import for useInputMask composable.
The useInputMask composable is used but not imported.
Add the missing import:
import {inputProps, useFormInput} from "../useFormInput.js"
+import { useInputMask } from '@/composables/useInputMask'Also applies to: 84-84
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue at lines 65 and 84, the
useInputMask composable is used but not imported. Add an import statement for
useInputMask from its module at the top of the file alongside the existing
imports to ensure it is properly referenced.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (7)
client/composables/useInputMask.js (3)
13-34: Fix the optional flag handling to match the backend implementation.The optional flag is never reset, making all tokens after a '?' optional. This should mark only the preceding token as optional.
49-77: Fix the input cleaning regex and apply optional chaining.The current regex removes all non-word characters, including valid mask literals like parentheses and hyphens. Also, apply the static analysis suggestion for optional chaining.
79-82: Update getUnmaskedValue to correctly extract input characters.The function should extract only the actual input characters (digits and letters), not remove all non-word characters.
client/components/forms/heavy/MaskInput.vue (4)
64-231: Convert component to use Vue Composition API script setup style.The component uses Vue Options API, which violates the project's coding guidelines. All Vue components should use the Composition API script setup style.
65-65: Add missing import for useInputMask composable.The
useInputMaskcomposable is used but not imported.
10-10: Fix input event handling for mask formatting.The
displayValuesetter callshandleInputwith a synthetic event object, buthandleInputis designed to work with native input events. This could cause issues with cursor positioning and formatting.
194-200: Avoid direct mutation of props.Directly mutating
props.form[props.name]violates Vue's one-way data flow principle.
🧹 Nitpick comments (1)
client/composables/useInputMask.js (1)
126-126: Apply optional chaining for safer property access.The static analysis tool suggests using optional chaining for safer access to the
regexproperty.Apply this diff to use optional chaining:
- if (token.regex && token.regex.test(cleanValue[valueIndex])) { + if (token.regex?.test(cleanValue[valueIndex])) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
client/components/forms/heavy/MaskInput.vue(1 hunks)client/components/open/forms/OpenFormField.vue(2 hunks)client/components/open/forms/fields/components/FieldOptions.vue(3 hunks)client/composables/useInputMask.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- client/components/open/forms/OpenFormField.vue
- client/components/open/forms/fields/components/FieldOptions.vue
🧰 Additional context used
📓 Path-based instructions (5)
client/**
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
client/**/*.vue
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
client/components/**/*
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
client/**/*.{js,vue}
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
client/**/*.js
Instructions used from:
Sources:
📄 CodeRabbit Inference Engine
- .cursor/rules/front-end.mdc
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/back-end.mdc:0-0
Timestamp: 2025-06-30T15:05:17.582Z
Learning: Applies to api/**/*.php : Use Laravel's validation features for form and request validation.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/back-end.mdc:0-0
Timestamp: 2025-06-30T15:05:17.582Z
Learning: Applies to api/**/*.php : Implement proper request validation using Form Requests.
client/components/forms/heavy/MaskInput.vue (8)
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : For forms inputs, use the components in client/components/forms such as <TextInput> etc.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Wrap form inputs in @VForm.vue.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Use Nuxt UI components (https://ui.nuxt.com/components) and Tailwind for components and styling.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.{js,vue} : Implement proper error boundaries or try-catch mechanisms to handle errors gracefully, especially in asynchronous operations.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Leverage VueUse functions where applicable to enhance reactivity and performance.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Always use the Vue Composition API script setup style.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Wrap asynchronous components in Suspense with a fallback UI made with <USkeleton/> components.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Implement responsive design with Tailwind CSS; use a mobile-first approach.
client/composables/useInputMask.js (3)
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : For forms inputs, use the components in client/components/forms such as <TextInput> etc.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Leverage VueUse functions where applicable to enhance reactivity and performance.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Wrap form inputs in @VForm.vue.
🪛 Biome (1.9.4)
client/composables/useInputMask.js
[error] 68-68: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 126-126: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🔇 Additional comments (1)
client/composables/useInputMask.js (1)
94-97: No changes needed: Frontend mask regex already matches backend patternThe regex in
client/composables/useInputMask.js(/^[9a*().?\s-]*$/) is functionally equivalent to the backend’sInputMaskRule::validateMaskPattern(/^[9a*().\-\?\s]*$/), allowing the same set of mask characters. You can safely ignore the update suggestion.Likely an incorrect or invalid review comment.
| const getMaskPlaceholder = computed(() => { | ||
| if (!mask.value) return '' | ||
|
|
||
| return parsedMask.value.map(token => { | ||
| if (token.literal) return token.char | ||
| if (token.optional) return '' | ||
| return token.char | ||
| }).join('') | ||
| }) |
There was a problem hiding this comment.
Fix the mask placeholder generation logic.
The placeholder function returns the mask characters themselves instead of showing the expected input format. For non-literal tokens, it should return the slot character or nothing based on whether they're optional.
Apply this diff to fix the placeholder logic:
const getMaskPlaceholder = computed(() => {
if (!mask.value) return ''
return parsedMask.value.map(token => {
if (token.literal) return token.char
if (token.optional) return ''
- return token.char
+ return slotChar
}).join('')
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const getMaskPlaceholder = computed(() => { | |
| if (!mask.value) return '' | |
| return parsedMask.value.map(token => { | |
| if (token.literal) return token.char | |
| if (token.optional) return '' | |
| return token.char | |
| }).join('') | |
| }) | |
| const getMaskPlaceholder = computed(() => { | |
| if (!mask.value) return '' | |
| return parsedMask.value.map(token => { | |
| if (token.literal) return token.char | |
| if (token.optional) return '' | |
| return slotChar | |
| }).join('') | |
| }) |
🤖 Prompt for AI Agents
In client/composables/useInputMask.js around lines 39 to 47, the
getMaskPlaceholder computed property incorrectly returns the mask characters for
non-literal tokens instead of the expected input format. To fix this, update the
logic so that for literal tokens it returns the character as is, for optional
tokens it returns an empty string, and for other tokens it returns the slot
character representing the expected input format rather than the mask character.
| computed: { | ||
| charCount() { | ||
| return this.compVal ? this.compVal.length : 0 | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Remove computed property that duplicates setup logic.
The charCount computed property in the Options API section duplicates functionality already available in the setup function and will cause conflicts.
Remove the computed section as it's redundant:
- computed: {
- charCount() {
- return this.compVal ? this.compVal.length : 0
- },
- },The character count should be computed in the setup function instead.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| computed: { | |
| charCount() { | |
| return this.compVal ? this.compVal.length : 0 | |
| }, | |
| }, |
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue around lines 225 to 229, remove
the computed property charCount from the Options API section because it
duplicates logic already implemented in the setup function. This redundancy can
cause conflicts, so delete the entire computed block containing charCount and
ensure that character count is only computed within the setup function.
| const maskedValue = ref('') | ||
| const inputRef = ref(null) |
There was a problem hiding this comment.
Add missing Vue imports for composition functions.
The component uses ref without importing it from Vue.
Add the missing import:
import {inputProps, useFormInput} from "../useFormInput.js"
+import { ref, computed, watch, nextTick } from 'vue'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const maskedValue = ref('') | |
| const inputRef = ref(null) | |
| // at the top of client/components/forms/heavy/MaskInput.vue | |
| import {inputProps, useFormInput} from "../useFormInput.js" | |
| +import { ref, computed, watch, nextTick } from 'vue' | |
| // …rest of your imports and <script setup> code… |
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue around lines 95 to 96, the ref
function is used without importing it from Vue. Add an import statement at the
top of the script section to import ref from 'vue' to ensure the composition API
function is available.
|
|
||
| // If the formatted value is the same as before, it means the new character was invalid | ||
| // In this case, we should revert to the previous state | ||
| if (formatted === previousFormatted && cleanInputValue.length > previousFormatted.length) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fix the mask value comparison logic.
The comparison uses previousFormatted.length but should use the unmasked value length for accurate validation.
Apply this diff to fix the comparison:
- if (formatted === previousFormatted && cleanInputValue.length > previousFormatted.length) {
+ if (formatted === previousFormatted && cleanInputValue.length > maskedValue.value.replace(/[^0-9a-zA-Z]/g, '').length) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (formatted === previousFormatted && cleanInputValue.length > previousFormatted.length) { | |
| if (formatted === previousFormatted && cleanInputValue.length > maskedValue.value.replace(/[^0-9a-zA-Z]/g, '').length) { |
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue at line 133, the condition
incorrectly compares cleanInputValue.length with previousFormatted.length, which
mixes masked and unmasked lengths. Update the comparison to use the length of
the unmasked previous value instead of previousFormatted.length to ensure
accurate validation of input changes.
All Done ✔️ |
JhumanJ
left a comment
There was a problem hiding this comment.
- Cursor is at then end instead of being at the right position - examples here: https://formkit.com/inputs/mask
- let's move this option to advanced settings in the field settings
- Regex inconsistency between frontend and backend
Backend (UserFormRequest.php): /^[9a*().\\-?\\s]*$/
Frontend (useInputMask.js): /^[9a*().?\s-]*$/
Backend validation (InputMaskRule.php): /^[9a*().\\-?\\s]*$/
The frontend allows ? but the backend regex doesn't match it correctly. Also, the frontend allows - anywhere while the backend escapes it.
- Issue: ? sets a flag for the next token, but it should mark the previous token as optional. Current behavior: 9? makes the 9 optional, but ?9 incorrectly makes 9 optional.
- Character count includes mask characters
I recommend you also try in cursor expert PR review of @Branch with composer-1 model
- Updated regex patterns in UserFormRequest and InputMaskRule to improve input mask validation. - Enhanced MaskInput component to utilize getUnmaskedValue for accurate character counting. - Refactored useInputMask composable to correctly mark tokens as optional and validate mask patterns. - Improved FieldOptions component by reintroducing input mask pattern help text and examples. These changes enhance the functionality and user experience of input masks across the application.
- Improved InputMaskRule to handle optional mask tokens more effectively, allowing for better validation of input values. - Updated error messages to escape mask patterns for XSS prevention. - Enhanced MaskInput component to support pasting formatted values and maintain cursor position during input changes. - Refactored useInputMask composable to streamline mask validation and formatting processes. These changes improve the robustness and user experience of input masking across the application.
|
Done |
|
@JhumanJ can you please check this
|
|
Created new PR #993 |


InputMaskRulefor validating input masks in form requests, enhancing data integrity for text fields.AnswerFormRequestandUserFormRequestto include input mask validation rules, allowing for flexible input formats.TextInputcomponent to support input masks, improving user experience with formatted input fields.useInputMaskcomposable for managing input mask logic, ensuring consistent behavior across components.FieldOptionsto allow configuration of input masks for text fields, providing users with more customization options.These changes aim to improve form handling and user input validation, ensuring that data adheres to specified formats while enhancing the overall user experience in form interactions.
Summary by CodeRabbit
New Features
Bug Fixes
Tests