data form component date picker aware, form component refactor

This commit is contained in:
Martin McKeaveney 2020-09-10 13:04:45 +01:00
parent 53c10f4479
commit 44426b6bf8
4 changed files with 116 additions and 197 deletions

View File

@ -21,7 +21,7 @@
"publishdev": "lerna run publishdev", "publishdev": "lerna run publishdev",
"publishnpm": "yarn build && lerna publish --force-publish", "publishnpm": "yarn build && lerna publish --force-publish",
"clean": "lerna clean", "clean": "lerna clean",
"dev": "node ./scripts/symlinkDev.js && lerna run --parallel --stream dev:builder", "dev": "node ./scripts/symlinkDev.js && lerna run --parallel dev:builder",
"test": "lerna run test", "test": "lerna run test",
"lint": "eslint packages", "lint": "eslint packages",
"lint:fix": "eslint --fix packages", "lint:fix": "eslint --fix packages",

View File

@ -41,6 +41,7 @@
"d3-selection": "^1.4.2", "d3-selection": "^1.4.2",
"fast-sort": "^2.2.0", "fast-sort": "^2.2.0",
"fusioncharts": "^3.15.1-sr.1", "fusioncharts": "^3.15.1-sr.1",
"lodash.debounce": "^4.0.8",
"svelte-flatpickr": "^2.4.0", "svelte-flatpickr": "^2.4.0",
"svelte-fusioncharts": "^1.0.0" "svelte-fusioncharts": "^1.0.0"
} }

View File

@ -1,7 +1,8 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { Label } from "@budibase/bbui" import { Label, DatePicker } from "@budibase/bbui"
import debounce from "lodash.debounce"
export let _bb export let _bb
export let model export let model
@ -14,60 +15,52 @@
number: "number", number: "number",
} }
const DEFAULTS_FOR_TYPE = {
string: "",
boolean: false,
number: null,
link: [],
}
let record let record
let store = _bb.store let store = _bb.store
let schema = {} let schema = {}
let modelDef = {} let modelDef = {}
let saved = false let saved = false
let saving = false
let recordId let recordId
let isNew = true let isNew = true
let errors = {}
let inputElements = {}
$: if (model && model.length !== 0) { $: if (model && model.length !== 0) {
fetchModel() fetchModel()
} }
$: fields = Object.keys(schema) $: fields = schema ? Object.keys(schema) : []
$: Object.values(inputElements).length && setForm(record) $: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
const createBlankRecord = () => { $: console.log(record)
if (!schema) return
const newrecord = {
modelId: model,
}
for (let fieldName in schema) {
const field = schema[fieldName]
// defaulting to first one, as a blank value will fail validation
if (
field.type === "string" &&
field.constraints &&
field.constraints.inclusion &&
field.constraints.inclusion.length > 0
) {
newrecord[fieldName] = field.constraints.inclusion[0]
} else if (field.type === "number") newrecord[fieldName] = null
else if (field.type === "boolean") newrecord[fieldName] = false
else if (field.type === "link") newrecord[fieldName] = []
else newrecord[fieldName] = ""
}
return newrecord
}
async function fetchModel() { async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}` const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL) const response = await _bb.api.get(FETCH_MODEL_URL)
modelDef = await response.json() modelDef = await response.json()
schema = modelDef.schema schema = modelDef.schema
record = createBlankRecord() // record = createBlankRecord()
record = {
modelId: model,
}
}
const save = debounce(async () => {
for (let field of fields) {
// Assign defaults to empty fields to prevent validation issues
if (!(field in record))
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
} }
async function save() {
// prevent double clicking firing multiple requests
if (saving) return
saving = true
const SAVE_RECORD_URL = `/api/${model}/records` const SAVE_RECORD_URL = `/api/${model}/records`
const response = await _bb.api.post(SAVE_RECORD_URL, record) const response = await _bb.api.post(SAVE_RECORD_URL, record)
@ -79,13 +72,11 @@
return state return state
}) })
errors = {}
// wipe form, if new record, otherwise update // wipe form, if new record, otherwise update
// model to get new _rev // model to get new _rev
if (isNew) { record = isNew ? { modelId: model } : json
resetForm()
} else {
record = json
}
// set saved, and unset after 1 second // set saved, and unset after 1 second
// i.e. make the success notifier appear, then disappear again after time // i.e. make the success notifier appear, then disappear again after time
@ -94,69 +85,27 @@
saved = false saved = false
}, 1000) }, 1000)
} }
saving = false
}
// we cannot use svelte bind on these inputs, as it does not allow if (response.status === 400) {
// bind, when the input type is dynamic errors = json.errors
const resetForm = () => {
for (let el of Object.values(inputElements)) {
el.value = ""
if (el.checked) {
el.checked = false
}
}
record = createBlankRecord()
} }
})
const setForm = rec => { onMount(async () => {
if (isNew || !rec) return
for (let fieldName in inputElements) {
if (typeof rec[fieldName] === "boolean") {
inputElements[fieldName].checked = rec[fieldName]
} else {
inputElements[fieldName].value = rec[fieldName]
}
}
}
const handleInput = field => event => {
let value
if (event.target.type === "checkbox") {
value = event.target.checked
record[field] = value
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
record[field] = value
return
}
value = event.target.value
record[field] = value
}
onMount(() => {
const routeParams = _bb.routeParams() const routeParams = _bb.routeParams()
recordId = recordId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0]) Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !recordId || recordId === "new" isNew = !recordId || recordId === "new"
if (isNew) { if (isNew) {
record = createBlankRecord() record = { modelId: model }
} else { return
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
_bb.api
.get(GET_RECORD_URL)
.then(response => response.json())
.then(rec => {
record = rec
setForm(rec)
})
} }
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
const response = await _bb.api.get(GET_RECORD_URL)
const json = await response.json()
record = json
}) })
</script> </script>
@ -164,23 +113,28 @@
{#if title} {#if title}
<h1>{title}</h1> <h1>{title}</h1>
{/if} {/if}
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<hr /> <hr />
<div class="form-content"> <div class="form-content">
{#each fields as field} {#each fields as field}
<div class="form-item"> <div class="form-item">
<Label small forAttr={'form-stacked-text'}>{field}</Label> <Label small forAttr={'form-stacked-text'}>{field}</Label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion} {#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)} bind:this={inputElements[field]}> <select bind:value={record[field]}>
{#each schema[field].constraints.inclusion as opt} {#each schema[field].constraints.inclusion as opt}
<option>{opt}</option> <option>{opt}</option>
{/each} {/each}
</select> </select>
{:else} {:else if schema[field].type === 'datetime'}
<input <DatePicker bind:value={record[field]} />
bind:this={inputElements[field]} {:else if schema[field].type === 'boolean'}
class="input" <input class="input" type="checkbox" bind:value={record[field]} />
type={TYPE_MAP[schema[field].type]} {:else if schema[field].type === 'number'}
on:change={handleInput(field)} /> <input class="input" type="number" bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<input class="input" type="text" bind:value={record[field]} />
{/if} {/if}
</div> </div>
<hr /> <hr />
@ -302,4 +256,9 @@
background-position: right 17px top 1.5em, right 10px top 1.5em; background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px; background-size: 7px 7px, 7px 7px;
} }
.error {
color: red;
font-weight: 500;
}
</style> </style>

View File

@ -1,7 +1,8 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { Label } from "@budibase/bbui" import { Label, DatePicker } from "@budibase/bbui"
import debounce from "lodash.debounce"
export let _bb export let _bb
export let model export let model
@ -14,60 +15,52 @@
number: "number", number: "number",
} }
const DEFAULTS_FOR_TYPE = {
string: "",
boolean: false,
number: null,
link: [],
}
let record let record
let store = _bb.store let store = _bb.store
let schema = {} let schema = {}
let modelDef = {} let modelDef = {}
let saved = false let saved = false
let saving = false
let recordId let recordId
let isNew = true let isNew = true
let errors = {}
let inputElements = {}
$: if (model && model.length !== 0) { $: if (model && model.length !== 0) {
fetchModel() fetchModel()
} }
$: fields = Object.keys(schema) $: fields = schema ? Object.keys(schema) : []
$: Object.values(inputElements).length && setForm(record) $: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
const createBlankRecord = () => { $: console.log(record)
if (!schema) return
const newrecord = {
modelId: model,
}
for (let fieldName in schema) {
const field = schema[fieldName]
// defaulting to first one, as a blank value will fail validation
if (
field.type === "string" &&
field.constraints &&
field.constraints.inclusion &&
field.constraints.inclusion.length > 0
) {
newrecord[fieldName] = field.constraints.inclusion[0]
} else if (field.type === "number") newrecord[fieldName] = null
else if (field.type === "boolean") newrecord[fieldName] = false
else if (field.type === "link") newrecord[fieldName] = []
else newrecord[fieldName] = ""
}
return newrecord
}
async function fetchModel() { async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}` const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL) const response = await _bb.api.get(FETCH_MODEL_URL)
modelDef = await response.json() modelDef = await response.json()
schema = modelDef.schema schema = modelDef.schema
record = createBlankRecord() // record = createBlankRecord()
record = {
modelId: model,
}
}
const save = debounce(async () => {
for (let field of fields) {
// Assign defaults to empty fields to prevent validation issues
if (!(field in record))
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
} }
async function save() {
// prevent double clicking firing multiple requests
if (saving) return
saving = true
const SAVE_RECORD_URL = `/api/${model}/records` const SAVE_RECORD_URL = `/api/${model}/records`
const response = await _bb.api.post(SAVE_RECORD_URL, record) const response = await _bb.api.post(SAVE_RECORD_URL, record)
@ -79,13 +72,11 @@
return state return state
}) })
errors = {}
// wipe form, if new record, otherwise update // wipe form, if new record, otherwise update
// model to get new _rev // model to get new _rev
if (isNew) { record = isNew ? { modelId: model } : json
resetForm()
} else {
record = json
}
// set saved, and unset after 1 second // set saved, and unset after 1 second
// i.e. make the success notifier appear, then disappear again after time // i.e. make the success notifier appear, then disappear again after time
@ -94,69 +85,27 @@
saved = false saved = false
}, 1000) }, 1000)
} }
saving = false
}
// we cannot use svelte bind on these inputs, as it does not allow if (response.status === 400) {
// bind, when the input type is dynamic errors = json.errors
const resetForm = () => {
for (let el of Object.values(inputElements)) {
el.value = ""
if (el.checked) {
el.checked = false
}
}
record = createBlankRecord()
} }
})
const setForm = rec => { onMount(async () => {
if (isNew || !rec) return
for (let fieldName in inputElements) {
if (typeof rec[fieldName] === "boolean") {
inputElements[fieldName].checked = rec[fieldName]
} else {
inputElements[fieldName].value = rec[fieldName]
}
}
}
const handleInput = field => event => {
let value
if (event.target.type === "checkbox") {
value = event.target.checked
record[field] = value
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
record[field] = value
return
}
value = event.target.value
record[field] = value
}
onMount(() => {
const routeParams = _bb.routeParams() const routeParams = _bb.routeParams()
recordId = recordId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0]) Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !recordId || recordId === "new" isNew = !recordId || recordId === "new"
if (isNew) { if (isNew) {
record = createBlankRecord() record = { modelId: model }
} else { return
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
_bb.api
.get(GET_RECORD_URL)
.then(response => response.json())
.then(rec => {
record = rec
setForm(rec)
})
} }
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
const response = await _bb.api.get(GET_RECORD_URL)
const json = await response.json()
record = json
}) })
</script> </script>
@ -164,23 +113,28 @@
{#if title} {#if title}
<h1>{title}</h1> <h1>{title}</h1>
{/if} {/if}
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<hr /> <hr />
<div class="form-content"> <div class="form-content">
{#each fields as field} {#each fields as field}
<div class="form-item"> <div class="form-item">
<Label small forAttr={'form-stacked-text'}>{field}</Label> <Label small forAttr={'form-stacked-text'}>{field}</Label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion} {#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)} bind:this={inputElements[field]}> <select bind:value={record[field]}>
{#each schema[field].constraints.inclusion as opt} {#each schema[field].constraints.inclusion as opt}
<option>{opt}</option> <option>{opt}</option>
{/each} {/each}
</select> </select>
{:else} {:else if schema[field].type === 'datetime'}
<input <DatePicker bind:value={record[field]} />
bind:this={inputElements[field]} {:else if schema[field].type === 'boolean'}
class="input" <input class="input" type="checkbox" bind:checked={record[field]} />
type={TYPE_MAP[schema[field].type]} {:else if schema[field].type === 'number'}
on:change={handleInput(field)} /> <input class="input" type="number" bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<input class="input" type="text" bind:value={record[field]} />
{/if} {/if}
</div> </div>
<hr /> <hr />
@ -293,4 +247,9 @@
background-position: right 17px top 1.5em, right 10px top 1.5em; background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px; background-size: 7px 7px, 7px 7px;
} }
.error {
color: red;
font-weight: 500;
}
</style> </style>