Create `@primevue/form` package and add `form` component

pull/6632/head
Mert Sincan 2024-10-18 15:48:40 +01:00
parent 69d0407fe6
commit 9870b303cf
19 changed files with 680 additions and 0 deletions

1
packages/form/README.md Normal file
View File

@ -0,0 +1 @@
# PrimeVue Form

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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`));

View File

@ -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'));

View File

@ -0,0 +1,42 @@
<script>
import BaseComponent from '@primevue/core/basecomponent';
import FormStyle from '@primevue/form/form/style';
export default {
name: 'BaseForm',
extends: BaseComponent,
style: FormStyle,
props: {
resolver: {
type: Function,
default: null
},
defaultValues: {
type: Object,
default: null
},
validateOnValueUpdate: {
type: [Boolean, Array],
default: true
},
validateOnBlur: {
type: [Boolean, Array],
default: false
},
validateOnMount: {
type: [Boolean, Array],
default: false
},
validateOnSubmit: {
type: [Boolean, Array],
default: true
}
},
provide() {
return {
$pcForm: this,
$parentInstance: this
};
}
};
</script>

130
packages/form/src/form/Form.d.ts vendored Normal file
View File

@ -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<any>;
/**
* Used to pass attributes to DOM elements inside the component.
* @type {FluidPassThroughOptions}
*/
pt?: PassThrough<FluidPassThroughOptions>;
/**
* 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<FluidEmitsOptions>;
/**
* **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<FluidProps, FluidSlots, FluidEmits>;
declare module 'vue' {
export interface GlobalComponents {
Fluid: DefineComponent<FluidProps, FluidSlots, FluidEmits>;
}
}
export default Fluid;

View File

@ -0,0 +1,37 @@
<template>
<form @submit.prevent="onSubmit" v-bind="ptmi('root')">
<slot :register :valid v-bind="states" />
</form>
</template>
<script>
import { omit } from '@primeuix/utils';
import { useForm } from '@primevue/form/useform';
import BaseForm from './BaseForm.vue';
export default {
name: 'Form',
extends: BaseForm,
inheritAttrs: false,
emits: ['submit'],
setup(props, { emit }) {
const $form = useForm(props);
const register = (field, options) => {
const [, fieldProps] = $form.defineField(field, options);
return fieldProps;
};
const onSubmit = $form.handleSubmit((e) => {
emit('submit', e);
});
return {
register,
onSubmit,
...omit($form, ['defineField', 'handleSubmit'])
};
}
};
</script>

View File

@ -0,0 +1,11 @@
{
"main": "./Form.vue",
"module": "./Form.vue",
"types": "./Form.d.ts",
"browser": {
"./sfc": "./Form.vue"
},
"sideEffects": [
"*.vue"
]
}

View File

@ -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 {}

View File

@ -0,0 +1,5 @@
import BaseStyle from '@primevue/core/base/style';
export default BaseStyle.extend({
name: 'form'
});

View File

@ -0,0 +1,6 @@
{
"main": "./FormStyle.js",
"module": "./FormStyle.js",
"types": "./FormStyle.d.ts",
"sideEffects": false
}

0
packages/form/src/index.d.ts vendored Normal file
View File

View File

@ -0,0 +1,3 @@
export { default as Form } from '@primevue/form/form';
export * from '@primevue/form/resolvers';
export * from '@primevue/form/useform';

View File

@ -0,0 +1 @@
export * from '@primeuix/form/resolvers';

View File

@ -0,0 +1,4 @@
{
"main": "./index.js",
"module": "./index.js"
}

View File

@ -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)
};
};

View File

@ -0,0 +1,4 @@
{
"main": "./index.js",
"module": "./index.js"
}

View File

@ -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"]
}