I have the following code (from a tutorial, but I want to expand it a bit):
<div style="margin-top: 10px;">
v-for="task in taskItems"
:key="task.id"
<q-icon :name="task.icon"/>
<div
{{ task.text }}
</div>
</div>
my taskItems array looks like this:
taskItems: [
{
id: 1,
icon: 'settings',
text: 'Dolor, sit amet consectetm tot',
name: 'style'
},
{
id: 2,
icon: 'exit',
text: 'Lossssr dolor, sit amet consectetm tot',
name: 'getaway'
},
{
id: 3,
icon: 'lego',
text: 'Lomet consectetm tot',
name: 'buildingblocks'
},
{
id: 4,
icon: 'lego',
text: 'Lomet consectetm tot',
name: 'buildingblocks'
}
]
and I have the following v-model:
numberOfTasks: [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' },
}
where a user can pick an object by clicking on a button (left out since that isn't the primary focus) and if they choose the first, then it is equal to value '1', if they choose the second, then it's equal to '2', etc.
The thing is, that I want my v-for at the top, which contains 4 tasks, to show the number of elements from its array equal to the numberOfTasks, that the user chooses.
So if the user chooses value: '3', then the v-for will only show 3 tasks from the tasks array.
I am new to Vue and have tried several things out, but none works.
How do I bind these together and do I need to use task.splice in my ?
Anyone have an idea, what to do?
I'll assume taskItems is a property of data as well as numberOfTask which store the value choosen form numberOfTasks.
You need to change v-for="task in taskItems" in v-for="task in actualTaskItems" where actualTaskItems is a computed property like so
// …
computed: {
actualTaskItems() {
return this.taskItems.splice(0, this.numberOfTask);
}
}
This is freehand so it might not be 100% correct, but you can simulate an index for loop in Vue. I've added a variable selectedNumberOfTasks which would just be a number (a data variable or something) that represents what the user has selected.
<div v-for="index in selectedNumberOfTasks" :key="index">
{{ taskItems[index].text }}
</div>
This will count up in the variable index to a max of selectedNumberOfTasks.
The JS for loop equivalent would be
for (let i = 0; i < this.selectedNumberOfTasks; i++)
Related
I have a list of fields
fields: [
{
id:0,
label: 'dropdown 1',
value: 0,
options: [{ id: 0, value: 0, label: 'Option 1' }]
},
{
id: 1,
label: 'dropdown 2',
value: 0,
options: [
{ id: 0, value: 0, label: 'Hide dropdown 1' },
{ id: 1, value: 1, label: 'Show dropdown 1' }
]
}
]
that are filtered before being shown like so:
<div v-for="field in fields.filter(ffield => showField(ffield))" :key="field.id">
<b-form-select v-model="field.value" #change="changed(field)">
<option v-for="option in field.options" :key="option.id" :value="option.value">
{{ option.label }}
</option>
</b-form-select>
</div>
I can hide 'dropdown 1' based on the selected option in 'dropdown 2'. This is managed by the 'showField' function:
function showField(field) {
return field.value !== 'dropdown 1' || fields[1].value === 1
}
Lets say dropdown 1 is hidden and I select the second option Show dropdown 1. Vue's reactivity runs the showField function for all items in the list before running the changed event on the affected field. So now dropdown 1 is visible and then the eventhandler is called.
But what's really weird is that the parameter field in the changed function corresponds to the value of fields[0] even though I selected an option in 'dropdown 2' or fields[1].
What is going on?
I understand that this doesn't happen when dropdown 1 is after dropdown 2 in the list.
I tried modifying node_modules/bootstrap-vue/esm/components/form-select/form-select.js rendering function but this must be some internal thing that's causing it and I'm not familiar enough to keep digging.
Maybe I disregarded some pattern for this sort of thing?
The filter in the v-for came in because we used v-if="showField(field)" together with the v-for and that was incorrect.
How can I avoid this?
I asked a question which might be unclear to someone. So, I deleted that question and ask again with new approach. I have an API response something like this:
{
id: 2,
name: 'Item 1',
items: [
{
slug: 'Phase 1',
values: [
{
highValue: '12',
lowValue: '8',
color: 'red'
},
{
highValue: '15',
lowValue: '5',
color: 'green'
}
]
},
{
slug: 'Phase 2',
values: [
{
highValue: '14',
lowValue: '6',
color: 'red'
},
{
highValue: '15',
lowValue: '5',
color: 'green'
}
]
}
]
},
{
id: 3,
name: 'Item 2',
items: [
{
slug: 'CBC',
values: [
{
highValue: '10',
lowValue: '7',
color: 'green'
},
{
highValue: '12',
lowValue: '3',
color: 'red'
}
]
}
]
}
I have static block for High Value, Low Value, Red & Green in my HTML. So, for those static blocks, I need to pick appropriate value from the response. For example, for High Value & Red block, I need to pick highValue from the response when color: 'red'. So, I write four function for example:
redHigh (item) {
const res = item.filter(obj => {
return obj.color === 'red'
})
return res[0].highValue
}
Now, I tried to bind the function in v-model like this way:
<v-text-field
outlined
:v-model="redHigh(sub.values)"
placeholder="High"
></v-text-field>
But, that was not working. If I wrote :value instead of :v-model, that would work. But, in that case I don't get the changed value after clicking save button.
save (formIndex) {
console.log(this.items[formIndex])
}
How to solve that?
Codepen Demo
v-model is not for a function; it's for a Vue's data property.
However, I understand your app requirement.
You just need to create Vue computed properties, that can generate a dynamic array using a custom function bind input event from your text field
you can read $emit or v-bind documentation about it
I just read the API of v-text-field component here https://vuetifyjs.com/en/api/v-text-field/#props.
Just need to update it to use value prop and bind change event
Which is would be like this
<div v-for="(sub, index) in item.items" :key="sub.slug">
<v-text-field
outlined
:value="redHigh(sub.values)"
#change="updateSubValue(index, 'red', 'high')"
placeholder="High"
></v-text-field>
updateSubValue(index, color, value) {
// find current sub by index
// find current value's key by color and value
// update vue data
}
It might index first, been a long time I don't develop the Vue app
<div v-for="(index, sub) in item.items" :key="sub.slug">
Or you can find the current sub by slug, no need to add index
I am working on an SPA application where I have a list of data variables that are used as the <option> tags in the dropdown. I want to navigate to another page on the #change event of the dropdown therefore I want to use either the id or name of the select command as the name of the data property. Here is what I mean:
Here is what I have in the data function:
data(){
return {
participants: [
{ value: 0, linkText: '', linkTerm: 'Select' },
{ value: 1, linkText: '/meetings/participants/create', linkTerm: 'Add New' },
{ value: 2, linkText: '/meetings/participants', linkTerm: 'All Participants' },
],
positions: [
{ value: 0, linkText: '', linkTerm: 'Select' },
{ value: 1, linkText: '/meetings/positions/create', linkTerm: 'Add New' },
{ value: 2, linkText: '/meetings/positions', linkTerm: 'All Positions' },
],
}
}
Here is the select tag where I use the above data variables as the <option> tag:
<select name="participants" id="participants" class="select-field" #change="changeRoute($event)">
<option v-for="p in participants" :value="p.value">{{ p.linkTerm }}</option>
</select>
Now I want to have one function changeRoute($event) from which I will navigate to different pages, therefore I want to use the id or name value as the data property, here is the function:
methods:{
changeRoute($event){
var name = $event.target.name;
var value = document.getElementById($event.target.id).value;
}
}
Here in the above function I want to use the name as the data property as below:
I want to write this:
this.name[value].linkText;
And because name here is the name of the tag which is actually participants so the above line of code should mean something like this:
this.participants[value].linkText
And that should return the linkText of the participants object of the data function.
Any help is appreciated in advance.
Change the below line
this.name[value].linkText;
to
this[name][value].linkText;
My problem is to replace the value of a computed property by the input made by a user. My setup is like this:
html
<div class="col-md-3">
<ul style="margin-top: 50px">
<ol v-for="note in notes">
<h3 #click="setActive($index)">{{note.name}}</h3>
</ol>
</ul>
</div>
<div class="col-md-9" v-show="activeNote">
<h2 v-show="nameIsText" #click="switchNameTag()">{{activeNote.name}}</h2>
<input class="form-control" v-show="!nameIsText" #keyup.enter="switchNameTag()" value="{{activeNote.name}}">
<textarea name="note-text" class="form-control" rows=10>{{activeNote.text}}</textarea>
</div>
js
<script>
var vm = new Vue({
el: 'body',
data: {
active: {},
nameIsText: true,
notes: [{
id: 1,
name: 'Note 1',
text: 'Text of note 1'
}, {
id: 2,
name: 'Note 2',
text: 'Text of note 2'
}, {
id: 3,
name: 'Note 3',
text: 'Text of note 3'
}, {
id: 4,
name: 'Note 4',
text: 'Text of note 4'
}, {
id: 5,
name: 'Note 5',
text: 'Text of note 5'
}]
},
methods: {
setActive: function(index) {
this.active = index;
},
switchNameTag: function() {
this.nameIsText = !this.nameIsText;
},
},
computed: {
activeNote: function() {
return this.notes[this.active];
},
},
});
</script>
I've made a simple note-app, if you click one note, a textarea with the text and a heading 2 with the name is displayed.
Now if you click the name within the <h2></h2>-Tags, the heading 2 is replaced by an input field - so the user can edit ne name of the current note.
Everything works, except the fact, that when I edit the name in the input field (the name is a computed property) the name isn't updating. A second problem is, if I click another note after editing the name of one note, the name of the old note remains in the input field instead of showing the name of the newly clicked note.
I've added two pictures for better understanding:
name as h2
name as input
So my (probably related) questions are, how can I edit the computed property in the input field, and display the name of a newly clicked note even if I haven't hit enter after editing the name in the input field?
You want to use the v-model binding for the items you're editing. These give you the two-way binding that will actively update the underlying data items.
Also needed to use v-if instead of v-show because activeNote can be undefined, in which case accessing its members is an error.
<div class="col-md-9" v-if="activeNote">
<h2 v-show="nameIsText" #click="switchNameTag()">{{activeNote.name}}</h2>
<input class="form-control" v-show="!nameIsText" #keyup.enter="switchNameTag()" v-model="activeNote.name">
<textarea name="note-text" class="form-control" rows=10 v-model="activeNote.text"></textarea>
</div>
Fiddle
Description
I have a small product order system, where a user can add order lines, and on each order line add one or more products. (I realise it's quite unusual for more than one product to be on the same order line, but that's another issue).
The products that can be selected on each line is based on a hierarchy of products. For example:
Example product display
T-Shirts
V-neck
Round-neck
String vest
JSON data
$scope.products = [
{
id: 1,
name: 'T Shirts',
children: [
{ id: 4, name: 'Round-neck', children: [] },
{ id: 5, name: 'V-neck', children: [] },
{ id: 6, name: 'String vest (exclude)', children: [] }
]
},
{
id: 2,
name: 'Jackets',
children: [
{ id: 7, name: 'Denim jacket', children: [] },
{ id: 8, name: 'Glitter jacket', children: [] }
]
},
{
id: 3,
name: 'Shoes',
children: [
{ id: 9, name: 'Oxfords', children: [] },
{ id: 10, name: 'Brogues', children: [] },
{ id: 11, name: 'Trainers (exclude)', children: []}
]
}
];
T-Shirts isn't selectable, but the 3 child products are.
What I'm trying to achieve
What I'd like to be able to do, is have a 'select all' button which automatically adds the three products to the order line.
A secondary requirement, is that when the 'select all' button is pressed, it excludes certain products based on the ID of the product. I've created an 'exclusion' array for this.
I've set up a Plunker to illustrate the shopping cart, and what I'm trying to do.
So far it can:
Add / remove order lines
Add / remove products
Add a 'check' for all products in a section, excluding any that are in the 'exclusions' array
The problem
However, although it adds the check in the input, it doesn't trigger the ng-change on the input:
<table class="striped table">
<thead>
<tr>
<td class="col-md-3"></td>
<td class="col-md-6"></td>
<td class="col-md-3"><a ng-click="addLine()" class="btn btn-success">+ Add order line</a></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="line in orderHeader.lines">
<td class="col-md-3">
<ul>
<li ng-repeat="product in products" id="line_{{ line.no }}_product_{{ product.id }}">
{{ product.name }} <a ng-click="selectAll(product.id, line.no)" class="btn btn-primary">Select all</a>
<ul>
<li ng-repeat="child in product.children">
<input type="checkbox"
ng-change="sync(bool, child, line)"
ng-model="bool"
data-category="{{child.id}}"
id="check_{{ line.no }}_product_{{ child.id }}"
ng-checked="isChecked(child.id, line)">
{{ child.name }}
</li>
</ul>
</li>
</ul>
</td>
<td class="col-md-6">
<pre style="max-width: 400px">{{ line }}</pre>
</td>
<td class="col-md-3">
<a ng-click="removeLine(line)" class="btn btn-warning">Remove line</a>
</td>
</tr>
</tbody>
</table>
Javascript
$scope.selectAll = function(product_id, line){
target = document.getElementById('line_'+line+'_product_'+product_id);
checkboxes = target.getElementsByTagName('input');
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].type == 'checkbox') {
category = checkboxes[i].dataset.category;
if($scope.excluded.indexOf(parseInt(category)) == -1)
{
checkboxes[i].checked = true;
// TODO: Check the checkbox, and set its bool parameter to TRUE
}
}
}
}
Update with full solution
There were a couple of issues with the above code. Firstly, I was trying to solve the problem by manipulating the DOM which is very much against what Angular tries to achieve.
So the solution was to add a 'checked' property on the products so that I can track if they are contained on the order line, and then the view is updated automatically.
One drawback of this method is that the payload would be significantly larger (unless it is filtered before being sent to the back-end API) as each order line now has data for ALL products, even if they aren't selected.
Also, one point that tripped me up was forgetting that Javascript passes references of objects / arrays, not a new copy.
The solution
Javascript
var myApp = angular.module('myApp', []);
myApp.controller('CartForm', ['$scope', function($scope) {
var inventory = [
{
id: 1,
name: 'T Shirts',
checked: false,
children: [
{ id: 4, name: 'Round-neck', checked: false, children: [] },
{ id: 5, name: 'V-neck', checked: false, children: [] },
{ id: 6, name: 'String vest (exclude)', checked: false, children: [] }
]
},
{
id: 2,
name: 'Jackets',
checked: false,
children: [
{ id: 7, name: 'Denim jacket', checked: false, children: [] },
{ id: 8, name: 'Glitter jacket', checked: false, children: [] }
]
},
{
id: 3,
name: 'Shoes',
checked: false,
children: [
{ id: 9, name: 'Oxfords', checked: false, children: [] },
{ id: 10, name: 'Brogues', checked: false, children: [] },
{ id: 11, name: 'Trainers (exclude)', checked: false, children: []}
]
}
];
$scope.debug_mode = false;
var products = angular.copy(inventory);
$scope.orderHeader = {
order_no: 1,
total: 0,
lines: [
{
no: 1,
products: products,
total: 0,
quantity: 0
}
]
};
$scope.excluded = [6, 11];
$scope.addLine = function() {
var products = angular.copy(inventory);
$scope.orderHeader.lines.push({
no: $scope.orderHeader.lines.length + 1,
products: products,
quantity: 1,
total: 0
});
$scope.loading = false;
}
$scope.removeLine = function(index) {
$scope.orderHeader.lines.splice(index, 1);
}
$scope.selectAll = function(product){
angular.forEach(product.children, function(item){
if($scope.excluded.indexOf(parseInt(item.id)) == -1) {
item.checked=true;
}
});
}
$scope.removeAll = function(product){
angular.forEach(product.children, function(item){
item.checked=false;
});
}
$scope.toggleDebugMode = function(){
$scope.debug_mode = ($scope.debug_mode ? false : true);
}
}]);
Click here to see the Plunker
You are really over complicating things first by not taking advantage of passing objects and arrays into your controller functions and also by using the DOM and not your data models to try to update states
Consider this simplification that adds a checked property to each product via ng-model
<!-- checkboxes -->
<li ng-repeat="child in product.children">
<input ng-model="child.checked" >
</li>
If it's not practical to add properties to the items themselves, you can always keep another array for the checked properties that would have matching indexes with the child arrays. Use $index in ng-repeat for that
And passing whole objects into selectAll()
<a ng-click="selectAll(product,line)">
Which allows in controller to do:
$scope.selectAll = function(product, line){
angular.forEach(product.children, function(item){
item.checked=true;
});
line.products=product.children;
}
With angular you need to always think of manipulating your data models first, and let angular manage the DOM
Strongly suggest reading : "Thinking in AngularJS" if I have a jQuery background?
DEMO
Why ng-change isn't fired when the checkbox is checked programatically?
It happens because
if($scope.excluded.indexOf(parseInt(category)) == -1)
{
checkboxes[i].checked = true;
// TODO: Check the checkbox, and set its bool parameter to TRUE
}
only affects the view (DOM). ng-change works alongside ngModel, which can't be aware that the checkbox really changed visually.
I suggest you to refer to the solution I provided at How can I get angular.js checkboxes with select/unselect all functionality and indeterminate values?, works with any model structure you have (some may call this the Angular way).