I have a file upload form built with vue.js. The input data is packed into JSON and sent to the backend. Normally, this works successfully, I can retrieve the POSTed JSON objects with GET.
Problems arise after implementing a picker input for the file type:
<select class="home-select dataform dataselect" v-model="file_type">
<option disabled value="">Select dataset type</option>
<option v-for="type in allowedFileTypes" :value="type" :key="type.id">{{ type }}</option>
</select>
The allowed file types are defined in a list in data() as either Indicator Matrix or Expression Matrix.
I also have a text input field for the name of the dataset.
The Problem
I get a 422 error when I leave fields empty.
I get a 422 error when I select "Expression Matrix" and fill everything else.
I DON'T get a 422 error when I select "Indicator Matrix" and fill everything else.
Why does this happen and how can I fix this? I also want to be able to leave fields empty.
The total vue component looks like this:
<template>
<div class="container">
<h1>Showcase Page</h1>
<select class="home-select dataform dataselect" v-model="file_type">
<option disabled value="">Select dataset type</option>
<option v-for="type in allowedFileTypes" :value="type" :key="type.id">{{ type }}</option>
</select>
<input type="text" placeholder="Enter dataset name" class="input dataform" v-model="name"/>
<button class="btn btn-default" type="button" #click="uploadFile">Upload</button>
</div>
</template>
<script>
export default {
name: 'test',
props: {},
components: {},
methods: {
async uploadFile() {
const response = await fetch(`${this.BASE_URL}/dataset`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: this.name,
type: this.file_type
})
})
console.log("response:")
console.log(response)
const blob = await response.blob();
console.log(blob)
}
},
data() {
return {
file: new File([""], "file"),
name: "",
file_type: "",
allowedFileTypes: ["Indicator Matrix", "Expression Matrix"],
BASE_URL: process.env.VUE_APP_SERVER_URL
}
}
}
</script>
I logged the values of the variables as well as their type with .type and typeof.
.type always gives undefined
typeof always gives string
What does it mean that .type is always undefined, even when the input of the variable is filled?
I found the root of the problem:
Thanks to Bravo I looked into the server. That the problem lies there is of course also indicated by the 422 error. I use grails and the constraints of the File domain class are probably the reason of the errors.
I made the fields nullable and "blankable", which solved the issue.
Related
Q1:
I am trying to create a form which is populated with initial values fetched from an API. Everytime the user edits any of the fields in the form, a POST request should be sent to the API and the initial values should be then updated. My current solution works, but as I'm using it in a for-loop in a django template, the readability of the resulting html file is suboptimal as the script is repeated very many times. I think extracting the fetch script to a function would make the template much more readable, but I don't know how to update the contents of the x-data component containing all the values user has given in the form in a function.
Current solution looks like this (styles etc unnecessary cleaned):
<form action="/submitcars" method="POST"
x-data="{ dynamic_cars: [] }"
x-init="dynamic_cars = await (await fetch('/dynamic_cars')).json()">
{% csrf_token %}
<div class="row-container">
{% for row in rows %}
{% for i in 0|range:3 %}
<div class="input_container car-{{i}}" x-show="open">
<div class="edited_icon" x-show="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited === 'true'">
<i class="material-icons refresh" #click="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited = 'false'">refresh</i>
</div>
<div class="edited_icon" x-show="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited === 'false'"></div>
<input required
step="any"
id=id_form_car_{{i}}-{{row.row_id}}
name=form_car_{{i}}-{{row.row_id}}
type="{{row.html_type}}"
tabindex="{{i}}"
x-model.lazy="dynamic_cars.cars[{{i}}].{{row.row_id}}.value"
#change= "dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited = 'true',
dynamic_cars = await (
await fetch('/dynamic_cars', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.head.querySelector('meta[name=csrf-token]').content
},
body: JSON.stringify(dynamic_cars)
})).json()"></input>
<div class="unit" x-text="dynamic_cars.cars[{{i}}].{{row.row_id}}.unit"></div>
</div>
{% endfor %}
{% endfor %}
</div>
<div class="submit-button">
<button type="submit" value="submit">Save</button>
</div>
</form>
...
EDIT: Added the django loops and variables to question. row.row_id has the property name which tells the information (make, model etc) I am showing on each row in a CSS grid. So all three cars have these properties. row.row_id's match the property names in dynamic_cars API response:
cars: [
{
{make: {value: "Toyota", unit: "", user_edited: "false"},
{model: {value: "Camry", unit: "", user_edited: "false"},
...
70+ more properties for each car
...
},
{...},
{...}
]
Instead of having the whole POST-fetch after the x-on:change, I would like to have something like:
x-on:change="postCars(dynamic_cars)"
<script>
async function postCars(dynamic_cars) {
response = await fetch('/dynamic_cars', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.head.querySelector('meta[name=csrf-token]').content
},
body: JSON.stringify(dynamic_cars)
})
.then(response => {
if(response.ok) return response.json();
})
}
</script>
By doing this, I can see that the POST request in the script has the data in dynamic_cars in its payload, and the response is correct, but the dynamic_cars object is set empty and the initial values in the form disappear if the input is edited. How should this be done correctly?
Q2:
Another question I have which is a little off-topic, but which probably is related to very basics of javascript is that the console in browser dev-tools shows error messages:
Alpine Expression Error: Cannot read properties of undefined (reading 'value')
Expression: "dynamic_cars.cars[1].make.value"
AND
Uncaught TypeError: Cannot read properties of undefined (reading '1')
Does this mean I have to define/initialize dynamic_cars in x-data exactly as the API returns? What if the API response is very complex and has lots of data or is even unknown? Writing the x-data="{}" open would mean hundreds of rows of javascript, which I wouldn't bother to write and maintain as the current solution works otherwise as intended, except for the console errors.
The reason you see those error messages in the console is that you try to bind form fields to non-existing objects. In the Alpine.js environment you only have an empty list: x-data="{ dynamic_cars: [] }". When Alpine.js initially try to bind input field to the respective variable, e.g. dynamic_cars.cars[1].make.value, the dynamic_cars does not even have a cars attribute, in fact, it is not even an object, but a list, so there's a type error as well.
After the faulty data-binding cycle, Alpine.js executes the code you provided in x-init that fetches the backend and finally updates the dynamic_cars variable, so now it has those attributes/fields you try to bind form fields previously. Surprisingly it's still working somehow, but IMHO it's rather undefined behavior and should be avoided.
But since you know the number of items/attributes in your form, it is trivial to create the correct JS data structure for them, so Alpine.js can bind them to the respective form fields. And after we fetch the backend we just have to update this object with the fetched data and Alpine.js updates the DOM automatically.
<form action="/submitcars" method="POST" x-data="carsForm" #change="postCars">
<div>
{% for row in rows %}
{% for i in 0|range:3 %}
<div class="input_container car-{{i}}" x-show="open">
<div class="edited_icon" x-show="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited">
<i class="material-icons refresh" #click="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited = false">refresh</i>
</div>
<div class="edited_icon" x-show="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited"></div>
<input required
step="any"
id=id_form_car_{{i}}-{{row.row_id}}
name=form_car_{{i}}-{{row.row_id}}
type="{{row.html_type}}"
tabindex="{{i}}"
x-model.lazy="dynamic_cars.cars[{{i}}].{{row.row_id}}.value" />
<div class="unit" x-text="dynamic_cars.cars[{{i}}].{{row.row_id}}.unit"></div>
</div>
{% endfor %}
{% endfor %}
</div>
<div class="submit-button">
<button type="submit" value="submit">Tallenna</button>
</div>
</form>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('carsForm', () => ({
dynamic_cars: {cars: {
{% for i in 0|range:3 %}
{{ i }}: {
{% for row in rows %}
{{ row.row_id }}: {value: '', user_edited: true, unit: ''},
{% endfor %}
},
{% endfor %}
}},
init() {
this.getCars()
},
getCars() {
// ... fetch backend as usual
// Sync new data with local this.dynamic_cars
this.syncData(response.json())
},
syncData(new_data) {
for (let i in new_data.cars) {
let car = new_data.cars[i]
for (let property_name in car) {
for (let prop_attr of ['value', 'unit', 'user_edited']) {
this.dynamic_cars.cars[i][property_name][prop_attr] = car[property_name][prop_attr]
}
}
}
},
postCars() {
// Post to the backend with payload: JSON.stringify(this.dynamic_cars)
// Sync response data with local this.dynamic_cars
this.syncData(response.json())
}
}))
})
</script>
Our Alpine.js component is called carsForm and we used the Alpine.data() in the alpine:init event that ensures Alpine.js is ready in our environment. You see that the HTML template contains only minimal Alpine.js-related attributes: an x-data and #change and the data-binding ones.
First, we create the empty dynamic_cars Alpine.js variable, that has as many rows/items/attributes/etc as needed, so Alpine.js can bind each of them to the respective form fields.
In the getCars() method executed from init(), we fetch the backend and call syncData() function with the response data as an argument. This function iterates over the deeply nested data structure and updates the corresponding value in our local dynamic_cars variable. After that Alpine.js updates the DOM.
After the user updates a form field the #change directive calls our postCars() method that post to the backend with payload dynamic_cars. When the backend responds, we call syncData() again to update the local dynamic_cars variable with the latest version of the data from the backend.
Note: the user_edited will be parsed, so you don't have to make string comparison, just use as a boolean variable.
I've been trying to test out a way in vue to build a select list with a hardcoded array of options, however, if a certain async response/event comes in with an assignee attached, I am setting that as 'currentAssignee' which is my preselected option.
This kind of works, but it initially looks empty/invisible. If I click the seemingly non-existent select box, the options will show 'Name One', 'Name Two' and 'John Doe' which is the name from the response. But it doesn't actually satisfy the 'selected' option because it is essentially invisible to the user on page load, until it's clicked
Should I be doing something different?
<select class="firstLastNames linkBox" v-model="currentAssignee" #change="changeAssignee()" >
<option :selected="true">{{currentAssigneeFirst}} {{currentAssigneeLast}}</option>
<option v-for="assignee in assigneeOptions" >{{assignee.email}}</option>
</select>
data () {
return {
currentAssignee: '',
assigneeOptions: [
{id: 0, email: "Name one"},
{id: 1, email: "Name two"}
],
},
}
/**further down, I set currentAssignee based on async event**/
this.currentAssignee = triggerEvent[0].assignee;
I put a code sample together here which I think fixes your issue:
https://codepen.io/timfranklin/pen/bGWYggG
Take a look at what is being bound by the v-model. The "value" of a select is not the object itself, it's some value of an object.
<select class="firstLastNames linkBox" v-model="currentAssignee" #change="changeAssignee($event)" >
<option disabled >Choose One</option>
<option v-for="assignee in assigneeOptions" :key="assignee.id" :value="assignee.id">{{assignee.email}}</option>
</select>
The important note here is the :value="assignee.id";
I am using sails.js 1.0 with vue.js and want to create a dynamic form that contains a dynamic amount of inputs based on the user's preference. So the user should be able to add another input, type in the data and send the complete form with the dynamic amount of data.
My form looks like this:
<ajax-form action="addStuff" :syncing.sync="syncing" :cloud-error.sync="cloudError" #submitted="submittedForm()" :handle-parsing="handleParsingForm">
...
<input class="form-control" id="input1" name="input1" type="text" :class="[formErrors.password ? 'is-invalid' : '']"
v-model.trim="formData.input1" placeholder="Input #1" autofocus>
...
<ajax-button type="submit" :syncing="syncing" class="btn btn-dark">Save changes</ajax-button>
</ajax-form>
The action addStuff in sails looks like this:
module.exports = {
friendlyName: 'Do some stuff',
description: 'Do some stuff with the form data.',
inputs: {
input1: {
description: 'The first input.',
required: true
}
},
fn: async function (inputs, exits) {
// Do some stuff with the inputs
return exits.success();
}
};
I know that normally I would be able to create a dynamic form using vue.js by
setting the data of the Vue instance to an array
creating a two-way-binding
implementing a v-for loop in the form, that then creates an input for every element in the data object
modifying this array by inserting a new element in the array every time the user wants to add another input.
But with sails and this ajax-form, I do not know how to access the vue instance and the data element of it and how to make this also dynamic in the action. Obviously the input would need to contain an array.
How would it be possible to achieve such a dynamic form?
I figured out the missing part. Sails.js is using parasails which is built on top of vue.js.
When generating a new sails page using the sails generator sails new test-project, there is also a contact form generated which also contains the necessary code which can be adapted for this purpose.
That contact form basically consists of
The .ejs page (=the html code that renders the form) in views/pages
The contact.page.js client-side script in assets/js/pages
The server side controller deliver-contact-form-message.js in api/controllers
In the client-side script, the initial formData can be set:
parasails.registerPage('maindivid', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: {
// Main syncing/loading state for this page.
syncing: false,
// Form data
formData: { /* … */ },
// For tracking client-side validation errors in our form.
// > Has property set to `true` for each invalid property in `formData`.
formErrors: { /* … */ },
// Server error state for the form
cloudError: '',
// Success state when form has been submitted
cloudSuccess: false,
},
...
as well as methods etc.
It follows a similar structure than plain vue.js.
To achieve what I was trying to do I added a field as array to the formData
formData: {
myinputs: [
{
key: '',
value: ''
}
]
},
Then I bound that in the .ejs file:
<div class="form-row" v-for="(filter, index) in formData.mypinputs">
<input class="form-control form-control-sm" type="text" :class="[formErrors.password ? 'is-invalid' : '']"
v-model.trim="formData.myinputs[index].key" placeholder="My field">
<button type="button" class="btn btn-secondary btn-sm" #click="addFilterForm">add field</button>
</div>
And finally added a method to the client-side script in contact.page.js (or your name) that gets called when the user clicks the "add field" button.
methods: {
addFilterForm: function() {
this.formData.myinputs.push({
key: '',
value: ''
});
},
Because of the two way binding, as soon as an element is added to the array formData.myinputs, another input is created and added to the DOM.
I'm new to Vue and trying to build a form that calls an API when an input is changed to dynamically build and set other inputs. It's purpose is to select a vehicle.
When you select a make, it populates the model and year select fields with appropriate values from the API via WebSocket. These fields may be filled automatically from the VIN (vehicle identification number) and any field in the process may change other fields.
This is causing a feedback loop where an input is changed by another input which in turn, calls the API which sends back another set of data and changes something else.
How can I ensure that data is only set once after a human changes a field?
My Vehicle selection code:
<template>
<div id="vehicleSelection">
<h1 class="title">{{ title }}</h1>
<b-field label="Make">
<b-select placeholder="Select a make" v-model="make" #input="setMake(make)" id="makeSelect">
<option
v-for="make in make_list"
:value="make"
:key="make">
{{ make }}
</option>
</b-select>
</b-field>
<b-field label="Model">
<b-select placeholder="Select a model" v-model="model" #input="setModel(model)" id="modelSelect">
<option
v-for="model in model_list"
:value="model"
:key="model">
{{ model }}
</option>
</b-select>
</b-field>
<b-field label="Year">
<b-select placeholder="Select a model year" v-model="year" #input="setYear(year)" id="yearSelect">
<option
v-for="year in year_list"
:value="year"
:key="year">
{{ year }}
</option>
</b-select>
</b-field>
</div>
</template>
<script>
import CommandClient from '../communication/command/CommandClient';
import MessageHandler from '../communication/handler/MessageHandler';
export default {
name: 'VehicleSelection',
methods: {
setMake(make) {
CommandClient.send(this.$socket, CommandClient.prepare('set_vehicle_make', ['make', make]));
},
setModel(model) {
CommandClient.send(this.$socket, CommandClient.prepare('set_vehicle_model', ['model', model]));
},
setYear(year) {
CommandClient.send(this.$socket, CommandClient.prepare('set_vehicle_year', ['year', year]));
},
},
created() {
this.$options.sockets.onmessage = (message) => {
MessageHandler.handle(this, message);
};
},
data() {
return {
title: 'Vehicle Selection',
make_list: [],
make: null,
model_list: [],
model: null,
year_list: [],
year: null,
};
},
};
</script>
There is some repetition here which will probably be refactored out but I'd like to just get it working first.
For context, the MessageHandler here takes a Vue component and sets data for it if the key is present in the API response:
const _ = require('lodash');
export default {
/**
* #param {Object} context
* #param {MessageEvent} message
* #return {void}
*/
handle(context, message) {
const messagePayload = JSON.parse(message.data);
Object.entries(messagePayload).forEach((data) => {
if (_.has(context.$data, data[0])) {
context.$set(context, data[0], data[1]);
}
});
},
};
I'm making some assumptions about your code since I don't know the specifics of it...
I would imagine you're getting a feedback loop if you're calling the API in response to any change made to a specific data property via watch; e.g. if you had code like this:
data() {
return {
year: 2010,
};
},
watch: {
year() {
// this.year changed either by user input or by any other means.
// Calling API here...
this.api();
},
},
You're combining v-model and #input in your template; this will work although it's a bit confusing (v-model is just syntax sugar for :value and #input; will two #inputs work? Yes, it turns out, but I digress).
If you only want to call the API in response to user input, then call the API in the #input handlers of the UI elements that you want to trigger the calls:
<b-select :value="make" #input="setMake">
<!-- For a native DOM element, do this instead -->
<select :value="make" #input="setMake($event.target.value)">
methods: {
// This method is *only* called by #input
setMake(make) {
// Set this.make in response to user input
this.make = make;
// Call API here only; this.make can be changed by other means
// but it won't trigger an API call in those cases
this.api();
},
},
MEDService.users("GET", "", {"action" : "getUsers"})
.success(function(data, status, headers, config) {
$scope.data1 = data;
have this code, how i can set this item(get from mongo) in select on front end?
data1 have fields: username, name, password. I need just username
<select ng-model="??" style="display: block" >
<option ng-selected="??"
ng-repeat="??"
value="???">
???
</option>
</select>
Assuming that your data1 looks like
data1 = [
{name:'Real Name1', username:'username1', password: 'secret'},
{name:'Real Name2', username:'username2', password: 'secret'},
{name:'Real Name3', username:'username3', password: 'secret'}
]
try
<select ng-model="selectedUser" ng-options="user.name for user in data1"></select>
The selected user will be stored in selectedUser, you can easily validate that by using
<h2>{{selectedUser}}</h2>
in your html.
Working example: http://plnkr.co/edit/pggWiNO0TpIlCLOlFtzW
You must create another property on your scope, like selectedValue and then use this markup :
<select ng-model="$scope.selectedValue">
<option ng-repeat="option in $scope.data1" value="{{option.id}}">{{option.name}}</option>
</select>
As I understand you have something like that:
$scope.data1 = [
{username: 'olga', name: 'Olga K', password: 'querty'},
....
];
As the result you are going to receive username in this case it's olga value.
I see two solution you can do
First solution for string array you may prepare your date to view.
$scope.data1 = $scope.data1.map(item => item.username);
Then your html may look like that
<select ng-model="SELECTED_PROPERTY" ng-options="o as o for o in data1"></select>
Second soultion
when you have object array
<select ng-model="SELECTED_PROPERTY" ng-options="o.username as o.name for o in data1"></select>
NOTE: if you never use other object properties first solution is better one cause use few RAM memory.