diff --git a/packages/form/README.md b/packages/form/README.md new file mode 100644 index 000000000..3848b9012 --- /dev/null +++ b/packages/form/README.md @@ -0,0 +1 @@ +# PrimeVue Form diff --git a/packages/form/package.json b/packages/form/package.json new file mode 100644 index 000000000..ce886d0ad --- /dev/null +++ b/packages/form/package.json @@ -0,0 +1,58 @@ +{ + "name": "@primevue/form", + "version": "4.1.0", + "author": "PrimeTek Informatics", + "description": "", + "homepage": "https://primevue.org/", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/primefaces/primevue.git", + "directory": "packages/form" + }, + "bugs": { + "url": "https://github.com/primefaces/primevue/issues" + }, + "main": "./src/index.js", + "module": "./src/index.js", + "types": "./src/index.d.ts", + "exports": { + ".": "./src/index.js", + "./form/style": "./src/form/style/FormStyle.js", + "./form": "./src/form/Form.vue", + "./*": "./src/*/index.js" + }, + "publishConfig": { + "main": "./index.mjs", + "module": "./index.mjs", + "types": "./index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.mjs" + }, + "./*": { + "types": "./*/index.d.ts", + "import": "./*/index.mjs" + } + }, + "directory": "dist", + "linkDirectory": false, + "access": "public" + }, + "scripts": { + "build": "NODE_ENV=production INPUT_DIR=src/ OUTPUT_DIR=dist/ pnpm run build:package", + "build:package": "pnpm run build:prebuild && rollup -c && pnpm run build:postbuild", + "build:prebuild": "node ./scripts/prebuild.mjs", + "build:postbuild": "node ./scripts/postbuild.mjs", + "dev:link": "pnpm link --global && npm link" + }, + "dependencies": { + "@primeuix/utils": "catalog:", + "@primeuix/form": "catalog:", + "@primevue/core": "workspace:*" + }, + "engines": { + "node": ">=12.11.0" + } +} diff --git a/packages/form/rollup.config.mjs b/packages/form/rollup.config.mjs new file mode 100644 index 000000000..a08826f70 --- /dev/null +++ b/packages/form/rollup.config.mjs @@ -0,0 +1,201 @@ +import alias from '@rollup/plugin-alias'; +import { babel } from '@rollup/plugin-babel'; +import resolve from '@rollup/plugin-node-resolve'; +import terser from '@rollup/plugin-terser'; +import postcss from 'rollup-plugin-postcss'; +import vue from 'rollup-plugin-vue'; + +import fs from 'fs-extra'; +import path, { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// @todo - Remove +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// globals +const GLOBALS = { + vue: 'Vue' +}; + +// externals +const GLOBAL_EXTERNALS = ['vue']; +const INLINE_EXTERNALS = [/@primevue\/core\/.*/]; +const EXTERNALS = [...GLOBAL_EXTERNALS, ...INLINE_EXTERNALS]; + +// alias +const ALIAS_ENTRIES = [ + { + find: /^@primevue\/icons\/(.*)$/, + replacement: path.resolve(__dirname, './src/$1/index.vue') + }, + { find: '@primevue/icons/baseicon/style', replacement: path.resolve(__dirname, './src/baseicon/style/BaseIconStyle.js') }, + { find: '@primevue/icons/baseicon', replacement: path.resolve(__dirname, './src/baseicon/BaseIcon.vue') } +]; + +// plugins +const BABEL_PLUGIN_OPTIONS = { + extensions: ['.js', '.vue'], + exclude: 'node_modules/**', + presets: ['@babel/preset-env'], + plugins: [], + skipPreflightCheck: true, + babelHelpers: 'runtime', + babelrc: false +}; + +const ALIAS_PLUGIN_OPTIONS = { + entries: ALIAS_ENTRIES +}; + +const POSTCSS_PLUGIN_OPTIONS = { + sourceMap: false +}; + +const TERSER_PLUGIN_OPTIONS = { + compress: { + keep_infinity: true, + pure_getters: true, + reduce_funcs: true + }, + mangle: { + reserved: ['theme', 'css'] + } +}; + +const PLUGINS = [vue(), postcss(POSTCSS_PLUGIN_OPTIONS), babel(BABEL_PLUGIN_OPTIONS)]; + +const ENTRY = { + entries: [], + onwarn(warning) { + if (warning.code === 'CIRCULAR_DEPENDENCY') { + //console.error(`(!) ${warning.message}`); + return; + } + }, + format: { + cjs_es(options) { + return ENTRY.format.cjs(options).es(options); + }, + cjs({ input, output, minify }) { + ENTRY.entries.push({ + onwarn: ENTRY.onwarn, + input, + plugins: [...PLUGINS, minify && terser(TERSER_PLUGIN_OPTIONS)], + external: EXTERNALS, + inlineDynamicImports: true, + output: [ + { + format: 'cjs', + file: `${output}${minify ? '.min' : ''}.cjs`, + sourcemap: true, + exports: 'auto' + } + ] + }); + + ENTRY.update.packageJson({ input, output, options: { main: `${output}.cjs` } }); + + return ENTRY.format; + }, + es({ input, output, minify }) { + ENTRY.entries.push({ + onwarn: ENTRY.onwarn, + input, + plugins: [...PLUGINS, minify && terser(TERSER_PLUGIN_OPTIONS)], + external: EXTERNALS, + inlineDynamicImports: true, + output: [ + { + format: 'es', + file: `${output}${minify ? '.min' : ''}.mjs`, + sourcemap: true, + exports: 'auto' + } + ] + }); + + ENTRY.update.packageJson({ input, output, options: { main: `${output}.mjs`, module: `${output}.mjs` } }); + + return ENTRY.format; + }, + umd({ name, input, output, minify }) { + ENTRY.entries.push({ + onwarn: ENTRY.onwarn, + input, + plugins: [alias(ALIAS_PLUGIN_OPTIONS), resolve(), ...PLUGINS, minify && terser(TERSER_PLUGIN_OPTIONS)], + external: GLOBAL_EXTERNALS, + inlineDynamicImports: true, + output: [ + { + format: 'umd', + name: name ?? 'PrimeVue', + file: `${output}${minify ? '.min' : ''}.js`, + globals: GLOBALS, + exports: 'auto' + } + ] + }); + + return ENTRY.format; + } + }, + update: { + packageJson({ input, output, options }) { + try { + const inputDir = path.resolve(__dirname, path.dirname(input)); + const outputDir = path.resolve(__dirname, path.dirname(output)); + const packageJson = path.resolve(outputDir, 'package.json'); + + !fs.existsSync(packageJson) && fs.copySync(path.resolve(inputDir, './package.json'), packageJson); + + const pkg = JSON.parse(fs.readFileSync(packageJson, { encoding: 'utf8', flag: 'r' })); + + !pkg?.main?.includes('.cjs') && (pkg.main = path.basename(options?.main) ? `./${path.basename(options.main)}` : pkg.main); + pkg.module = path.basename(options?.module) ? `./${path.basename(options.module)}` : packageJson.module; + pkg.types && (pkg.types = './index.d.ts'); + + fs.writeFileSync(packageJson, JSON.stringify(pkg, null, 4)); + } catch {} + } + } +}; + +function addIcons() { + const iconDir = path.resolve(__dirname, process.env.INPUT_DIR); + + fs.readdirSync(path.resolve(__dirname, iconDir), { withFileTypes: true }) + .filter((dir) => dir.isDirectory()) + .forEach(({ name: folderName }) => { + fs.readdirSync(path.resolve(__dirname, iconDir + '/' + folderName)).forEach((file) => { + if (/\.vue$/.test(file)) { + const input = process.env.INPUT_DIR + folderName + '/' + file; + const output = process.env.OUTPUT_DIR + folderName + '/index'; + + ENTRY.format.es({ input, output }); + } + }); + }); +} + +function addStyle() { + fs.readdirSync(path.resolve(__dirname, process.env.INPUT_DIR), { withFileTypes: true }) + .filter((dir) => dir.isDirectory()) + .forEach(({ name: folderName }) => { + try { + fs.readdirSync(path.resolve(__dirname, process.env.INPUT_DIR + folderName + '/style')).forEach((file) => { + if (/\.js$/.test(file)) { + const name = file.split(/(.js)$/)[0].toLowerCase(); + const input = process.env.INPUT_DIR + folderName + '/style/' + file; + const output = process.env.OUTPUT_DIR + folderName + '/style/index'; + + ENTRY.format.es({ input, output }); + } + }); + } catch {} + }); +} + +addIcons(); +addStyle(); + +export default ENTRY.entries; diff --git a/packages/form/scripts/postbuild.mjs b/packages/form/scripts/postbuild.mjs new file mode 100644 index 000000000..6ba562151 --- /dev/null +++ b/packages/form/scripts/postbuild.mjs @@ -0,0 +1,14 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { clearPackageJson, copyDependencies, renameDTSFile, resolvePath } from '../../../scripts/build-helper.mjs'; + +const { __dirname, __workspace, INPUT_DIR, OUTPUT_DIR } = resolvePath(import.meta.url); + +copyDependencies(INPUT_DIR, OUTPUT_DIR, '/style'); +renameDTSFile(OUTPUT_DIR, 'index'); + +fs.copySync(path.resolve(__dirname, '../package.json'), `${OUTPUT_DIR}/package.json`); +fs.copySync(path.resolve(__dirname, '../README.md'), `${OUTPUT_DIR}/README.md`); +fs.copySync(path.resolve(__workspace, './LICENSE.md'), `${OUTPUT_DIR}/LICENSE.md`); + +clearPackageJson(path.resolve(__dirname, `../${OUTPUT_DIR}/package.json`)); diff --git a/packages/form/scripts/prebuild.mjs b/packages/form/scripts/prebuild.mjs new file mode 100644 index 000000000..e8449e469 --- /dev/null +++ b/packages/form/scripts/prebuild.mjs @@ -0,0 +1,5 @@ +import path from 'path'; +import { removeBuild, resolvePath, updatePackageJson } from '../../../scripts/build-helper.mjs'; + +removeBuild(import.meta.url); +updatePackageJson(path.resolve(resolvePath(import.meta.url).__dirname, '../package.json')); diff --git a/packages/form/src/form/BaseForm.vue b/packages/form/src/form/BaseForm.vue new file mode 100644 index 000000000..3112e2091 --- /dev/null +++ b/packages/form/src/form/BaseForm.vue @@ -0,0 +1,42 @@ + diff --git a/packages/form/src/form/Form.d.ts b/packages/form/src/form/Form.d.ts new file mode 100644 index 000000000..41590557d --- /dev/null +++ b/packages/form/src/form/Form.d.ts @@ -0,0 +1,130 @@ +/** + * + * Fluid is a layout component to make descendant components span full width of their container. + * + * [Live Demo](https://www.primevue.org/fluid/) + * + * @module fluid + * + */ +import type { DefineComponent, DesignToken, EmitFn, PassThrough } from '@primevue/core'; +import type { ComponentHooks } from '@primevue/core/basecomponent'; +import type { PassThroughOptions } from 'primevue/passthrough'; +import { TransitionProps, VNode } from 'vue'; + +export declare type FluidPassThroughOptionType = FluidPassThroughAttributes | ((options: FluidPassThroughMethodOptions) => FluidPassThroughAttributes | string) | string | null | undefined; + +export declare type FluidPassThroughTransitionType = TransitionProps | ((options: FluidPassThroughMethodOptions) => TransitionProps) | undefined; + +/** + * Custom passthrough(pt) option method. + */ +export interface FluidPassThroughMethodOptions { + /** + * Defines instance. + */ + instance: any; + /** + * Defines valid properties. + */ + props: FluidProps; + /** + * Defines valid attributes. + */ + attrs: any; + /** + * Defines parent options. + */ + parent: any; + /** + * Defines passthrough(pt) options in global config. + */ + global: object | undefined; +} + +/** + * Custom passthrough(pt) options. + * @see {@link FluidProps.pt} + */ +export interface FluidPassThroughOptions { + /** + * Used to pass attributes to the root's DOM element. + */ + root?: FluidPassThroughOptionType; + /** + * Used to manage all lifecycle hooks. + * @see {@link BaseComponent.ComponentHooks} + */ + hooks?: ComponentHooks; +} + +/** + * Custom passthrough attributes for each DOM elements + */ +export interface FluidPassThroughAttributes { + [key: string]: any; +} + +/** + * Defines valid properties in Fluid component. + */ +export interface FluidProps { + /** + * It generates scoped CSS variables using design tokens for the component. + */ + dt?: DesignToken; + /** + * Used to pass attributes to DOM elements inside the component. + * @type {FluidPassThroughOptions} + */ + pt?: PassThrough; + /** + * Used to configure passthrough(pt) options of the component. + * @type {PassThroughOptions} + */ + ptOptions?: PassThroughOptions; + /** + * When enabled, it removes component related styles in the core. + * @defaultValue false + */ + unstyled?: boolean; +} + +/** + * Defines valid slots in Fluid component. + */ +export interface FluidSlots { + /** + * Default content slot. + */ + default: () => VNode[]; +} + +/** + * Defines valid emits in Fluid component. + */ +export interface FluidEmitsOptions {} + +export declare type FluidEmits = EmitFn; + +/** + * **PrimeVue - Fluid** + * + * _Fluid is a layout component to make descendant components span full width of their container._ + * + * [Live Demo](https://www.primevue.org/fluid/) + * --- --- + * ![PrimeVue](https://primefaces.org/cdn/primevue/images/logo-100.png) + * + * @group Component + * + */ +declare const Fluid: DefineComponent; + +declare module 'vue' { + export interface GlobalComponents { + Fluid: DefineComponent; + } +} + +export default Fluid; diff --git a/packages/form/src/form/Form.vue b/packages/form/src/form/Form.vue new file mode 100644 index 000000000..819f13e0e --- /dev/null +++ b/packages/form/src/form/Form.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/form/src/form/package.json b/packages/form/src/form/package.json new file mode 100644 index 000000000..1c8ef855d --- /dev/null +++ b/packages/form/src/form/package.json @@ -0,0 +1,11 @@ +{ + "main": "./Form.vue", + "module": "./Form.vue", + "types": "./Form.d.ts", + "browser": { + "./sfc": "./Form.vue" + }, + "sideEffects": [ + "*.vue" + ] +} diff --git a/packages/form/src/form/style/FormStyle.d.ts b/packages/form/src/form/style/FormStyle.d.ts new file mode 100644 index 000000000..9ae1b3400 --- /dev/null +++ b/packages/form/src/form/style/FormStyle.d.ts @@ -0,0 +1,12 @@ +/** + * + * + * + * [Live Demo](https://www.primevue.org/form/) + * + * @module formstyle + * + */ +import type { BaseStyle } from '@primevue/core/base/style'; + +export interface FormStyle extends BaseStyle {} diff --git a/packages/form/src/form/style/FormStyle.js b/packages/form/src/form/style/FormStyle.js new file mode 100644 index 000000000..4f5dbdfc8 --- /dev/null +++ b/packages/form/src/form/style/FormStyle.js @@ -0,0 +1,5 @@ +import BaseStyle from '@primevue/core/base/style'; + +export default BaseStyle.extend({ + name: 'form' +}); diff --git a/packages/form/src/form/style/package.json b/packages/form/src/form/style/package.json new file mode 100644 index 000000000..31fe6b840 --- /dev/null +++ b/packages/form/src/form/style/package.json @@ -0,0 +1,6 @@ +{ + "main": "./FormStyle.js", + "module": "./FormStyle.js", + "types": "./FormStyle.d.ts", + "sideEffects": false +} diff --git a/packages/form/src/index.d.ts b/packages/form/src/index.d.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/form/src/index.js b/packages/form/src/index.js new file mode 100644 index 000000000..7e19301ce --- /dev/null +++ b/packages/form/src/index.js @@ -0,0 +1,3 @@ +export { default as Form } from '@primevue/form/form'; +export * from '@primevue/form/resolvers'; +export * from '@primevue/form/useform'; diff --git a/packages/form/src/resolvers/index.js b/packages/form/src/resolvers/index.js new file mode 100644 index 000000000..17e6c1b9c --- /dev/null +++ b/packages/form/src/resolvers/index.js @@ -0,0 +1 @@ +export * from '@primeuix/form/resolvers'; diff --git a/packages/form/src/resolvers/package.json b/packages/form/src/resolvers/package.json new file mode 100644 index 000000000..7604ba623 --- /dev/null +++ b/packages/form/src/resolvers/package.json @@ -0,0 +1,4 @@ +{ + "main": "./index.js", + "module": "./index.js" +} diff --git a/packages/form/src/useform/index.js b/packages/form/src/useform/index.js new file mode 100644 index 000000000..920ea0f7f --- /dev/null +++ b/packages/form/src/useform/index.js @@ -0,0 +1,121 @@ +import { resolve } from '@primeuix/utils'; +import { computed, mergeProps, nextTick, onMounted, reactive, toValue, watch } from 'vue'; + +function tryOnMounted(fn, sync = true) { + if (getCurrentInstance()) onMounted(fn); + else if (sync) fn(); + else nextTick(fn); +} + +export const useForm = (options = {}) => { + const states = reactive({}); + const valid = computed(() => Object.values(states).every((field) => !field.invalid)); + + const getInitialState = (field) => { + return { + value: options.defaultValues?.[field], + touched: false, + dirty: false, + pristine: true, + valid: true, + invalid: false, + errors: [] + }; + }; + + const isFieldValidate = (field, validateOn) => { + const value = resolve(validateOn, field); + + return value === true || (Array.isArray(value) && value.includes(field)); + }; + + const defineField = (field, fieldOptions) => { + states[field] ||= getInitialState(field); + + const props = mergeProps(resolve(fieldOptions, states[field])?.props, resolve(fieldOptions?.props, states[field]), { + name: field, + onBlur: () => { + states[field].touched = true; + (fieldOptions?.validateOnBlur ?? isFieldValidate(field, options.validateOnBlur)) && validate(field); + }, + onChange: (event) => { + states[field].value = event.hasOwnProperty('value') ? event.value : event.target.type === 'checkbox' || event.target.type === 'radio' ? event.target.checked : event.target.value; + }, + onInvalid: (errors) => { + states[field].invalid = true; + states[field].errors = errors; + } + }); + + watch( + () => states[field].value, + (newValue, oldValue) => { + if (states[field].pristine) { + states[field].pristine = false; + } + + if (newValue !== oldValue) { + states[field].dirty = true; + } + + (fieldOptions?.validateOnBlur ?? isFieldValidate(field, options.validateOnValueUpdate ?? true)) && validate(field); + } + ); + + return [states[field], props]; + }; + + const handleSubmit = (callback) => { + return async (event) => { + let results = undefined; + + (options.validateOnSubmit ?? true) && (results = await validate()); + + return callback({ + originalEvent: event, + valid: toValue(valid), + states: toValue(states), + ...results + }); + }; + }; + + const validate = async (field) => { + const values = Object.entries(states).reduce((acc, [key, val]) => { + acc[key] = val.value; + + return acc; + }, {}); + + const result = (await options.resolver?.({ values })) ?? {}; + + for (const sField of Object.keys(states)) { + if (sField === field || !field) { + const errors = result.errors?.[sField] ?? []; + const value = result.values?.[sField] ?? states[sField].value; + + states[sField].invalid = errors.length > 0; + states[sField].valid = !states[sField].invalid; + states[sField].errors = errors; + states[sField].value = value; + } + } + + return result; + }; + + const reset = () => { + Object.keys(states).forEach((field) => (states[field] = getInitialState(field))); + }; + + options.validateOnMount && tryOnMounted(validate); + + return { + defineField, + handleSubmit, + validate, + reset, + valid: toValue(valid), + states: toValue(states) + }; +}; diff --git a/packages/form/src/useform/package.json b/packages/form/src/useform/package.json new file mode 100644 index 000000000..7604ba623 --- /dev/null +++ b/packages/form/src/useform/package.json @@ -0,0 +1,4 @@ +{ + "main": "./index.js", + "module": "./index.js" +} diff --git a/packages/form/tsconfig.json b/packages/form/tsconfig.json new file mode 100644 index 000000000..8a364744c --- /dev/null +++ b/packages/form/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": false, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": false, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@primevue/form/*": ["./src/*"], + "@primevue/core/*": ["../../packages/core/src/*"] + } + }, + "include": ["**/*.ts", "src/*"], + "exclude": ["node_modules", "dist"] +}