@weareconceptstudio/form 0.2.8 → 0.2.9

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.
@@ -1,4 +1,4 @@
1
- import React, { useCallback, forwardRef, useImperativeHandle } from 'react';
1
+ import React, { useCallback, forwardRef, useImperativeHandle, useEffect } from 'react';
2
2
  import { FormProvider, useForm } from 'react-hook-form';
3
3
  import { Text } from '@weareconceptstudio/core';
4
4
  import classNames from 'classnames';
@@ -44,6 +44,18 @@ const BaseForm = forwardRef(({ initialValues: defaultValues, values, children, o
44
44
  isFormTouched: Object.keys(methods.formState.touchedFields).length > 0,
45
45
  submit: () => methods.handleSubmit(handleSubmit)(),
46
46
  }));
47
+ useEffect(() => {
48
+ let errors = methods.formState.errors;
49
+ if (Object.keys(errors).length > 0) {
50
+ const firstErrorField = Object.keys(errors)[0];
51
+ const errorElement = document.querySelector(`[name="${firstErrorField}"]`);
52
+ if (errorElement) {
53
+ errorElement.scrollIntoView({ behavior: "smooth", block: "center" });
54
+ // @ts-ignore
55
+ errors[firstErrorField].ref.select();
56
+ }
57
+ }
58
+ }, [methods.formState.errors]);
47
59
  return (React.createElement(FormProvider, { ...methods },
48
60
  methods.formState.errors.root?.serverError && (React.createElement("div", { className: 'global-error-wrap' },
49
61
  React.createElement(Text, { className: 'backend-error', text: methods.formState.errors.root?.serverError.message }),
@@ -5,7 +5,6 @@ import Form from '../';
5
5
  import Input from '../../input';
6
6
  import Checkbox from '../../checkbox';
7
7
  import Radio from '../../radio';
8
- import PhoneNumber from '../../phone-number';
9
8
  import Select from '../../select';
10
9
  import Upload from '../../upload';
11
10
  import DatePicker from '../../date-picker';
@@ -17,9 +16,9 @@ const formElements = {
17
16
  'password': React.createElement(Input.Password, null),
18
17
  'textarea': React.createElement(Input.TextArea, null),
19
18
  'number': React.createElement(Input.Number, null),
19
+ 'phone': React.createElement(Input.PhoneNumber, null),
20
20
  'checkbox': React.createElement(Checkbox, null),
21
21
  'radio': React.createElement(Radio, null),
22
- 'phone': React.createElement(PhoneNumber, null),
23
22
  'select': React.createElement(Select, null),
24
23
  'upload': React.createElement(Upload, null),
25
24
  'date': React.createElement(DatePicker, null),
@@ -1,5 +1,4 @@
1
1
  import React, { cloneElement, isValidElement, useRef } from 'react';
2
- import lodashGet from 'lodash.get';
3
2
  import classNames from 'classnames';
4
3
  import { isForwardRefChild, useTranslation } from '@weareconceptstudio/core';
5
4
  import ErrorMessage from '../../error-message';
@@ -11,7 +10,7 @@ const FormItem = ({ label, name, children, className, errorKey, required = true,
11
10
  }
12
11
  const childRef = useRef(null);
13
12
  const { translate } = useTranslation();
14
- const { formState } = useInput({
13
+ const { fieldState } = useInput({
15
14
  name,
16
15
  rules: useRules({
17
16
  rules,
@@ -23,10 +22,10 @@ const FormItem = ({ label, name, children, className, errorKey, required = true,
23
22
  }),
24
23
  errorKey: errorKey || label,
25
24
  });
26
- const hasError = !!lodashGet(formState.errors, name);
25
+ const { error, invalid } = fieldState;
27
26
  const classString = classNames('form-item', {
28
27
  [className]: className,
29
- 'has-error': hasError,
28
+ 'has-error': error || invalid,
30
29
  // @ts-ignore
31
30
  disabled: children.props.disabled,
32
31
  });
@@ -1,5 +1,5 @@
1
1
  import React, { useMemo } from 'react';
2
- import { isValidPhoneNumber } from 'react-phone-number-input';
2
+ import { isValidPhoneNumber } from 'libphonenumber-js';
3
3
  import { useTranslation } from '@weareconceptstudio/core';
4
4
  export const validationPatterns = ({ type, pattern, message, translate }) => {
5
5
  switch (type) {
@@ -22,7 +22,10 @@ export const useRules = ({ rules, required, requiredMessage, childType, errorKey
22
22
  if (childType.toLowerCase() === 'input' || childType.toLowerCase() === 'textarea') {
23
23
  r.push({
24
24
  validator: (value) => {
25
- return value?.trim().length > 0 || translate('form.validateMessages.whitespace', { errorKey: translate(errorKey) });
25
+ if (!value || typeof value !== 'string' || value.trim().length === 0) {
26
+ return translate('form.validateMessages.whitespace', { errorKey: translate(errorKey) });
27
+ }
28
+ return true;
26
29
  },
27
30
  });
28
31
  }
@@ -45,7 +48,7 @@ export const mapValidationRules = (errorKey, rules) => {
45
48
  const { translate } = useTranslation();
46
49
  return rules?.reduce((acc, rule) => {
47
50
  if (rule.required) {
48
- acc.required = rule.message || translate('form.validateMessages.required', { errorKey: translate(errorKey) });
51
+ acc.required = { value: rule.required, message: rule.message || translate('form.validateMessages.required', { errorKey: translate(errorKey) }) };
49
52
  }
50
53
  if (rule.pattern || rule.type) {
51
54
  acc.pattern = validationPatterns({ ...rule, translate });
@@ -4,11 +4,12 @@ import { mapValidationRules } from './rules';
4
4
  const defaultFormat = (value) => (value == null ? '' : value);
5
5
  const defaultParse = (value) => (value === '' ? null : value);
6
6
  export const useInput = (props) => {
7
- const { defaultValue, name, onBlur: initialOnBlur, onChange: initialOnChange, format = defaultFormat, parse = defaultParse, multiple, errorKey, rules = [] } = props;
7
+ const { defaultValue, name, onBlur: initialOnBlur, onChange: initialOnChange, format = defaultFormat, parse = defaultParse, multiple, errorKey, rules = [], disabled } = props;
8
8
  const { field: controllerField, fieldState, formState, } = useController({
9
9
  name,
10
10
  defaultValue,
11
11
  rules: mapValidationRules(errorKey, rules),
12
+ disabled: disabled || undefined,
12
13
  });
13
14
  const onBlur = useEvent((...event) => {
14
15
  controllerField.onBlur();
package/dist/index.d.ts CHANGED
@@ -9,4 +9,3 @@ export { default as DatePicker } from './date-picker';
9
9
  export { default as Radio } from './radio';
10
10
  export { default as Select } from './select';
11
11
  export { default as Upload } from './upload';
12
- export { default as PhoneNumber } from './phone-number';
package/dist/index.js CHANGED
@@ -10,4 +10,3 @@ export { default as DatePicker } from './date-picker';
10
10
  export { default as Radio } from './radio';
11
11
  export { default as Select } from './select';
12
12
  export { default as Upload } from './upload';
13
- export { default as PhoneNumber } from './phone-number';
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { CountryCode } from 'libphonenumber-js';
3
+ interface PhoneNumberProps {
4
+ name: string;
5
+ onChange?: (value: string) => void;
6
+ onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
7
+ defaultCountry?: CountryCode;
8
+ maxLength?: number;
9
+ placeholder: string;
10
+ disabled?: boolean;
11
+ }
12
+ interface PhoneNumberRef extends HTMLInputElement {
13
+ childType: string;
14
+ }
15
+ declare const PhoneNumber: React.ForwardRefExoticComponent<PhoneNumberProps & React.RefAttributes<PhoneNumberRef>>;
16
+ export default PhoneNumber;
@@ -0,0 +1,72 @@
1
+ import React, { useImperativeHandle, useState, forwardRef, useEffect } from 'react';
2
+ import { parsePhoneNumberFromString, getCountryCallingCode, } from 'libphonenumber-js';
3
+ import BaseInput from '../BaseInput';
4
+ import { useInput } from '../../form/hooks/useInput';
5
+ const PhoneNumber = forwardRef((props, ref) => {
6
+ const { name, onChange, onBlur, defaultCountry = 'AM', maxLength = 14, placeholder, disabled } = props;
7
+ const { field } = useInput({
8
+ type: 'phone-number',
9
+ name,
10
+ onChange,
11
+ onBlur,
12
+ });
13
+ const defaultPrefix = `+${getCountryCallingCode(defaultCountry)}`;
14
+ const [inputValue, setInputValue] = useState(field.value || defaultPrefix);
15
+ const [selectedCountry, setSelectedCountry] = useState(defaultCountry);
16
+ // ✅ Attach childType to the input element
17
+ useImperativeHandle(ref, () => ({
18
+ // @ts-ignore
19
+ ...field.ref, // Preserve default input methods
20
+ childType: 'phone', // Custom property
21
+ }));
22
+ // ✅ Ensure the prefix is set when the component mounts
23
+ useEffect(() => {
24
+ if (!field.value || !field.value.startsWith('+')) {
25
+ let val = defaultPrefix;
26
+ if (field.value) {
27
+ val = parsePhoneNumberFromString(field.value, selectedCountry).formatInternational();
28
+ }
29
+ setInputValue(val);
30
+ field.onChange(val);
31
+ }
32
+ }, []);
33
+ const handleChange = (e) => {
34
+ let value = e.target.value;
35
+ // ✅ Remove all characters except numbers and "+"
36
+ value = value.replace(/[^\d+]/g, '');
37
+ // ✅ Ensure "+" is only at the start
38
+ if (value.includes('+') && value.indexOf('+') !== 0) {
39
+ value = value.replace(/\+/g, '');
40
+ }
41
+ // ✅ Remove any extra prefix occurrences (avoid +374+374 bug)
42
+ const countryCode = getCountryCallingCode(selectedCountry);
43
+ const cleanValue = value.replace(new RegExp(`^\\+?${countryCode}`), '');
44
+ // ✅ Ensure value always starts with "+374"
45
+ value = `+${countryCode}${cleanValue}`;
46
+ // ✅ If user deletes everything, reset only to "+374"
47
+ if (value === '+') {
48
+ value = `+${countryCode}`;
49
+ }
50
+ const phoneNumber = parsePhoneNumberFromString(value, selectedCountry);
51
+ if (phoneNumber) {
52
+ value = phoneNumber.formatInternational();
53
+ }
54
+ setInputValue(value);
55
+ field.onChange(phoneNumber ? phoneNumber.number : value);
56
+ };
57
+ const handleKeyDown = (e) => {
58
+ const countryCode = getCountryCallingCode(selectedCountry);
59
+ const prefix = `+${countryCode}`;
60
+ // ✅ Prevent backspace/delete from removing the prefix
61
+ if ((e.key === 'Backspace' || e.key === 'Delete') && inputValue === prefix) {
62
+ e.preventDefault();
63
+ }
64
+ };
65
+ // const handleCountryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
66
+ // setSelectedCountry(e.target.value);
67
+ // };
68
+ return (React.createElement(BaseInput, { ...props, value: field.value, disabled: disabled },
69
+ React.createElement("div", { className: "phone-number-container" },
70
+ React.createElement("input", { type: "tel", ...field, value: inputValue, className: "phone-number-input", disabled: disabled, onKeyDown: handleKeyDown, onChange: handleChange, onBlur: field.onBlur, maxLength: maxLength, placeholder: placeholder }))));
71
+ });
72
+ export default PhoneNumber;
@@ -2,10 +2,12 @@ import Input from './Input';
2
2
  import TextArea from './TextArea';
3
3
  import Password from './Password';
4
4
  import Number from './Number';
5
+ import PhoneNumber from './PhoneNumber';
5
6
  type InputComponent = typeof Input & {
6
7
  TextArea: typeof TextArea;
7
8
  Password: typeof Password;
8
9
  Number: typeof Number;
10
+ PhoneNumber: typeof PhoneNumber;
9
11
  };
10
12
  declare const EnhancedInput: InputComponent;
11
13
  export default EnhancedInput;
@@ -2,9 +2,11 @@ import Input from './Input';
2
2
  import TextArea from './TextArea';
3
3
  import Password from './Password';
4
4
  import Number from './Number';
5
+ import PhoneNumber from './PhoneNumber';
5
6
  // Assign the sub-components to the Input component
6
7
  const EnhancedInput = Input;
7
8
  EnhancedInput.TextArea = TextArea;
8
9
  EnhancedInput.Password = Password;
9
10
  EnhancedInput.Number = Number;
11
+ EnhancedInput.PhoneNumber = PhoneNumber;
10
12
  export default EnhancedInput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weareconceptstudio/form",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Concept Studio Form",
5
5
  "author": "Concept Studio",
6
6
  "license": "ISC",
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@weareconceptstudio/core": "*",
24
- "react-hook-form": "^7.52.1",
25
- "react-phone-number-input": "3.4.8",
24
+ "libphonenumber-js": "1.11.20",
25
+ "react-hook-form": "7.54.2",
26
26
  "react-select": "5.8.0"
27
27
  },
28
28
  "devDependencies": {
@@ -1,3 +0,0 @@
1
- export default PhoneNumber;
2
- declare const PhoneNumber: React.ForwardRefExoticComponent<React.RefAttributes<any>>;
3
- import React from 'react';
@@ -1,37 +0,0 @@
1
- import React, { forwardRef, useImperativeHandle, useEffect } from 'react';
2
- import classNames from 'classnames';
3
- import PhoneInput from 'react-phone-number-input/input';
4
- import { parsePhoneNumber } from 'react-phone-number-input';
5
- import useFormInstance from '../form/hooks/useFormInstance';
6
- import { Controller } from 'react-hook-form';
7
- const PhoneNumber = forwardRef(({ name, className, autoComplete = 'new-password', country = 'AM', maxLength = 14, options = {} }, ref) => {
8
- useImperativeHandle(ref, () => ({ childType: 'phone' }));
9
- const { control, setValue, getValues, trigger } = useFormInstance();
10
- const classString = classNames('phone-number-input', {
11
- [className]: className,
12
- });
13
- useEffect(() => {
14
- let defaultValue = getValues(name);
15
- try {
16
- if (defaultValue && !defaultValue.startsWith('+')) {
17
- const parsed = parsePhoneNumber(defaultValue, country);
18
- if (parsed)
19
- defaultValue = parsed.number;
20
- }
21
- }
22
- catch (e) { }
23
- setValue(name, defaultValue);
24
- }, [name, getValues, setValue, country]);
25
- return (React.createElement(Controller, { name: name, control: control, render: ({ field: { onChange, value, onBlur } }) => {
26
- return (React.createElement(PhoneInput, { name: name, international: true, value: value, country: country, maxLength: maxLength, withCountryCallingCode: true, className: classString, autoComplete: autoComplete, onChange: (value) => {
27
- onChange(value || '');
28
- setValue(name, value || '');
29
- // control._options.mode == 'onChange' && trigger(name);
30
- trigger(name);
31
- }, onBlur: () => {
32
- onBlur();
33
- // control._options.mode == 'onBlur' && trigger(name);
34
- }, ...options }));
35
- } }));
36
- });
37
- export default PhoneNumber;