Can I pass metadata through Vue Formulate's schema API without affecting input attributes? - vue-formulate

The goal:
generate form fields from JSON/CMS
have a param in the JSON that allows two fields to sit next to each other on a single line
The solution so far:
I’m using Vue Formulate's schema API to generate fields. In Vue Formulate's options, I can conditionally add a class to the outer container based on a parameter in the context.
classes: {
outer(context, classes) {
if (context.attrs.colspan === 1) {
return classes.concat('col-span-1')
}
return classes.concat('col-span-2')
},
I’m using Tailwind, which requires no classname concatenation and actually want the default to be col-span-2, so if you’re inclined to copy this, your logic may vary.
With a few classes applied to the FormulateForm, this works really well. No additional wrapper rows required thanks to CSS grid:
<FormulateForm
v-model="values"
class="sm:grid sm:grid-cols-2 sm:gap-2"
:schema="schema"
/>
The schema now looks something like this:
[
{
type: 'text',
name: 'first_name',
label: 'First name',
validation: 'required',
required: true,
colspan: 1,
},
The problem/question
Vue Formulate’s schema API passes all attributes defined (other than some reserved names) down to the input element. In my case, that results in:
<div
data-classification="text"
data-type="text"
class="formulate-input col-span-1"
data-has-errors="true"
>
<div class="formulate-input-wrapper">
<label
for="formulate-global-1"
class="formulate-input-label formulate-input-label--before"
>
First name
</label>
<div
data-type="text"
class="formulate-input-element formulate-input-element--text"
>
<input
type="text"
required="required"
colspan="1" <--------------- hmm…
id="formulate-global-1"
name="first_name"
>
</div>
</div>
</div>
I recognize that I can name my attribute data-colspan so that I’m not placing a td attribute on an input, but I think of colspan as metadata that I don’t want applied to the template. Is there a way to prevent this from being applied to the input—perhaps a reserved word in the schema API that allows an object of metadata to be accessed via context without getting applied to v-bind="$attrs"?

The vue-formulate team helped me out on this one. Very grateful. Much love.
There is a way to prevent it from landing on the input, and that's to use the reserved outer-class property in the schema:
[
{
type: 'text',
name: 'first_name',
label: 'First name',
validation: 'required',
required: true,
'outer-class': ['col-span-1'],
},
This means that I don't need to do this at all:
classes: {
outer(context, classes) {
if (context.attrs.colspan === 1) {
return classes.concat('col-span-1')
}
return classes.concat('col-span-2')
},
vue-formulate supports replacing or concatenating classes via props. I managed to overlook it because I didn't recognize that everything you pass into the schema API is ultimately the same as applying a prop of that name.
Classes can be applied to several other parts of the component as well—not just the outer/container. More information here:
https://vueformulate.com/guide/theming/customizing-classes/#changing-classes-with-props

Related

Convert 3rd party REST API into html forms?

I want to convert an API service into a forms the user of my app can fill in. I think I need the right vocabulary to ask farther questions. Is there a term for this process?
Hydra is the only vocab I know about in the topic, though they don't support this kind of conversion. https://www.hydra-cg.com/spec/latest/core/ HAL forms is another solution: https://rwcbook.github.io/hal-forms/ but I guess based on the name that it is not abstract enough. You need to separate hyperlinks from forms. The former is for describing the interface of the webservice the later is for describing the GUI, which can use the webservice amongst many different types of clients. E.g. a select/choice parameter can be converted into radio button, checkbox, select input, range, etc. There needs to be some sort of mapping about this and this decision is made by the client developers, not by the service developers. Supporting multiple languages and labelling must be mapped too.
What you need is very detailed hyperlink descriptions and you can turn your hyperlinks into forms and hyperlink parameters into input fields. There are certain types of parameters e.g.
text (password?, multiline?, lengthRange, verified:regex|url|email|etc.)
repetition (source e.g. password double check)
select(alternatives[], selected[], selectionSize)
date(range, selectedRange, selectionSize)
time(range, selectedRange, selectionSize)
color(alternatives|range, selectedRange, selectionSize)
file(multi?, sizeRange, MIME-types[])
...
Better to support only what you need, because I think you need to write it yourself. Try to be very general, abstract instead of specific in this vocab. E.g. select can support select single and select multiple. The single can be supported with selectionSize=1. The alternatives can be added with a list or with another hyperlink which can be part of the response or lazy loaded, or it can be even an URI template which expects a keyword. The data of the fields can depend on each other, e.g. first you select city and the next field is street selection where you download the data by filling an URI template. So these fields can be interrelated and doing it properly requires a complex vocab.
From the REST API side of story binding these to the hyperlinks is relative easy:
{
id: "/api/users/123"
type: "/docs/User"
userid: 123,
addCar: {
type: "/docs/User/addCar",
method: "PUT",
uri: {
template: "/api/users/{id}/cars/{plateNumber}",
id: {
type: "docs/Text",
range: "docs/Car/id",
value: {
type: "/docs/Query",
context: "./resource",
select: "userid",
},
readOnly: true,
required: true
},
plateNumber: {
type: "/docs/Text",
range: {
id: "/docs/Car/plateNumber",
verified: {
type: "/docs/verification/regex",
pattern: "[A-Z]{4,4}\d{2,2}"
}
},
required: true
}
},
body: {
brand: {
type: "/docs/Selection",
range: "/docs/Car/Brand/id",
alternatives: {
type: "/docs/Car/Brand/listBrands",
method: "GET",
uri: "/api/car-brands"
},
value: null,
selectionSize: [1,1],
required: true
}
}
}
}
The generated HTML form can be something like the following with radio input, but you can use select input too:
<form onsubmit="magic(this); return false">
<input type="hidden" id="user_id" name="user_id" value="123">
<input type="text" id="plate_number" name="plate_number" pattern="[A-Z]{4,4}\d{2,2}" required="required"><br>
<label for="plate_number">Plate Number</label><br>
Select Brand:
<input type="radio" id="brand_vw" name="brand" value="VW" required="required">
<label for="brand_vw">Volkswagen</label><br>
<input type="radio" id="brand_ford" name="brand" value="Ford">
<label for="brand_ford">Ford</label><br>
<input type="submit" value="Add Car">
</form>
The request is something like:
PUT /api/users/123/cars/ABCD12 {brand: "Ford"}
When you do it with an automated client you do:
carService.getUser({id: 123}).addCar({plateNumber: "ABCD12", brand: "Ford"})
When you do it with GUI, then:
carService = new Service("/docs")
// GET /docs/* might be cached
// or you can download a single JSON-LD docs file and use # for terms
// you bookmarked the getUser link from a previous call
// or you get it with GET /api/
// you can fill it with the actual user
user = carService.getUser({id: jwt.userId})
// GET /api/users/123
// you find the addCar link and generate a form from it
// GET /api/car-brands
// the user fills the form and sends it with magic
user.addCar({plateNumber: "ABCD12", brand: "Ford"})
// PUT /api/users/123/cars/ABCD12 {brand: "Ford"}
As you can see this can be very complicated and the upper hyperlink description is ad-hoc. If you need a proper RDF vocab, then it takes serveral years to design it properly. Still this kind of technique could be used in theory.
A complete client cannot be generated, because that involves knowing what you are doing or what you possibly need and in which part of the client. E.g. in this case it is where to display the form, when do we need this form and why, where to get the actual user id and the hyperlink from, etc. If the user has to decide everything about this, then in theory it can be generated and would look like a simple webpage, which can be browsed.

Vue/Vuelidate: Dynamically show required star (*) based on requiredIf

I'm creating a dynamic re-usable vue component that wraps a text input and includes the vuelidate validation stylings, etc.:
<template>
<div class="form-group" :class="{'form-group--error': validator.$error}">
<label>{{ label }}<span v-if="validator.$params.required">*</span></label>
<input type="text v-model="validator.$model" />
<div class="error" v-if="validator.$error && !validator.required">* This field is required</div>
</div>
<template>
<script>
export default {
name: "FormTextField",
props: ["validator", "label"]
}
</script>
(validator prop is $v from parent)
The problem I have is trying to show the <span>*</span> dynamically based on if the field is required. This currently works (v-if="validator.$params.required") as long as I only specify the required validator:
fieldName: {
required: required
}
Now, I need to instead declare my validation like this:
fieldName: {
required: requiredIf( ... )
}
The question is how to access the result of the requiredIf function? validator.$params.required will always be true since it's just checking if the param is there. And validator.required is the status of the validation, not the result of the requiredIf call to see whether it SHOULD be required or not.
Any suggestions on how I can show the required star dynamically based on vuelidate state?
Instead of validator.$params.required, check validator.required, which is only defined when there's a required or requiredIf validator rule applied. The value of validator.required is true when the field is missing, or false otherwise.
When the field is optional (has no required/requiredIf rule), the validator.required property does not exist, so we can't use v-if="!validator.required". Instead, explicitly compare validator.require to false:
<label>{{ label }}<span v-if="validator.required === false">*</span></label>
<div class="error" v-if="validator.$error && validator.required === false">* This field is required</div>
demo

Vue - set v-model dynamically (with a variable containing a string)

I haven't been able to set v-model dynamically.
It works if I type explicitly:
<div class="form-group mr-3 mb-2">
<input type="text"
v-model="form[filters][firstlastname]"
>
</div>
But I want to loop through an object wherein I have string , like: 'form[filters][firstlastname]'
The parent has the form with properties:
data() {
return {
form: new Form({
filters: {
gender: [],
firstlastname: 'My firstlastname'
So, from the parent I pass down the form and filters into the child component, here is filters:
let formFilters = { filters: [
{
type: 'text',
property: 'form[filters][firstlastname]', // <-- string
placeholder: 'Name',
},
{
type: 'number',
property: 'paginate',
placeholder: 'Max rows'
},
]
}
Child component: (here I loop through the object and generate the input fields)
<div v-for="(filter,index) in formFilters.filters"
:key="`${index}_${filter.property}`"
>
<input
v-if="filter.type === 'text' || filter.type === 'number'"
:placeholder="filter.placeholder"
:type="filter.type"
v-model="filter.property" //<--- set the property
>
This doesn't work. The v-model just interprets it as a string and not a reference to a form property.
I tested other ways, like: v-model="``${[filter.property]}``" (single, not double ```` but it wont show in stackoverflow otherwise) and other crazy things but it isn't valid.
So how do I set v-model with a variable containing a string (so that it can be set dynamically)?
This is a very tricky problem....
You can access any property present in the data inside html template using 2 ways,
Referring to the property directly
Using $data
data() {
return {
firstlastname: 'Mr First last name'
}
}
so, in html template you can use either
<p>{{firstlastname}}</p>
or
<p>{{$data.firstlastname}}</p>
For your scenario $data can be used for primitive data types like string or number,
<input
v-if="filter.type === 'text' || filter.type === 'number'"
:placeholder="filter.placeholder"
:type="filter.type"
v-model="$data[filter.property]">
But this will not work for your second scenario where you are trying to access nested property of an object form.filters.firstlastname
You can access this property using the following notation $data[form][filters][firstlastname]
In your case, the for loop will result as $data[form.filters.firstlastname] or $data[[form][filters][firstlastname]] which will throw an exception
As suggested in the comments, try different approach or flatten the object. You can refer to this link to see how to flatten the object https://stackoverflow.com/a/25370536/2079271

Vue.js: binding select boxes, but don't want to ajax all the options

Good day. I'm using Vue.js to render an arbitrary number of select elements from the data in a component.
Here's sample JSON data that indicates there are two select elements, each with one or more options.
{
"dropdowns":[
{
"cd":"UG9ydGZvbGlv",
"formname":"sp_filter_UG9ydGZvbGlv",
"nm":"Portfolio",
"selected":"1a",
"options":[
{
"cd":"1a",
"val":"Option 1A"
}
]
},
{
"cd":"UHJvZHVjdCBOYW1l",
"formname":"sp_filter_UHJvZHVjdCBOYW1l",
"nm":"Product Name",
"selected":"2b",
"options":[
{
"cd":"2a",
"val":"Option 2A"
},
{
"cd":"2b",
"val":"Option 2B"
}
]
}
]
}
Here's the template HTML:
<form>
<div v-for="dropdown in dropdowns">
<div v-if="dropdown.availableToView">
<h4>{{dropdown.nm}}</h4>
<select v-model="dropdown.selected" v-on:change="triggerUpdate">
<option value="">(Make a selection)</option>
<option v-for="option in dropdown.options" :value="option.cd">{{option.val}}</option>
</select>
</div>
</div>
</form>
So far so good.
I've got the data loading and Vue is building the dropdowns.
When the user changes any select box (remember there can be an arbitrary number of them), the trigger action needs to submit ALL of the elements in the form via ajax. It sounds like the most correct option is to bind the form fields to the underlying component data, as I've done.
My triggerUpdate looks like this:
methods: {
triggerUpdate: function() {
axios({
method: "post",
url: actionURL,
data: this.dropdowns
})
.then(response => (this.data = response));
}
}
...but this submits the entire dropdowns data element, including all of the options in each select box. It's unnecessary to send all of the options in. I just want to send each field name along with its selected option (i.e. the "value").
I know i could serialize the whole form and make that my ajax payload. But that seems to be making an "end run" around Vue.js. Everyone talks about having your form fields bound to the Vue model...is it then correct to basically ignore the model when making an ajax request whose purpose is to then update the model?
I'm relatively new to Vue.js so I'd appreciate help with what I'm overlooking here. How should I go about sending in the data from the form (a) while using proper Vue.js binding and (b) without sending extraneous data?
Thanks for your time.
If you need to post only the selected values, and you store those in each dropdown's selected property, the sensible approach seems to be just mapping it to a simple array of name/value objects.
Try this (it assumes the name of each field is the formname property, if it isn't you can just replace it):
var submitData = this.dropdowns.map((dropdown) => {
return { name: dropdown.formname, value: dropdown.selected };
});
Then you send submitData in your ajax request.

How to specify multiple dynamic attributes by single computed prop in VueJS

I have this html element:
Link text
I want to add data-tooltip and title attributes dynamically by condition:
Link text
Is there any way in VueJS to add multiple dynamic attributes at same time:
<!-- instead of this: -->
Link text
<!-- something like this: -->
<a href="javascript:" ...tooltipAttributes >Link text</a>
You could take advantage of v-bind on the DOM element you wish to apply multiple attributes to based on some dynamically changing condition.
Here's a Plunker example demonstrating how you might go about it.
Take note of the object returned:
computed: {
multiAttrs() {
return this.showAttrs ? {
'data-toggle': 'tooltip',
title: 'Some tooltip text',
} : null;
}
}
You should be able to use v-bind="tooltipAttributes"
the docs here https://v2.vuejs.org/v2/api/#v-bind have more info, but the key part is under usage
Dynamically bind one or more attributes, or a component prop to an expression.
From the Docs:
1. You can dynamically bind multiple attributes/props to a single element by using v-bind:
(no colon, no extra attribute, just v-bind)
<a href="#" v-bind="tooltipAttributes" >Link text</a>
2. And then declare the variable in the computed section:
(you can also declare it in the data section, but that would require manual direct value changes)
computed() {
return {
tooltipAttributes: {
title: 'Title',
'data-toggle': this.toggle === true && !disabled
}
}
}
Note: Attributes with dashes/hyphens - in them (e.g. data-toggle) need to be a string because Javascript doesn't recognize - as a valid symbol in variable naming.
This is THE SAME AS:
<a href="#" title="Title" :data-toggle="this.toggle === true && !disabled" >Link text</a>