Related
i have a number field, that i want to bind value from an array like this :
<div v-for="p in products">
<input type="number" :value="carModel[p.name]" />
</div>
where p.name is a string (product name), car_a :
const app = Vue.createApp({
data() {
return {
products: {...},
carModel:[{car_a:4}, {car_b:2} ]
}
}
...
But this does not works, the input remains empty, while following runs without issue :
<div v-for="p in products">
<input type="number" :value="carModel" />
</div>
const app = Vue.createApp({
data() {
return {
products: {...},
carModel:4
}
}
...
So, my question is, how to bind the value properly from array if i have the key ?
thank you
First off, I wonder if your carModel array item object structure could be improved; perhaps better would be something like:
products: ["car_a", "car_b"],
carModel: [
{
name: "car_a",
value: 4,
},
{
name: "car_b",
value: 2,
},
{
name: "car_c",
value: 3,
},
]
I assume that the Strings held in the products array are a sub-set of the Strings in the carModel array of objects, and since you want an input that is reactive with the data, you will want the input's model to be the values held by each carModel object. So rather than v-for loop over the products array, v-for loop over the carModel but filter out the elements whose Strings are not held by the products array. Since we should not combine v-for with v-if, this filtering should be done in a computed property:
computed: {
filteredCarModel() {
return this.carModel.filter(cm => {
return this.products.includes(cm.name);
});
}
},
so then in the HTML template you can loop over the computed property:
<div v-for="cm in filteredCarModel" :key="cm.name">
<label :for="cm.name">{{ cm.name }} </label>
<input type="number" :id="cm.name" v-model="cm.value" />
</div>
This is key:
This will display proper values in the input elements, and these elements will remain reactive to the model such that changes to the inputs will cause changes to the data model, and visa-versa. Thus the display will be truly bound to the data model, which is what you're asking and what Vue.js is all about.
Here is a sample HTML showing that the data is in fact reactive:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<html>
<head>
<meta charset=utf-8" />
<title>Vue Example</title>
<script src="https://unpkg.com/vue#next"></script>
</head>
<body>
<h1>Vue Example</h1>
<div id="app">
<h2>Show Data</h2>
<div v-for="cm in carModel" :key="cm.name">
{{ cm.name }} : {{ cm.value }}
</div>
<h2>Change Data</h2>
<div v-for="cm in filteredCarModel" :key="cm.name">
<label :for="cm.name">{{ cm.name }} </label>
<input type="number" :id="cm.name" v-model="cm.value" />
</div>
</div>
<script>
Vue.createApp({
data() {
return {
products: ["car_a", "car_b"],
carModel: [
{
name: "car_a",
value: 4,
},
{
name: "car_b",
value: 2,
},
{
name: "car_c",
value: 3,
},
],
};
},
computed: {
filteredCarModel() {
return this.carModel.filter((cm) => {
return this.products.includes(cm.name);
});
},
},
}).mount("#app");
</script>
</body>
</html>
</html>
Note that if the products array is not a subset of the carModel array, if all the Strings present in products are also present in carModel name fields, then there will be no need to have a filteredCarModel() computed property.
change your carModel to hash
carModel: { 'car_a': 4, 'car_b': 2 }
because what you are trying to do is access array by string(which it should be number as index of the array)
Observations :
products should be an array to iterate via v-for.
As carModel is an array. You can not access object properties directly. Access it via index.
Working Demo :
new Vue({
el: "#app",
data: {
products: [{name: 'car_a'}, {name: 'car_b'}],
carModel:[{car_a: 4}, {car_b: 2}]
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(product, index) in products" :key="index">
<input type="number" :value="carModel[index][product.name]"/>
</div>
</div>
I'm looking solution for how to auto fill input in vue js. I have a form which include of input type text, select dropdown, quantity, etc. I want when the select dropdown is selected, then the quantity of vCPU, vRAM, and Storage Capacity will be automatically filled with the value according to the selected Server Flavor.
I tried to choose Flavor Server with Flavor 1 options, vCPU should immediately be filled with a value 4, vRAM should be filled with a value 2, and storage capacity should be filled with a value 10. But the quantity does not appear.
But in the price estimation the numbers are correct, namely vCPU (4), vRAM (2), Storage Capacity (10)
I'm confused, to put the if conditional in the <base-quantity> at the #updateQuantity custom event or in v-if attribute. Is there anyone here that can help me solve this problem?
The full source code is in this codesandbox => https://codesandbox.io/s/suspicious-almeida-rjyy9
Lite.vue
<template>
<div class="container">
<h2 class="font-size-26 txt-secondary">Deka Flexi</h2>
<div class="row">
<div class="col-12">
<form #submit.prevent="submitFormLite">
<div class="form-group form-group--border">
<label class="font-size-22 txt-secondary txt-semibold">Commitment Type</label>
<select name="commitment" id="" class="select-custom" v-model="selectedCommitTypeLite">
<option v-for="ctl in commitTypeLite" :key="ctl.name" :value="ctl.value">{{ ctl.name }}</option>
</select>
</div>
<div class="form-group form-group--border">
<label class="font-size-22 txt-secondary txt-semibold">Server Name</label>
<input type="text" name="name" class="custom-input" v-model="serverNameLite" />
</div>
<div class="form-group form-group--border">
<label class="font-size-22 txt-secondary txt-semibold">Server Flavor</label>
<select name="storage-type" id="" class="select-custom" v-model="selectedServerFlavorLite">
<option v-for="sfl in serverFlavorLite" :key="sfl.name" :value="sfl.value">{{ sfl.name }}</option>
</select>
</div>
<h6 class="font-size-22 txt-secondary txt-semibold">
Components
</h6>
<div class="row form-group--border">
<div class="col-md-6">
<div class="form-group text-center">
<div class="font-size-18 txt-secondary">vCPU (GHz)</div>
<base-quantity #updateQuantity="updateCpuLite"></base-quantity>
</div>
</div>
<div class="col-md-6">
<div class="form-group text-center">
<div class="font-size-18 txt-secondary">vRAM (GB)</div>
<base-quantity #updateQuantity="updateRamLite"></base-quantity>
</div>
</div>
<div class="col-md-6">
<div class="form-group text-center">
<label class="font-size-18 txt-secondary">Storage Type</label>
<select name="storage-type" id="" class="select-custom" v-model="selectedStorageTypeLite">
<option v-for="stl in storageTypeLite" :key="stl.name" :value="stl.value">{{ stl.name }}</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group text-center">
<div class="font-size-18 txt-secondary">Storage Capacity (GB)</div>
<base-quantity #updateQuantity="updateCapacityLite" v-model="updateCapacityLite"></base-quantity>
</div>
</div>
</div>
<div class="row pt-4">
<div class="col-md-6">
<div class="form-group text-center">
<label class="font-size-18 txt-secondary">Public IP</label>
<select name="public-ip" id="" class="select-custom" v-model="selectedPublicIpLite">
<option v-for="pil in publicIpLite" :key="pil.name" :value="pil.value">{{ pil.name }}</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group text-center">
<label class="font-size-18 txt-secondary">OS Type</label>
<select name="os-lite" id="" class="select-custom" v-model="selectedOsLite">
<option v-for="ol in osLite" :key="ol.name" :value="ol.value">{{ ol.name }}</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group text-center">
<div class="font-size-18 txt-secondary">Quantity</div>
<base-quantity #updateQuantity="updateQuantityLite"></base-quantity>
</div>
</div>
</div>
<div class="form-group mt-4 text-center">
<button class="button button__add" #click="addToCart">Submit</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import BaseQuantity from "../base/BaseQuantity.vue";
export default {
components: {
BaseQuantity,
},
data() {
return {
serverNameLite: '',
storageTypeLite: [
{
name: "Storage Type 1",
value: 100
},
{
name: "Storage Type 2",
value: 120
}
],
publicIpLite: [
{
name: "Yes",
value: 120
},
{
name: "No",
value: 20
}
],
osLite: [
{
name: "OS 1",
value: 80
},
{
name: "OS 2",
value: 100
},
{
name: "OS 3",
value: 120
}
],
serverFlavorLite: [
{
name: "Flavor 1",
value: "flavor-1"
},
{
name: "Flavor 2",
value: "flavor-2"
},
{
name: "Flavor 3",
value: "flavor-3"
}
],
commitTypeLite: [
{
name: "Commitment Type 1",
value: 80
},
{
name: "Commitment Type 2",
value: 100
},
{
name: "Commitment Type 3",
value: 120
}
],
selectedStorageTypeLite: "",
selectedPublicIpLite: "",
selectedOsLite: "",
selectedCommitTypeLite: "",
selectedServerFlavorLite:""
};
},
watch: {
serverNameLite: function() {
this.$store.commit('setServerNameLite', this.serverNameLite);
},
selectedStorageTypeLite: function() {
let storageTypeLite = this.storageTypeLite.find((storageTypeLite) => storageTypeLite.value == this.selectedStorageTypeLite);
this.$store.commit('setStorageTypeLite', storageTypeLite);
},
selectedPublicIpLite: function() {
let publicIpLite = this.publicIpLite.find((publicIpLite) => publicIpLite.value == this.selectedPublicIpLite);
this.$store.commit('setPublicIpLite', publicIpLite);
console.log(publicIpLite);
},
selectedOsLite: function() {
let osLite = this.osLite.find((osLite) => osLite.value == this.selectedOsLite);
this.$store.commit('setOsLite', osLite);
},
selectedCommitTypeLite: function() {
let commitTypeLite = this.commitTypeLite.find((commitTypeLite) => commitTypeLite.value == this.selectedCommitTypeLite);
this.$store.commit('setCommitTypeLite', commitTypeLite);
},
selectedServerFlavorLite: function() {
let serverFlavorLite = this.serverFlavorLite.find((serverFlavorLite) => serverFlavorLite.value == this.selectedServerFlavorLite);
this.$store.commit('setServerFlavorLite', serverFlavorLite);
if(this.selectedServerFlavorLite == "flavor-1"){
this.updateCpuLite(4);
this.updateRamLite(2);
this.updateCapacityLite(10);
}
},
},
methods: {
async addToCart() {
let isLiteEmpty = await this.$store.dispatch('isLiteEmpty');
if(!isLiteEmpty) {
this.$store.commit('calculateLiteCost');
this.$store.commit('setLite', this.$store.getters.getLiteState);
this.$store.commit('calculatePrice');
}
},
updateCpuLite(val) {
this.$store.commit('setCpuLite', {qty: val, value: 100});
console.log(val);
},
updateRamLite(val) {
this.$store.commit('setRamLite', {qty: val, value: 100});
console.log(val);
},
updateCapacityLite(val) {
this.$store.commit('setCapacityLite', {qty: val, value: 100});
console.log(val);
},
updateQuantityLite(val) {
this.$store.commit('setQuantityLite', {qty: val, value: 100});
console.log(val);
},
},
};
</script>
selectedServerFlavorLite: function() {
let serverFlavorLite = this.serverFlavorLite.find((serverFlavorLite) => serverFlavorLite.value == this.selectedServerFlavorLite);
this.$store.commit('setServerFlavorLite', serverFlavorLite);
if(this.selectedServerFlavorLite == "flavor-1"){
this.updateCpuLite(4);
this.updateRamLite(2);
this.updateCapacityLite(10);
}
},
BaseQuantity.vue
<template>
<div class="quantity" :class="disabled ? 'quantity__untoggle' : 'quantity__toggle'">
<button type="button" #click="decrement" class="btn quantity__decrement" :disabled="disabled">-</button>
<input type="text" class="quantity__value" :value="quantity" :disabled="disabled" readonly>
<button type="button" #click="increment" class="btn quantity__increment" :disabled="disabled">+</button>
</div>
</template>
<script>
export default {
props : ['disabled'],
data(){
return{
quantity: null
}
},
watch:{
quantity :function(val){
this.$emit('updateQuantity',val);
}
},
methods :{
increment () {
this.quantity++
},
decrement () {
if(this.quantity === 0) {
alert('Negative quantity not allowed')
} else {
this.quantity--
}
}
}
}
</script>
There are multiple ways, but it's all based on how your data is stored and connected through components.
Let's start from BaseQuantity.vue:
data() {
return {
quantity: 0 // you have a local saved state of the quantity
}
},
methods: {
increment() {
this.quantity++ // when user hits something, you increment the LOCAL state
},
}
watch: {
quantity: function(val) { // when the LOCAL value updates
this.$emit('updateQuantity', val); // you emit event it has been updated
}
}
Basically each of your Base Quantity components defines its state (starting from 0), and then tracks its own actions that update that state.
You use those components like
<base-quantity #updateQuantity="updateServer"
And the method calls Vuex to store the new value (gotten from the component, equals to it's internal state):
updateServer(val) {
this.$store.commit('setServer', {qty: val, value: 100});
}
Your first issue is that each of those Base Quantity components define their own initial state, which is internal and separate. Currently, there's no real way to tell any of those "your value is X", it's quite the opposite - they tell the parent "I've updated my value".
In order to do so, you must somehow set the initial value. The very basic approach would be to pass the initial value to the component:
props : ['disabled', 'initialValue'],
data(){
return {
quantity: this.initialValue
}
},
Problem two now is that you don't only need an initial value, you need to set the value from outside, whenever the user selects a dropdown option. But you would also like to keep track of manual updates on the component. So you need a two-directional binding of the values (set and update). That's where the v-model comes in handy. Here's a good article explaining how it worked before, and how it works now: https://v3-migration.vuejs.org/breaking-changes/v-model.html#overview Basically you'll use it like that:
<base-quantity v-model="serverQuantity" />
<!-- would be shorthand for: -->
<base-quantity
:modelValue="serverQuantity"
#update:modelValue="serverQuantity= $event"
/>
You don't store the data in your Calculator component - you store it in Vuex. Now this is the part where you have a lot of solutions and you need to be careful how you design your data flow. I would go with the simplest one:
Use store getters to instruct Base Quantity what its value is: <base-quantity :value="$store.getters.serverQuantity" />. This is a reactive property that would be updated when the store updates its value for server quantity. If you don't have getters you could use the state instead, but that's not suggested.
Remove local variable for quantity, and just use the passed property: <input :value="value" />
Upon update (clicking on a button), emit an event with the new value without updating it locally: increment() { this.$emit('updateQuantity', this.value + 1)
In your handler, commit the update as of now
In order to handle dropdown selection, if you use the approach described above, you just need to wait for the user input (dropdown selection) and populate the store with all the needed fields. Since they are passed to each component, the value would be automatically populated:
watch: {
selectedPackage: function() {
let pack = this.storagePackage.find((pack) => pack.value == this.selectedPackage);
this.$store.commit('setPackage', pack);
// here you should somehow map the pack to the values you'd like to populate
// let's say package "One" (value: 30) means you'd have 8 vRAM, then:
this.updateRam(package.ram);
// this would call this.$store.commit('setRam', { qty: 8, value: 100 });
this.updateCapacity(100);
this.updateCpu(5);
// all the other fields you'd like to update, with the specific values this PACK has (I haven't seen any map for plan -> values)
},
}
p.s. I'm not entirely sure why you store qty: val, value: 100 everywhere, but probably you have some reason about it.
With this approach you'll have a single source of truth about the data. And separate components that just say "I want this value (whatever it is) to be either incremented or decremented". So the component is completely isolated of any knowing of the real business logic of modification and storage, as well as the property name.
The parent is the one that handles data both ways - first it sends it to each component, and second - it handles the user actions, which are then commited to a Vuex store (single point of truth).
You can set values using the v-model but this is a different scenario where the values are based on the selected events. So I reached this solution where you can create a function that sets the values in custom mode
Try using this:
setValues: function () {
this.specifications === this.specifications[1]
? (this.os = this.os[0].name)
: (this.os = '');
You can pass in other Databases, StorageType, Firewall values as well.
I am building a Vue.js application that allows a user to make and view a feed of products.
I have a parent element NewFeed which allows a user to have a name for their feed, and then allows a user to push products into this feed.
The child element NewProduct allows the user to input data for the product, then pushes into the parent's products array. I have noticed that every time I push a new product into the array, every element in the array is changed to be the value of the new element.
How do I change this so that the elements remain as they were originally input?
NewFeed (Parent Element):
Vue.component('NewFeed', {
data: function() {
return {
newFeed: {
name: "",
products: []
},
}
},
methods: {
addProduct: function(p) {
p.id = this.pushedProducts;
this.newFeed.products.push(p);
}
},
template: `
<div class="">
<label for='feedNameInput'/>Feed Name: </label>
<input id='feedNameInput' v-bind:value='newFeed.name' v-on:input='newFeed.name = $event.target.value'/> <br>
<ViewProduct v-for='product in newFeed.products' :key='product.id'></ViewProduct>
<NewProduct v-on:pushProduct='addProduct($event)'></NewProduct>
<button v-on:click='pushNewFeed'>Add Feed</button>
</div>
`});
NewProduct (Child Element)
Vue.component('NewProduct', {
data: function() {
return {
newProduct: { id: 0 }
}
},
methods: {
addProduct: function() {
this.$emit('pushProduct', this.newProduct);
this.newProduct.id++;
}
},
template: `
<div class='newProduct'>
<label for='productNameInput'/>Product Name: </label>
<input id='productNameInput' v-bind:value='newProduct.name' v-on:input='newProduct.name = $event.target.value'/> <br>
<label for='productOriginalPriceInput'>Original Price: </label>
<input id='productOriginalPriceInput' v-bind:value='newProduct.originalPrice' v-on:input='newProduct.originalPrice = $event.target.value'/><br>
<label for='productNewPrice'>New Price: </label>
<input id='productNewPrice' v-bind:value='newProduct.newPrice' v-on:input='newProduct.newPrice = $event.target.value'/><br>
<label for='productDiscountAmount'>Discount Amount: </label>
<input id='productDiscountAmount' v-bind:value='newProduct.discountAmount' v-on:input='newProduct.discountAmount = $event.target.value'/><br>
<label for="productImage">Upload Image: </label>
<input type='file' accept='image/*' id='productImage' v-bind:file='newProduct.imageFile' v-on:change='newProduct.imageFile = $event.target.files[0];'/><br><br><br>
<button v-on:click="addProduct">Add Product</button>
</div>
`});
This happens because the child component emits the same object reference each time (JavaScript objects are passed by reference). So all of the emitted objects are the same object. You have to create a brand new object each time. Maybe the simplest way to do that is to reset the child object right after you emit:
addProduct: function() {
this.$emit('pushProduct', this.newProduct);
const id = this.newProduct.id + 1;
this.newProduct = { id }
}
I got a view in which lies the following <div>
<div ng-repeat="product in Product_List" ng-show="Product_List.length >=1">
<input type="text" ng-model="product.ProductCode" ng-value="CurrentProduct.ProductCode">
<input type="text" ng-model="product.ProductName" ng-value="CurrentProduct.ProductName">
</div>
Now push to the Product_List array, resulting in having the elements above repeated as follows,
$scope.Product_List.push({
"ProductCode": "",
"ProductName": ""
});
So far so good, but following is my problem,
How do i make an array as follows every time i add elements to the Product_List ?
[
{
"ProductCode" :"P1",
"ProductName" :"Coffee"
},
{
"ProductCode" : "P2",
"ProductName" : "Beer"
}
]
You can use the same product object for current value it will update the object value. http://jsfiddle.net/sudeep1049/jbj4movu/
<input type="text" ng-model="product.ProductCode" >
<input type="text" ng-model="product.ProductName">
I'm trying to use and understand the Aurelia ValueConverter in the context of a multi-select form. What I thought would be straight forward, has turned out to be a challenge for me.
I have a form to create a new deal which has multiple categories assigned to it via a multi-select input field. I've bound the output from the form into new_deal.categorizations (in the database deals have categories through categorizations).
Right now on create, through a 'brute force' method, I'm converting each category ID into a {category_id: id} object before posting to the API.
Example just logging the POST output:
create(){
var categorizations = this.new_deal.categorizations;
this.new_deal.categorizations = categorizations.map(function (e) {
return {category_id: e}
});
logger.debug ('POST: ', JSON.stringify(this.new_deal));
}
Example output:
POST: {"name":"new deal","categorizations":[{"category_id":"1"},{"category_id":"2"}]}
But I think this would better be accomplished through a ValueConverter.
Plunker is here with the full code but it's basically:
app.js:
export class App {
constructor(){
this.categories = [{id: 1, name: 'test1'}, {id: 2, name: 'test2'}];
this.new_deal = {
name: 'new deal',
categorizations: null,
};
}
create(){
var categorizations = this.new_deal.categorizations;
this.new_deal.categorizations = categorizations.map(function (e) {return {category_id: e}});
logger.debug ('POST: ', JSON.stringify(this.new_deal));
}
create2(){
logger.debug ('POST: ', JSON.stringify(this.new_deal));
}
}
export class CategoryToIDValueConverter {
fromView(id) {
return id ? id: null;
}
}
And app.html:
<template>
<h1>Testing ValueConverter</h1>
<h3 >New Brute Force Deal</h3>
<form role="form">
<label>Name</label>
<input type="text" placeholder="Ex. Buy One Get One Free" value.bind="new_deal.name">
<label>Categories</label>
<select value.bind="new_deal.categorizations" multiple size="2">
<option repeat.for="category of categories" value.bind="category.id">${category.name}</option>
</select>
</form>
<button type="submit" click.delegate="create()">Save</button>
<h3>New ValueConverter Deal</h3>
<form role="form">
<label>Name</label>
<input type="text" placeholder="Ex. Buy One Get One Free" value.bind="new_deal.name">
<label>Categories</label>
<select class="form-control" value.bind="new_deal.categorizations | categoryToID" multiple size="2">
<option repeat.for="category of categories" value.bind="category.id">${category.name}</option>
</select>
</form>
<button class="btn btn-success" type="submit" click.delegate="create2()">Save</button>
</template>
With this I get an output of
POST: {"name":"new deal","categorizations":["1","2"]}
In fromView in app.js, I would think I could change:
return id ? id: null;
To return an object instead of an individual value:
return id ? {category_id: id} : null
But that results in this error:
Uncaught Error: Only null or Array instances can be bound to a multi-select.
Upon further inspection, it looks like id is coming into fromView as an array...
So I modified fromView to this:
fromView(id) {
if(id){
var categorizations = [];
id.forEach(function(cat_id){
categorizations.push({category_id: cat_id})
});
logger.debug(categorizations);
logger.debug(Object.prototype.toString.call(categorizations));
return categorizations;
} else { return null; }
}
}
Trying to expect an array, and then build an array of categorization objects to return, but as you can see in this Plunker, it loses the select as you click (though the debug logs show the objects being created).
You have an array of category objects, each having a name (string) and id (number). These will be used to populate a select element that allows multiple selection:
export class App {
categories = [
{ id: 1, name: 'test1'},
{ id: 2, name: 'test2'}
];
}
<select multiple size="2">
<option repeat.for="category of categories">${category.name}</option>
</select>
The deal object is comprised of a name (string) and categorizations. Categorization objects look like this: { category_id: 1 }
export class App {
categories = [
{ id: 1, name: 'test1'},
{ id: 2, name: 'test2'}];
deal = {
name: 'new deal',
categorizations: [],
}
}
We want to bind the select element's value to the deal object's categorizations which is an array of objects. This means each of the select element's options need to have a object "value". An HTMLOptionElement's value attribute only accepts strings. Anything we assign to it will be coerced to a string. We can store the categorization object in a special model attribute which can handle any type. More info on this can be found in the aurelia docs.
<select multiple size="2">
<option repeat.for="category of categories" model.bind="{ category_id: category.id }">${category.name}</option>
</select>
Finally we need to bind the select element's value to the deal object's categorizations:
<select value.bind="deal.categorizations" multiple size="2">
<option repeat.for="category of categories" model.bind="{ category_id: category.id }">${category.name}</option>
</select>
All together, the view and view-model look like this:
export class App {
categories = [
{ id: 1, name: 'test1'},
{ id: 2, name: 'test2'}];
deal = {
name: 'new deal',
categorizations: [],
}
createDeal() {
alert(JSON.stringify(this.deal, null, 2));
}
}
<template>
<form submit.delegate="createDeal()">
<label>Name</label>
<input type="text" placeholder="Ex. Buy One Get One Free" value.bind="deal.name">
<label>Categories</label>
<select value.bind="deal.categorizations" multiple size="2">
<option repeat.for="category of categories" model.bind="{ category_id: category.id }">${category.name}</option>
</select>
<button type="submit">Save</button>
</form>
</template>
Here's a working plunker: http://plnkr.co/edit/KO3iFBostdThrHUA0QHY?p=preview
Figured it out with some help from whayes on the Aurelia Gitter channel. So I was on the right track with expecting an array in the fromView method but I also needed a toView method in the ValueConverter.
export class CategoryToIDValueConverter {
toView(cats){
if (cats){
var ids = [];
cats.forEach(function(categorization){
ids.push(categorization.category_id);
});
return ids;
} else { return null; }
}
fromView(id) {
if(id){
var categorizations = [];
id.forEach(function(cat_id){
categorizations.push({category_id: cat_id})
});
return categorizations;
} else { return null; }
}
}
I had tried that too, but I initially assumed I needed to add the converter to the select line of the form and the option line, like so:
<select class="form-control" value.bind="new_deal.categorizations | categoryToID" multiple size="2">
<option repeat.for="category of categories" value.bind="category.id">${category.name}</option>
</select>
But that's actually incorrect. I only needed to apply the categoryToID ValueConverter to the select line and it all worked as expected.
Working Plunker showing how the brute force method doesn't change the model until you click save, and the ValueConverter changes it any time you change the selection.
Final app.js
import {LogManager} from 'aurelia-framework';
let logger = LogManager.getLogger('testItems');
export class App {
constructor(){
this.categories = [{id: 1, name: 'test1'}, {id: 2, name: 'test2'}];
this.new_deal = {
name: 'new deal',
categorizations: [],
};
setInterval(() => this.debug = JSON.stringify(this.new_deal, null, 2), 100);
}
create(){
var categorizations = this.new_deal.categorizations;
this.new_deal.categorizations = categorizations.map(function (e) {return {category_id: e}});
alert(JSON.stringify(this.new_deal, null, 2));
}
create2(){
alert(JSON.stringify(this.new_deal, null, 2));
}
}
export class CategoryToIDValueConverter {
toView(cats){
if (cats){
var ids = [];
cats.forEach(function(categorization){
ids.push(categorization.category_id);
});
return ids;
} else { return null; }
}
fromView(id) {
if(id){
var categorizations = [];
id.forEach(function(cat_id){
categorizations.push({category_id: cat_id})
});
return categorizations;
} else { return null; }
}
}
Final app.html
<template>
<h1>Testing ValueConverter</h1>
<h3 >New Brute Force Deal</h3>
<form role="form">
<label>Name</label>
<input type="text" placeholder="Ex. Buy One Get One Free" value.bind="new_deal.name">
<label>Categories</label>
<select value.bind="new_deal.categorizations" multiple size="2">
<option repeat.for="category of categories" value.bind="category.id">${category.name}</option>
</select>
</form>
<button type="submit" click.delegate="create()">Save</button>
<h3>New ValueConverter Deal</h3>
<form role="form">
<label>Name</label>
<input type="text" placeholder="Ex. Buy One Get One Free" value.bind="new_deal.name">
<label>Categories</label>
<select class="form-control" value.bind="new_deal.categorizations | categoryToID" multiple size="2">
<option repeat.for="category of categories" value.bind="category.id">${category.name}</option>
</select>
</form>
<button type="submit" click.delegate="create2()">Save</button>
<!-- debug -->
<pre><code>${debug}</code></pre>
</template>