Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 38 additions & 26 deletions packages/common-helpers/src/form/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import isEqual from "lodash-es/isEqual";
import type {
iFormInput,
iFormInputDefault,
iFormOption,
iFormValue,
iSelectOption,
tFormAutocomplete,
tFormIcon,
tFormInputDefault,
tOptionsLoaderFn,
} from "@open-xamu-co/ui-common-types";
import {
eFormType,
Expand Down Expand Up @@ -130,7 +131,7 @@ export class FormInput<
implements iFormInput<V, T>
{
// private
private _options: iSelectOption[];
private _options: iFormOption[];
private _values: V[];
private _defaults?: [
iFormInputDefault<eFormTypeBase | eFormTypeSimple | eFormTypeComplex>,
Expand All @@ -145,6 +146,7 @@ export class FormInput<
// public readonly
public readonly name: string;
public readonly title?: string;
public readonly optionsFilter?: tOptionsLoaderFn;

/**
* Form input constructor
Expand All @@ -163,46 +165,47 @@ export class FormInput<
this.name = formInput.name;
this.multiple = formInput.multiple ?? false;
this.title = formInput.title;
this._options = formInput.options?.map(toOption) ?? [];

// Initialize options array, skip if function
if (Array.isArray(formInput.options)) {
this._options = formInput.options.map(toOption);
this.optionsFilter = undefined;
} else {
this._options = [];
this.optionsFilter = formInput.options;
}

this._defaults = formInput.defaults;
this.min = formInput.min ?? 1;
this.meta = formInput.meta || {};

// max cannot be lower than min or more than options if they exist
// Max cannot be lower than min or more than options if they exist
const maxValue = this._options.length || formInput.max || 9e9;

this.max = maxValue < this.min ? this.min : maxValue;
this._values = formInput.values ||= [];

if (isChoiceType(this.type)) {
// autoset single value if required
if (this.required && !this._values.length) {
const values = this.options.map(({ value }) => value);

this._values = values.slice(0, Math.max(1, this.min)) as V[];
}
// Autoset values if required
if (this.required && !this._values.length) this.autosetValues();
} else if (this.type !== eFormType.FILE) {
const length = Math.max(1, this.min); // negative values fallback
const length = Math.max(1, this.min); // Negative values fallback
const values = Array(length).fill(getDefault(formInput.type, formInput.defaults));

// use defaults
// Use defaults
if (this._values.length < length) this._values = values;
}
}

get options(): iSelectOption[] {
get options(): iFormOption[] {
return this._options;
}
set options(updatedOptions: iSelectOption[] | undefined) {
set options(updatedOptions: iFormOption[] | undefined) {
this._options = updatedOptions || [];

if (isChoiceType(this.type)) {
// autoset single value if required
if (this.required && !this._values.length) {
const values = <V[]>this.options.map(({ value }) => value);

this._values = values.slice(0, Math.max(1, this.min));
}
// Autoset values if required
if (this.required && !this._values.length) this.autosetValues();
}

this.rerender();
Expand All @@ -217,12 +220,8 @@ export class FormInput<
this._values = [];

if (isChoiceType(this.type)) {
// autoset single value if required
if (this.required && !this._values.length) {
const values = <V[]>this.options.map(({ value }) => value);

this._values = values.slice(0, Math.max(1, this.min));
}
// Autoset values if required
if (this.required && !this._values.length) this.autosetValues();
} else if (this.type !== eFormType.FILE) {
const length = Math.max(1, this.min); // negative values fallback
const values = Array(length).fill(getDefault(this.type, this.defaults));
Expand All @@ -246,6 +245,19 @@ export class FormInput<
this.rerender();
}

/** Autoset values */
private autosetValues() {
const autosetValuesArr = [];

for (let i = 0; i < Math.max(1, this.min); i++) {
const option = this._options[i];

autosetValuesArr.push((option?.value || "") as V);
}

this._values = autosetValuesArr as V[];
}

/**
* set rerender function
*
Expand Down
9 changes: 8 additions & 1 deletion packages/common-helpers/src/format.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { iFormOption, iSelectOption } from "@open-xamu-co/ui-common-types";
import type { iFormInputOptions, iFormOption, iSelectOption } from "@open-xamu-co/ui-common-types";

/**
* Safe length for Vue keys / counts when `options` may be a static array or an async loader.
*/
export function getFormInputOptionsLength(options?: iFormInputOptions): number {
return Array.isArray(options) ? options.length : 0;
}

/**
* create iSelectOption or iFormOption from compatible values
Expand Down
6 changes: 3 additions & 3 deletions packages/common-helpers/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ export default function useI18n<L extends Record<string, string | Record<string,
let locale = get(options.locale || {}, key, fallback);
const interpolate = /\{(.+?)\}/g;
const plurals = locale.split("|");
const count = typeof data === "number" ? data : (data?.count ?? -1);
const count = (typeof data === "object" ? data.count : data) ?? -1;

// Pluralization
if (count > -1 && plurals.length > 1) {
if (plurals.length > 1) {
if (plurals.length === 2) {
// product, products
locale = plurals[count > 1 ? 1 : 0];
} else if (plurals.length === 3) {
// no products, a product, products
locale = plurals[count ? (count > 1 ? 2 : 1) : 0];
locale = plurals[count > 0 ? (count > 1 ? 2 : 1) : 0];
}
}

Expand Down
14 changes: 8 additions & 6 deletions packages/common-types/src/form/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import type {
import type {
iFormInput,
iFormInputDefault,
iFormOption,
iFormValue,
tFormAutocomplete,
tFormIcon,
tOptionsLoaderFn,
} from "./types";
import type { iSelectOption } from "../values.js";

export declare abstract class tFormInputDefault<
T extends eFormTypeBase | eFormTypeSimple | eFormTypeComplex = eFormTypeSimple,
Expand Down Expand Up @@ -44,12 +45,13 @@ export declare abstract class tFormInput<
// public readonly
public readonly name: string;
public readonly title?: string;
public readonly multiple: boolean;
public readonly min: number;
public readonly max: number;
public readonly meta?: Record<string, any>;
public readonly optionsFilter?: tOptionsLoaderFn;
// public
public options: iSelectOption[];
public multiple: boolean;
public min: number;
public max: number;
public meta?: Record<string, any>;
public options: iFormOption[];
public values: V[];
public defaults?: [
iFormInputDefault<eFormTypeBase | eFormTypeSimple | eFormTypeComplex>,
Expand Down
23 changes: 22 additions & 1 deletion packages/common-types/src/form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,29 @@ export interface iFormInputDefault<
autocomplete?: tFormAutocomplete;
}

export type tOptionsLoaderFn =
| ((v?: string | number, signal?: AbortSignal) => iFormOption[])
| ((v?: string | number, signal?: AbortSignal) => Promise<iFormOption[]>);

/** If a function is given, it should be responsible of determining if v is a value or an alias (search) */
export type iFormInputOptions = (string | number | iFormOption)[] | tOptionsLoaderFn;

/**
* Complex input, sub input support
* Used as input for the classes
*/
export interface iFormInput<
V extends iFormValue | iFormValue[],
T extends eFormTypeBase | eFormTypeSimple | eFormTypeComplex,
> extends iFormInputDefault<T> {
name: string;
options?: (string | number | iFormOption)[];
/**
* Options for select, checkbox, radio inputs
* The usage of a function requires a compatible component (SelectFilter)
*
* @values array of options or function that returns options
*/
options?: iFormInputOptions;
/**
* An array of values to simplify validation
*
Expand Down Expand Up @@ -189,6 +203,13 @@ export interface iFetchResponse<R = any> {
[x: string]: any;
}

/**
* Takes the parsed inputs values and perform a request (content creation, update, etc).
*
* @param values Values of the input
* @param fetcher Request function
* @returns Response with data and errors if any
*/
export type tResponseFn<T, V extends Record<string, any> = Record<string, any>> = (
values: V
) => Promise<iFetchResponse<T>>;
3 changes: 2 additions & 1 deletion packages/components-vue/src/components/base/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
const { t } = useHelpers(useI18n);

const selectOptions = computed<iFormOption[]>(() => {
return (props.options ?? []).map(toOption);
// Only use array type, skip if function
return Array.isArray(props.options) ? props.options.map(toOption) : [];
});
/** Prefer a predictable identifier */
const selectId = computed(() => {
Expand Down
5 changes: 3 additions & 2 deletions packages/components-vue/src/components/collapse/Simple.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<template>
<nav class="list">
<nav class="list" :class="`--txtColor-${themeValues[0]}`">
<BaseInput
v-if="title || $slots.header"
v-slot="{ id: baseId }"
:type="type || 'checkbox'"
v-bind="{ id, name, title, checked, theme }"
>
<label :for="id || baseId" class="toggle--list" :class="`--txtColor-${themeValues[0]}`">
<label :for="id || baseId" class="toggle--list" :class="headerClasses">
<slot name="header">
<span>{{ title }}</span>
<IconFa name="angle-down" :size="20" />
Expand Down Expand Up @@ -37,6 +37,7 @@
title?: string;
checked?: boolean;
el?: vComponent | string;
headerClasses?: string;
}

/**
Expand Down
24 changes: 22 additions & 2 deletions packages/components-vue/src/components/form/Input.stories.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Meta, StoryObj } from "@storybook/vue3-vite";
import { ref } from "vue";

import { FormInput } from "@open-xamu-co/ui-common-helpers";
import { eFormType } from "@open-xamu-co/ui-common-enums";

import FormInputComponent from "./Input.vue";
import { eFormType } from "@open-xamu-co/ui-common-enums";
import { ref } from "vue";
import { mockOptionsLoader } from "../select/Filter.stories";

const nameInput = new FormInput({
name: "name",
Expand Down Expand Up @@ -52,4 +53,23 @@ export const Code: Story = {
},
};

export const AsyncOptions: Story = {
render: (args) => ({
components: { FormInputComponent },
setup() {
const model = ref<string[]>([""]);

return { args, model };
},
template: '<FormInputComponent v-bind="args" v-model="model" />',
}),
args: {
input: new FormInput({
name: "asyncOptions",
type: eFormType.SELECT_FILTER,
options: mockOptionsLoader,
}),
},
};

export default meta;
6 changes: 3 additions & 3 deletions packages/components-vue/src/components/form/Input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<FormInputLoop
v-else
v-slot="{ i }"
:key="input.options.length + models.length"
:key="getFormInputOptionsLength(input.options) + models.length"
:models="models"
:input="input"
:theme="theme"
Expand All @@ -48,7 +48,7 @@
v-for="(model, index) in models[i].value"
:key="
[
input.options.length,
getFormInputOptionsLength(input.options),
input.defaults?.[i]?.placeholder,
input.defaults?.[i]?.type,
i + Number(index),
Expand Down Expand Up @@ -310,7 +310,7 @@

import type { iInvalidInput, iSelectOption, tFormInput } from "@open-xamu-co/ui-common-types";
import { eFormType as eFT } from "@open-xamu-co/ui-common-enums";
import { useI18n, useForm } from "@open-xamu-co/ui-common-helpers";
import { useI18n, useForm, getFormInputOptionsLength } from "@open-xamu-co/ui-common-helpers";

import BaseBox from "../base/Box.vue";
import BaseErrorBoundary from "../base/ErrorBoundary.vue";
Expand Down
Loading
Loading