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.
Related
I created a wizard form which uses Vuelidate to validate it's fields. The big majority of the fields have only the "required" function, so my validations are something close to this:
validations() {
if (this.currentStep === 0) {
return {
person: {
name: {
required,
},
age: {
required,
},
}
}
} else if (this.currentStep === 1) {
return {
person: {
street: {
required,
},
city: {
required,
},
state: {
required,
},
}
}
The thing is, I am receiving this data from an API, so the user can either fill the fields himself, or let the machine do it for him. When I receive the data, I make this attribution in a function in JS close to the following:
attributeData():
this.person.name = apiData.name;
this.person.age = apiData.age;
this.person.street = apiData.street;
this.person.city = apiData.city;
this.person.state = apiData.state;
If the user types the info, then everything works fine. If I receive the info from the API, then I get the error as if the input was empty.
This is how every field in my HTML is organized:
<b-form-group label="Name">
<b-form-input
v-model="person.name"
type="text"
size="sm"
:class="{
'is-invalid':
submitted && $v.person.name.$error,
}"
></b-form-input>
<div
v-if="submitted && $v.person.name.$error"
class="invalid-feedback"
>
<span v-if="!$v.person.name.required"
>Name is required.</span
>
</div>
</b-form-group>
Any idea of why Vuelidate can't recognize the values when I attribute them directly in JS? I've made this exact same way in another page and it worked. In DevTools even the $model of Vuelidate has the value that came from the API.
This error may occur if you have two elements using the same identifier.
Example:
data: {user_id: null} and setted v-model="user_id" in one input.
And another element input with: id:user_id
Beware if you are not manipulating the value and then it lost the reference, example:
data: {user: {name: null}}
And you filled it by API but latter in created or mounted put something like:
this.user = {}
The reference user.name was gone and validation can't work anymore.
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 integrating nuxt-i18n into one of my projects to enable multiple languages. It all works fine, but when I switch language while filling in a form, all data from the page is lost.
So, I created a form and use v-model to bind the data to the page's data object.
<div class="form-group">
<label for="username">{{ $t('form.username') }}</label>
<input v-model="user.username" type="username" class="form-control" id="username" />
</div>
This is the page's data object
data: () => {
return {
user: {
username: ''
}
}
So when I type a username in the field, the model is updated, as I expect. But as soon as I switch the language from that page, the data is lost as soon as the chosen language has been set.
This is how I switch the language when clicking, for example, the dutch flag icon.
switchLocalePath(locale.code) // local.code is 'nl' in this case
When the user switches the language, the slug should also update. The code below shows the settings for the i18n package in my nuxt.config.js file.
modules: [
['nuxt-i18n', {
locales: [
{
code: 'en',
iso: 'en-US',
name: 'English'
},
{
code: 'nl',
iso: 'nl-NL',
name: 'Nederlands'
}
],
defaultLocale: 'nl',
parsePages: false,
pages: {
'account/register': {
en: '/account/register',
nl: '/account/registreren'
},
'account/index': {
en: '/account/login',
nl: '/account/inloggen'
}
},
vueI18n: {
fallbackLocale: 'nl',
messages: { nl, en }
}
}],
]
The actual question
So almost everything works just as I expect. But every time I change the language, the page's data object is cleared (It doesn't seem like the page actually reloads). So when filling in a form, then changing the language before submitting, all data is lost.
How can I make sure, if possible, that all data presists when toggling the language?
Thanks in advance!
I use this method to change the locale and the same behavior occurs.
<v-list>
<v-list-item
v-for="locale in availableLocales"
:key="locale.code"
#click="$router.push(switchLocalePath(locale.code))"
>
<v-list-item-title>{{ locale.name }}</v-list-item-title>
</v-list-item>
</v-list>
Also tried using $i18n.setLocale(locale.code) same thing happens.
Not sure if this should be a comment but since I can't comment yet (requires 50 rep), I post this as an extension of the description of the problem.
I am using VueJS and I have a form with two fields. The user is required to enter the first field, and I want the value of second field to be calculated using the value from first field and passing it through a method.
HTML
<div id="app">
<input type="text" v-model="value.label">
<input type="text" v-model="value.slug">
<!-- Slug value to display here -->
<br /><br />
{{value}}
</div>
Javascript
new Vue({
el:'#app',
data:{
value:{
label: '',
slug: '' // This value is calculated by passing label to santiize()
}
},
computed: {
},
methods: {
sanitize (label) {
return label+'something'
}
}
});
The user enters the first field which updates value.label
We need to pass value.label through sanitize() method and update value.slug . This should also immediately show up in the form field.I don't know how to do it. So, if the user types nothing in the form field it will have an automatic value returned as described.
Along with that it would have been awesome, if we allow the user to bypass what the santize function returns, if the user decides to type the slug value himself in the form field. So, if the user decides to type, the typed value will be set.
I created this fiddle for it - https://jsfiddle.net/9z61hvx0/8/
I was able to solve the problem by changing the data structure a bit and adding a watcher to 'label...
new Vue({
el:'#app',
data:{
label:'',
slug: ''
},
computed: {
computedSlug () {
return this.value.label+'Manish'
}
},
watch: {
label: function () {
this.slug = this.sanitize(this.label)
}
},
methods: {
sanitize (label) {
return label+'something'
}
}
});
Any other more sophesticated answers are most welcome :)
I'm trying to resolve this issue but no matter what I try (based on several suggestions solutions found here as well), I can never make it work.
I would like the Jquery validation plugin to validate automatically all the generated fields from a form. My problem is that it will only work on the first generated field; the validation of the subsequent ones will just be a duplicate of the first.
Here's the pertinent html code:
<form class="someFormClass" method="post">
<span>
<input class="calendarName" name="description" value="<?= value_from_php ?>">
<input class="calendarName" name="description" value="<?= value_from_php ?>">
</span>
</form>
And here's the jQuery validation code:
$(function () {
$('form').each(function () {
$(this).validate({
errorElement: "div",
rules: {
description: {
required: true,
remote: {
url: "calendar/calendar_available/",
type: "post",
data: {
name: function () {
return $(".calendarName").val();
}
}
}
}
},
messages: {
description: {
required: "Description field can't be blank !",
remote: "This calendar already exists."
}
}
});
});
So, as stated, the plug-in behaves properly for the first field. But if I check the values posted in Chrome's Network, the "name" key created in the jQuery validation will always send the value of the first input.
I tried many things (trying to implement on more level of ".each" method in the validation, trying to generate dynamically a specific id for each field to point on (instead of a class), trying to modify the plugin code as suggested here (How to validate array of inputs using validate plugin jquery), but it didn't work.
I think there's something I don't grasp about the logic here.
UPDATE :
So, one of the reasons of my problem is that jQuery validation absolutely requires input with different names. See : Jquery Validation with multiple textboxes with same name and corresponding checkboxes with same name
So, I made a script to generate a different name for each input with the intention to dynamically create validation rules based on those names, following this suggestion : https://stackoverflow.com/a/2700420/3504492
My validation script now look like this :
$(function() {
var rules = new Object();
var messages = new Object();
$('input[name*=description_]:text').each(function() {
var currentName = $("input[name="+this.name+"]").val();
rules[this.name] = {
description: {
required: true,
remote: {
url: "calendar/calendar_available/",
type: "post",
data: currentName
}
}
},
color: {required: true}
};
messages[this.name] = {
description: {
required: "Description field can't be blank !",
remote: "This calendar already exists."
},
color: {required: "Color field can't be blank !"}
};
});
$('form').each(function () {
$(this).validate({
errorElement: "div",
rules: rules,
messages: messages
});
}) });
This almost works. Almost because if I limit the rules et messages to the required keys, it will display the validation each field (if I add the specific name to the message string, it will display on the proper field). But with a most complex rule like mine (with a remote key containing various keys for instance), I get a " Cannot read property 'call' of undefined. Exception occurred when checking element , check the 'description' method." error in the Console.
My guess is that the "description" declaration in the "rules" definition should be dynamic too (the current "name" field being visited).
Any suggestion?