feat: add attribute types to input labels

This commit is contained in:
tglide
2022-12-12 18:38:10 +00:00
parent 7c784f78af
commit f1b4205fe6
16 changed files with 239 additions and 23 deletions
@@ -2,6 +2,7 @@
import { FormItem, Helper } from '.';
export let label: string;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let value = false;
@@ -27,6 +28,8 @@
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
<div class="input-text-wrapper">
<input
{id}
@@ -4,6 +4,7 @@
export let label: string;
export let showLabel = true;
export let optionalText: string | undefined = undefined;
export let id: string;
export let value = '';
export let required = false;
@@ -39,6 +40,8 @@
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
<div class="input-text-wrapper">
<input
{id}
@@ -3,6 +3,7 @@
import { FormItem, Helper } from '.';
export let label: string;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let value: number = null;
@@ -52,6 +53,8 @@
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
<div class="input-text-wrapper">
<input
{id}
@@ -3,6 +3,7 @@
export let id: string;
export let label: string;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let value: string | number | boolean;
export let placeholder = '';
@@ -33,6 +34,8 @@
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
<div class="select">
<select
{id}
+5 -2
View File
@@ -4,6 +4,7 @@
import { FormItem, Helper } from '.';
export let label: string;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let value = '';
@@ -42,8 +43,8 @@
</script>
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}
>{label}
<label class:u-hide={!showLabel} class="label" for={id}>
{label}
{#if tooltip}
<span
class="icon-info"
@@ -53,6 +54,8 @@
}} />
{/if}
</label>
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
<div class="input-text-wrapper">
<input
{id}
+75
View File
@@ -0,0 +1,75 @@
/**
* Strict typed `Object.entries`
* Extracted from https://github.com/antfu/utils
*
* @category Object
*/
export function objectEntries<T extends object>(obj: T) {
return Object.entries(obj) as Array<[keyof T, T[keyof T]]>;
}
type KeyTypesMap<T> = Record<keyof T, Array<string> | ((v: unknown) => boolean) | string>;
/**
* Checks if the given value is an object of type T, given a map of keys and
* their types/validators.
*
* @category Object
*
* @template T
* @param {unknown} object
* @param {KeyTypesMap} keyTypesMap
* @returns {value is T}
*
* @example
* ```
* type ExampleType = {
* a: string;
* b: number;
* c: string | number;
* d: string[]
* e: {
* f: string;
* g: number;
* }
* }
*
* export const isExampleType = (value: unknown): value is ExampleType => {
* return isObjectType(value, {
* a: 'string',
* b: 'number',
* c: ['string', 'number'],
* d: (v) => Array.isArray(v) && v.every((v) => typeof v === 'string'),
* e: (v) => isObjectType(v, {
* f: 'string',
* g: 'number',
* }),
* });
* };
* ```
*/
export const isObjectType = <T>(
object: unknown,
keyTypesMap: Partial<KeyTypesMap<T>>
): object is T => {
if (typeof object !== 'object' || object === null) {
return false;
}
for (const [key, check] of objectEntries(keyTypesMap)) {
const value = (object as Record<keyof T, unknown>)[key];
if (typeof check === 'function') {
if (!check(value)) {
return false;
}
} else if (typeof check === 'string') {
if (typeof value !== check) {
return false;
}
} else if (!check.includes(typeof value)) {
return false;
}
}
return true;
};
+10
View File
@@ -0,0 +1,10 @@
/**
* Capitalizes the first letter of a string
*
* @export
* @param {string} str
* @returns {string}
*/
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
@@ -9,6 +9,7 @@
export let id: string;
export let label: string;
export let value: string | number | boolean;
export let optionalText: string | undefined = undefined;
export let attribute:
| Models.AttributeBoolean
| Models.AttributeEmail
@@ -43,6 +44,7 @@
{id}
{label}
{attribute}
{optionalText}
bind:value />
{:else}
<svelte:component
@@ -50,6 +52,7 @@
{id}
{label}
{attribute}
{optionalText}
bind:value />
{/if}
{/if}
@@ -3,7 +3,8 @@
import { FormList } from '$lib/elements/forms';
import Button from '$lib/elements/forms/button.svelte';
import Pill from '$lib/elements/pill.svelte';
import type { Attributes } from '../store';
import { capitalize } from '$lib/helpers/string';
import { isAttributeEnum, type Attributes } from '../store';
import Attribute from './attribute.svelte';
export let attributes: Attributes[] = [];
@@ -26,6 +27,11 @@
[key]: [...formValues[key], null]
};
}
function getAttributeType(attribute: Attributes) {
if (isAttributeEnum(attribute)) return 'Enum';
return `${capitalize(attribute.type)}${attribute.array ? '[]' : ''}`;
}
</script>
{#if attributes.length}
@@ -35,7 +41,11 @@
{#if attribute.array}
{#if formValues[attribute.key].length === 0}
<div class="u-flex u-cross-center u-main-space-between">
<span class="label u-margin-0">{label}</span>
<div class="u-flex u-cross-center u-gap-4">
<span class="label u-margin-0">{label}</span>
<span class="optional">{getAttributeType(attribute)}</span>
</div>
<Button text noMargin on:click={() => addArrayItem(attribute.key)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text"> Add item</span>
@@ -43,24 +53,30 @@
</div>
{/if}
{#each [...formValues[attribute.key].keys()] as index}
<li class="form-item is-multiple">
<div class="form-item-part u-stretch">
<Attribute
{attribute}
id={`${attribute.key}-${index}`}
label={index === 0 ? label : ''}
bind:value={formValues[attribute.key][index]} />
</div>
<div class="form-item-part u-cross-child-end">
<Button text on:click={() => removeArrayItem(attribute.key, index)}>
<span class="icon-x" aria-hidden="true" />
</Button>
</div>
</li>
{/each}
{#if formValues[attribute.key].length !== 0}
<ul class="u-grid u-gap-8">
{#each [...formValues[attribute.key].keys()] as index}
<li class="form-item is-multiple">
<div class="form-item-part u-stretch">
<Attribute
{attribute}
optionalText={index === 0
? getAttributeType(attribute)
: undefined}
id={`${attribute.key}-${index}`}
label={index === 0 ? label : ''}
bind:value={formValues[attribute.key][index]} />
</div>
<div class="form-item-part u-cross-child-end">
<Button
text
on:click={() => removeArrayItem(attribute.key, index)}>
<span class="icon-x" aria-hidden="true" />
</Button>
</div>
</li>
{/each}
</ul>
<Button text noMargin on:click={() => addArrayItem(attribute.key)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text"> Add item</span>
@@ -71,6 +87,7 @@
<Attribute
{attribute}
{label}
optionalText={getAttributeType(attribute)}
id={attribute.key}
bind:value={formValues[attribute.key]} />
</FormList>
@@ -6,12 +6,14 @@
export let label: string;
export let value: boolean;
export let attribute: Models.AttributeBoolean;
export let optionalText: string | undefined = undefined;
</script>
{#if attribute?.array}
<InputSelect
{id}
{label}
{optionalText}
showLabel={!!label?.length}
placeholder="Select a value"
required={attribute.required}
@@ -21,5 +23,5 @@
]}
bind:value />
{:else}
<InputCheckbox {id} {label} required={attribute.required} bind:value />
<InputCheckbox {id} {label} {optionalText} required={attribute.required} bind:value />
{/if}
@@ -6,6 +6,13 @@
export let label: string;
export let value: string;
export let attribute: Models.AttributeDatetime;
export let optionalText: string | undefined = undefined;
</script>
<InputDateTime {id} {label} showLabel={!!label?.length} required={attribute.required} bind:value />
<InputDateTime
{id}
{label}
{optionalText}
showLabel={!!label?.length}
required={attribute.required}
bind:value />
@@ -6,6 +6,7 @@
export let label: string;
export let value: string;
export let attribute: Models.AttributeEnum;
export let optionalText: string | undefined = undefined;
$: options = attribute.elements.map((element) => {
return {
@@ -20,5 +21,6 @@
{options}
{id}
{label}
{optionalText}
placeholder="Select a value"
showLabel={!!label?.length} />
@@ -6,11 +6,13 @@
export let label: string;
export let value: number;
export let attribute: Models.AttributeInteger;
export let optionalText: string | undefined = undefined;
</script>
<InputNumber
{id}
{label}
{optionalText}
required={attribute.required}
min={attribute.min}
max={attribute.max}
@@ -6,11 +6,13 @@
export let label: string;
export let value: string;
export let attribute: Models.AttributeString;
export let optionalText: string | undefined = undefined;
</script>
<InputText
{id}
{label}
{optionalText}
showLabel={!!label?.length}
required={attribute.required}
maxlength={attribute.size}
@@ -1,6 +1,7 @@
import { derived } from 'svelte/store';
import { page } from '$app/stores';
import type { Models } from '@aw-labs/appwrite-console';
import { isObjectType } from '$lib/helpers/object';
export type Attributes =
| Models.AttributeBoolean
@@ -12,6 +13,12 @@ export type Attributes =
| Models.AttributeString
| Models.AttributeUrl;
export function isAttributeEnum(attribute: unknown): attribute is Models.AttributeEnum {
return isObjectType<Models.AttributeEnum>(attribute, {
format: (v) => v === 'enum'
});
}
type Collection = Omit<Models.Collection, 'attributes'> & {
attributes: Array<Attributes>;
};
+74
View File
@@ -0,0 +1,74 @@
import { isObjectType, objectEntries } from '$lib/helpers/object';
test('objectEntries', () => {
const object = {
a: 1,
b: 2,
c: 3
};
expect(objectEntries(object)).toEqual(Object.entries(object));
});
test('isObjectType', () => {
type TestType = {
a: string;
b: number;
c: string | number;
d: string[];
e: {
f: string;
g: number;
};
h: undefined | null | string;
};
const valid_type = {
a: '1',
b: 1,
c: '1',
d: ['1', '2'],
e: {
f: '1',
g: 1
},
h: null
};
const invalid_type = {
a: 1,
b: '1',
c: true
};
const invalid_type_2 = {
...valid_type,
d: [1, '2']
};
const invalid_type_3 = {
...valid_type,
c: null
};
const isTestType = (value: unknown): value is TestType => {
return isObjectType(value, {
a: 'string',
b: 'number',
c: ['string', 'number'],
d: (v) => Array.isArray(v) && v.every((v) => typeof v === 'string'),
e: (v) =>
isObjectType(v, {
f: 'string',
g: 'number'
}),
h: (v) => v === null || v === undefined || typeof v === 'string'
});
};
expect(isTestType(valid_type)).toBe(true);
expect(isTestType(invalid_type)).toBe(false);
expect(isTestType(invalid_type_2)).toBe(false);
expect(isTestType(invalid_type_3)).toBe(false);
expect(isTestType('12331')).toBe(false);
});