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.
Related
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.
I’m trying to display the store URLs from an API.
In the API are available different URLs and in the output, they are displayed as one single link. How can I display URLs in different lines?
My vue code:
<a v-bind:href=“storeUrl”>
{{storeUrl}}
</a>
My script:
....
computed:{
storeUrl() {
return this.games.store.map(({{url}}) => url).join(‘ ’)
},
}
I’m using https://api.rawg.io/api/games API
This is the current output:
This answer solves some problems after #chillin's answer.
As I mentioned in the comments, the reason you're not seeing any store urls is that you are iterating over an object that doesn't exist:
The problem, (as #chillin saw) is that you are iterating through
game.store when you should be iterating through game.stores If you
inspect the game object, you'll notice that there is a stores
array within, but not a store one.
You should also note that having the urls in anchors on their own will cause them to be squashed into one line. Wrapping the anchors in <p> elements solves that problem:
Before:
<a v-for="store in game.store" :href="store.url" :key="store.ID">{{store.url}}</a>
After:
<p v-for="store in game.stores" :key="store.ID">
<a :href="store.url">{{store.url}}</a>
</p>
Also, I'm not sure if the stores array could ever have duplicate IDs, (maybe if multiple versions of the same game are in the same store), but if that ever does happen your code could crash. So it might be a better option to simply use the index of the object as it's key, like so,
<p v-for="(store, index) in game.stores" :key="index">
<a :href="store.url">{{store.url}}</a>
</p>
...so as to avoid this potential problem.
Here's a demo and here's the modified codepen (I also removed the computed property storeUrl, as it was unused.
Updated with actual example
First, don't join them in the computed, and then implement using a v-for, something like this should work.
Basically this is my own take of course, but based on the actual API data, something like this should work, with a loop in a loop, I map out the data just for ease of use, you will end up with something like:
[
{
key: 'gta-v',
storeUrls: [
{
key: 'steam',
url: 'http:// ...'
},
{
key: 'riot',
url: 'http:// ...'
}
]
},
{
key: 'fortnite',
storeUrls: [
{
key: 'steam',
url: 'http:// ...'
},
{
key: 'riot',
url: 'http:// ...'
}
]
}
]
Using this we can also double down on a v-for in the template, and sort your data by game, and for each game loop through it's storeUrl's for a nice clean list, this also utilises the use of actual keys, rather than index.
<template>
<div class="root">
<div class="game" v-for="game in games" :key="game.key">
<h1>{{ game.key }}</h1>
<a v-for="store in game.storeUrls" :href=“store.url” :key="store.key">
{{store.url}}
</a>
</div>
</div>
</template>
export default {
data() {
return {
myData: null
}
},
computed: {
games() {
if (!this.myData) return [];
return this.myData.results.map(game => {
key: game.slug,
storeUrls: game.stores.map(store => {
return {
key: store.store.slug,
url: store.url_en
}
});
});
}
},
methods: {
getData() {
// However you do it, but the base data, with no mapping.
this.myData = axios.get('...');
}
}
}
I'm new to Vue, so maybe someone else can provide a better answer. But seems right to use v-for to loop over your store URLs.
Assuming your gameId watcher is running and completing successfully, I don't think you need to change anything since this.game.stores should already contain an array of objects.
So you should be able to do something like:
<a v-for="store in game.stores" :href="store.url" :key="store.store.id">{{ store.url }}</a>
I don't know the difference between store.id and store.store.id, but I've assumed that store.store.id uniquely identifies a store and is okay to be used as the key. (You'd have to check the API documentation to see what the IDs represent.)
I have to say, I am new to the whole Vue framework. I have created a selectable table. The data selected from this table is stored in an object. This function should run in the back. So, I think I should run it the computed section. My object looks like this. I am trying to retrieve only the ids. This data is stored in the variable selected.
[ { "id": 5, "name": "CD", "box": "A5", "spot": 1 }, { "id": 2, ""name": "DVD", "box": "A2", "spot": 1 } ]
I would like to only retrieve the values from the key id. These values should be stored in an array. The array should be pushed when the submit button is clicked. I will later use this array to store this information in this array into a database.
You can find my code below. I guess I am missing something because it doesn't seem to work. How should I refer to this function that it runs automatically and that the array can be called in my controller to put it into the database?
Thanks for the help.
Template
<div class="row">
<div class="col-2">
<b-form-input v-model="date" class="form-control" type="date" value="getDate" ></b-form-input>
<pre class="mt-3 mb-0">{{ date }}</pre>
</div>
<div class="col-6">
<b-form-input v-model="description" placeholder="Enter some text"></b-form-input>
<pre class="mt-3 mb-0">{{ description }}</pre>
</div>
<!-- Submit data to db -->
<div class="col-4">
<button class="btn btn-primary custom-button-width" center-block>Request antibody pool</button>
</div>
</div>
JavaScript
data() {
return {
// data from db
data: this.testerdata,
// selected is the object where my selected data is stored.
selected: [],
// here should my id end up
selectedids: {},
description: '',
date: '',
}
},
computed: {
tester() {
var array = [];
for (var test in this.selected) {
if (test == "id") {
array += this.selected[test];
}
}
console.log(array);
}
},
methods: {
storeData: async function() {
axios.post('/panel', {
description: this.description,
date: this.date,
selectedids: this.tester(selectedids)
}).then(function(response) {
console.log(response);
}).catch(function(error) {
console.log(error);
}).finally(function() {
// always executed
});
},
}
The problem is with this line:
array += this.selected[test];
This the way to add an item to an array:
array.push(this.selected[test]);
Additionally, you are trying to loop over properties of this.selected, but it is an array which contains objects and has no properties. Instead, you want to loop over each object item in the array and push the id property.
And, since this.selected is an array, not an object, you should not use a for...in loop-- which is for objects-- because the order will not be guaranteed. You can read more about that in the MDN for...in docs.
You could use a normal for loop or the array's forEach loop. I'll show the forEach:
this.selected.forEach(obj => {
array.push(obj.id);
});
I understand that you might be looking for vanilla Javascript answer here.
However a lot of code can be saved if you can use RamdaJS library to solve such issues.
It has an excellent set of functions like pluck, which will do the same thing in 1 line.
let array = R.pluck('id')(this.selected);
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 using AJAX to send a user-input form to Flask, where it is processed, used in a query, and the results are sent back as a JSON object. AJAX is called upon form submission, and I'm returning the proper query results. But, when the results are returned, the JSON object is printing to the browser window, not maintaining the template format of the original page.
views.py
#app.route('/table_data', methods=['POST'])
def table_data():
request_info = request.form
#Incoming data is processed here and converted into following format:
data = [{'name': 'bill', 'state': 'CA', 'id': 012345},
{'name': 'cindy', 'state': 'NY', 'id': 098765}, ...]
return jsonify(data)
index.html:
<script type="text/javascript" src="{{ url_for('static'), filename='/js/script.js'}}"></script>
<form action="{{ url_for('table_data') }}" class="form-inline" method="post"
role="form" enctype="application/json">
<input type="text" class="form-control" id="name" name="name">
<input type="text" class="form-control" id="state" name="state">
<input type="text" class="form-control" id="id" name="id">
<button type='submit' class="btn btn-default">Submit</button>
</form>
<p id="response"></p>
script.js:
$(document).ready(function() {
$('form').on('submit', function(event) {
$.ajax({
url: '/table_data',
data: $('form').serializeArray(),
type: 'POST',
success: function(response) {
$('#response').text(response);
}
})
});
});
I expect the returned JSON object to be printed to the screen underneath the form, but instead, the data returned is printed directly to the browser window, which doesn't maintain the structure of 'index.html' (which has navbars and a form). I am returned this:
[
{
'name': 'bill',
'state': 'CA',
'id': 012345
},
{
'name': 'cindy',
'state': 'NY',
'id': 098765
},
...
]
Again, this is the proper format, but the data isn't printing to the data like I want it to. Brand new to JavaScript and jQuery/AJAX, so I'm fairly sure I'm missing something pretty trivial.
SOLUTION:
Turns out I had quite a few issues going on. First, in a line I didn't include in the OP, my load of jQuery was bad. Not sure why, but I opened my console and saw the JS error (using Chrome, right click >> inspect >> "Console" tab at top right of page). I changed this from what I had (src="{{ url_for('static', filename="/js/jquery-3.2.1.min.js) }}" ) and instead just loaded it as a google hosted library:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
My response was then printed in the paragraph id="repsonse", but as:
[object, Object], [object, Object], ..., [object, Object]
To actually print the contents of the JSON object, I changed the line
$('#response').text(response);
to:
$('#response').text(JSON.stringify(response));
That printed my JSON objects with their contents to a paragraph on the page.