dynamic id ng-repeat - javascript

I am trying to set a dynamic id to my div within my ng-repeat. Let me show an example.
<div id="$index" ng-repeat="repeat in results.myJsonResults">
<div id="$index" ng-click="saveID($index)" ng-repeat="subRepeat in results.myJsonResults.subresults">
</div>
My problem is, when I click on my child div, I want to get my parent id name, but looks like angular doesn't set the ID properly to the div. Is it possible to set a dynamic ID in this concept?
PS: I tried, in the past, create a counter method on my controller and set an index, but it turns out that angular only recognizes the last value of this ID instead.

To answer your question, try this:
<div id="{{$index}}" ...>
While the above should work, this might be not what you want really (!). Please note that this is rather rare with AngularJS to manipulate elements by referring those by id.
You should focus on your model, declarative describe UI and let AngularJS do the rest without doing low-level DOM manipulations "by hand".

A use case I can think of is associating <label> elements with their respective <input> elements, as seen in http://jsfiddle.net/YqUAp/
Applied there is a pkozlowski.opensource's method
<label for="{{ 'textField-' + $index }}">Option {{ $index }}</label>
<input type="text" id="{{ 'textField-' + $index }}" ng-model="field"/>
Though I'm unsure if this is the most effective method. (Even if only for readability)

<div id="{{$index}}" ...>
Works well, but you can also put a dynamic id with the field repeated if - for instance - subRepeat would have an id field.
That would be:
<div id="subRepeat{{subRepeat.id}}" ...>
That will put ids like subRepeat1, subRepeat2, ... on your repeated div

you need to hold the index in your objects
results = { myJsonResults : [
{ name: "abc", id: 1, subResults: [
{ subName: "123", id: 1 },
{ subName: "456", id: 2 }] }
{ name: "xyz", id: 2, subResults: [
{ subName: "789", id: 1 },
{ subName: "098", id: 2 }] }
] };
Also your repeat refers to results, which disconnects them, instead use thisResult:
<div id="thisResult.id" ng-repeat="thisResult in results.myJsonResults">
{{ thisResult.name }}
<div id="subResult.id" ng-click="saveID(subResult.id)" ng-repeat="subResult in thisResult.subResults"> {{ subResult.subName }} </div>
</div>

Related

Find an element by its id in the html file

I have an array
items = [{
"id" : 1,
"name": "peter"},
{
"id" : 2,
"name": "john"}
...
]
I am trying to find an item in the html file by the id based on that, I am trying to hide and element.
It can be easily done in js file, but I am trying to see if there is an easy way to do it in html file
If you want to hide an item, lets say its index is 2, you can do something like this in the HTML
<ng-container *ngFor="let eachItem of allArrayItems; let index=i">
<div *ngIf="i != 2"> // This ngIf will be hidden if the index is 2
whatever logic
</div>
</ng-container>
You can add a template selector to the element
<input #myElement>
Then you can add a reference in your ts file
#ViewChild('myElement') myInput: ElementRef<HtmlInputElement>;
Or even reference the element directly in the template
<button (click)="myElement.focus()"></button>
https://angular.io/api/core/ViewChild
https://angular.io/api/core/ElementRef

Dynamic classes using ng-repeat in AngularJS

This is not a duplicate.
In the other post, they are just doing a ternary operation. I wanna changes classes within ng-repeat.
I have this piece of code with little bugs.
HTML:
<div id="server-id-list-container" class="panel-body col-md-12 scrollbar">
<div class="server-id-list-element" ng-class="serverIdLength > 12 ? 'col-md-3' : 'col-md-2'" ng-repeat="server in selection.serverIds">
<p class="alert alert-info">{{server.serverId}}<span ng-click="removeServerId($index)" class="glyphicon glyphicon-remove"></span></p>
</div>
</div>
Controller:
_.forEach($scope.selection.serverIds, function(a) {
$scope.serverIdLength = a.serverId.length;
});
Scope Object:
[
{
"serverId": "loma1pwipdb2002",
"serverName": "",
},
{
"serverId": "shdmqprtp1",
"serverName": "",
}
]
When I enter "loma1pwipdb2002", the class becomes col-md-3 and since I am using ng-repeat applies for all elements. I want the class to be applied only to serverIdLength > 12 and if its lesser than 12, col-md-2 should get applied.
Please advice.
Is it correct that you want to switch your class for each element of selection.serverIds list separately based on serverId string length? Need to know your selection.serverIds, is it your "Scope Object"? If yes, then I would do just
<div
class="server-id-list-element"
ng-repeat="server in selection.serverIds"
ng-class="server.serverId.length > 12 ? 'col-md-3' : 'col-md-2'"> ... </div>
The problem is that your $scope.serverIdLength is being calculated once for all the list. While you want to have a dynamic class based on each item specific property.
Let's continue discussion if I didn't understand the issue and the entry conditions.
the issue seems to lie here:
_.forEach($scope.selection.serverIds, function(a) {
$scope.serverIdLength = a.serverId.length;
});
No matter what $scope.serverIdLength will always be set to the length of the last serverId. That because it's a global variable and there is only one instance of it. This is why all your classes match. They all reference the same variable.
Instead like #dhilt suggested ditch the controller code and acccess the length in the dom:
ng-class="server.serverId.length > 12 ? 'col-md-3' : 'col-md-2'"
Try that:
ng-class="{'col-md-3':server.serverId.length > 12, 'col-md-2':server.serverId.length <= 12}"

How to define a temporary variable in Vue.js template

Here is my current template:
<a-droppable v-for="n in curSize" :key="n - 1" :style="{width: `${99.99 / rowLenMap[orderList[n - 1]]}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLenMap[orderList[n - 1]] > 10}">
<some-inner-element>{{rowLenMap[orderList[n - 1]]}}</some-inner-element>
</a-draggable>
</a-droppable>
The problem is that i have to write rowLenMap[orderList[n - 1]] multiple times, and i'm afraid vue.js engine will also calculate it multiple times.
What i want is something like this:
<a-droppable v-for="n in curSize" :key="n - 1" v-define="rowLenMap[orderList[n - 1]] as rowLen" :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLen > 10}">
<some-inner-element>{{rowLen}}</some-inner-element>
</a-draggable>
</a-droppable>
I think it's not difficult to implement technically because it can be clumsily solved by using something like v-for="rowLen in [rowLenMap[orderList[n - 1]]]". So is there any concise and official solution?
I found a very simple (almost magical) way to achieve that,
All it does is define an inline (local) variable with the value you want to use multiple times:
<li v-for="id in users" :key="id" :set="user = getUser(id)">
<img :src="user.avatar" />
{{ user.name }}
{{ user.homepage }}
</li>
Note : set is not a special prop in Vuejs, it's just used as a placeholder for our variable definition.
Source: https://dev.to/pbastowski/comment/7fc9
CodePen: https://codepen.io/mmghv/pen/dBqGjM
Update : Based on comments from #vir us
This doesn't work with events, for example #click="showUser(user)" will not pass the correct user, rather it will always be the last evaluated user, that's because the user temp variable will get re-used and replaced on every circle of the loop.
So this solution is only perfect for template rendering because if component needs re-render, it will re-evaluate the variable again.
But if you really need to use it with events (although not advisable), you need to define an outer array to hold multiple variables at the same time :
<ul :set="tmpUsers = []">
<li v-for="(id, i) in users" :key="id" :set="tmpUsers[i] = getUser(id)" #click="showUser(tmpUsers[i])">
<img :src="tmpUsers[i].avatar" />
{{ tmpUsers[i].name }}
{{ tmpUsers[i].homepage }}
</li>
</ul>
https://codepen.io/mmghv/pen/zYvbPKv
credits : #vir us
Although it doesn't make sense here to basically duplicate the users array, this could be handy in other situations where you need to call expensive functions to get the data, but I would argue you're better off using computed property to build the array then.
Judging by your template, you're probably best off with a computed property, as suggested in the accepted answer.
However, since the question title is a bit broader (and comes up pretty high on Google for "variables in Vue templates"), I'll try to provide a more generic answer.
Especially if you don't need every item of an array transformed, a computed property can be kind of a waste. A child component may also be overkill, in particular if it's really small (which would make it 20% template, 20% logic and 60% props definition boilerplate).
A pretty straightforward approach I like to use is a small helper component (let's call it <Pass>):
const Pass = {
render() {
return this.$scopedSlots.default(this.$attrs)
}
}
Now we can write your component like this:
<Pass v-for="n in curSize" :key="n - 1" :rowLen="rowLenMap[orderList[n - 1]]" v-slot="{ rowLen }">
<a-droppable :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLen > 10}">
<some-inner-element>{{rowLen}}</some-inner-element>
</a-draggable>
</a-droppable>
</Pass>
<Pass> works by creating a scoped slot. Read more about scoped slots on the Vue.js documentation or about the approach above in the dev.to article I wrote on the topic.
Appendix: Vue 3
Vue 3 has a slightly different approach to slots. First, the <Pass> component source code needs to be adjusted like this:
const Pass = {
render() {
return this.$slots.default(this.$attrs)
}
}
Today I needed this and used <template> tag and v-for like this
I took this code and
<ul>
<li v-for="key in keys"
v-if="complexComputation(key) && complexComputation(key).isAuthorized">
{{complexComputation(key).name}}
</li>
</ul>
Changed it to this
<ul>
<template v-for="key in keys">
<li v-for="complexObject in [complexComputation(key)]"
v-if="complexObject && complexObject.isAuthorized">
{{complexObject.name}}
</li>
</template>
</ul>
And it worked and I was pleasantly surprised because I didn't know this was possible
This seems like the perfect use case of a child component. You can simply pass your complex computed value(s) as a property to the component.
https://v2.vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props
How about this:
<div id="app">
<div
v-for="( id, index, user=getUser(id) ) in users"
:key="id"
>
{{ user.name }}, {{ user.age }} years old
<span #click="show(user)">| Click to Show {{user.name}} |</span>
</div>
</div>
CodePen: https://codepen.io/Vladimir-Miloevi/pen/xxJZKKx
<template>
<div>
<div v-for="item in itemsList" :key="item.id">
{{ item.name }}
<input v-model="item.description" type="text" />
<button type="button" #click="exampleClick(item.id, item.description)">
Click
</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{
id: 1,
name: 'Name1',
},
{
id: 2,
name: 'Name2',
},
],
}
},
computed: {
itemsList() {
return this.items.map((item) => {
return Object.assign(item, { description: '' })
})
},
},
methods: {
exampleClick(id, description) {
alert(JSON.stringify({ id, description }))
},
},
}
</script>
Just tested using vue3 and works, i think it works universally
{{ (somevariable = 'asdf', null) }}
<span v-if="somevariable=='asdf'">Yey</span>
<span v-else>Ney</span>
It outputs nothing while setting your variable.
mandatory:
opening "("
set your variable
closing ", null)"
curSize is an array. Your temporary values comprise a corresponding implied array sizedOrderList = curSize.map(n => orderList[n-1]). If you define that as a computed, your HTML becomes
<a-droppable v-for="n, index in sizedOrderList" :key="curSize[index]" :style="{width: `${99.99 / rowLenMap[n]}%`, order: n}">
<a-draggable :class="{thin: rowLenMap[n] > 10}">
<some-inner-element>{{rowLenMap[n]}}</some-inner-element>
</a-draggable>
</a-droppable>

AngularJS nested ng-repeat click & show/hide

I've done a ton of reading and research on this topic the past few days and have found some good answers, but for some of the answers I question performance and necessity.
My question pertains to nested ng-repeat scopes. I'm wondering what the best way to achieve an "add item" scenario for adding an item to the nested foreach.
My Code
My HTML is simply 2 ng-repeats and my goal is to be able to add an item to the second (nested) ng-repeat
<div ng-app="myApp">
<div class="nav" ng-controller="FoodsController as vm">
<div class="level1" ng-repeat="foods in vm.foodGroups">{{foods.Name}}
<button type="button" ng-click="vm.addNewFood()">add new food</button>
<div ng-show="vm.newFoodBeingAdded">
<input type="text">
</div>
<div class="level2" ng-repeat="food in foods.FoodsInGroup">{{food.Name}}</div>
</div>
</div>
</div>
My Angular controller looks like this:
app.controller('FoodsController', function () {
var vm = this;
vm.foodGroups = [{
"Name": "Grains",
"FoodsInGroup": [{
"Name": "Wheat"
}, {
"Name": "Oats"
}]
}, {
"Name": "Fruits",
"FoodsInGroup": [{
"Name": "Apple"
}, {
"Name": "Orange"
}]
}];
vm.newFoodBeingAdded = false;
vm.addNewFood = function () {
vm.newFoodBeingAdded = true;
};
});
What should happen
The general work flow would be a user clicks an Add New button and it shows a text box with a "save" button. The text box & button would be within the parent foreach. Once a user saves the item it would then be added to the nested foreach (that logic isn't shown).
The issue
The issue is that when I click "Add New Food" (which should just show 1 of the text boxes & save buttons), all of the text boxes show. How do I ensure I am "scoping" this correctly and that only the text box/button within that parent are shown?
Possible solution
One answer I found was to create a child controller for each nested item. For example I'd have a FoodGroupsController which would manage all the logic for the nested foreach (because there will be a lot more going on than just adding a new item in a real app, so it could be justified).
jsFiddle
Here's a jsFiddle with the code that currently does not function correctly.
There is the forked Fiddle
I made just few changes. The fact is that you were binding the ng-show with a single var in your controller. It was a show me all or show me nothing possibility.
So the fix it, you have to bind this, in your food item, not in the controller himself.
Html :
<button type="button" ng-click="vm.addNewFood(foods)">add new food</button>
<div ng-show="foods.newFoodBeingAdded" class="add-new-food">
<input type="text" placeholer="add a new food">
<button type="button">save new food</button>
</div>
Controller :
vm.addNewFood = function (foods) {
foods.newFoodBeingAdded = true;
};
With this code, you pass the food in param of your function, so you can change the boolean of your food only. And then, your ng-show is just binding on this boolean.

VueJs: How to Edit an Array Item

Simple Todo-App. Please excuse my ignorance for making a rather basic question.
But how would you go about and edit a certain item on an array?
Normally I would try to bind the value of my input to a new property on my data object and then assign this new property to the old property on click throuch Vue's two way databinding.
Like this: http://jsfiddle.net/z7960up7/
Well in my case I use the v-repeat directive, which loops through my data array but I can't use the v-model directive to use the two way databinding, because the values of the properties get corrupted if I do so. (See here: http://jsfiddle.net/doL46etq/2/)
And now I wonder, how I would go about updating my array of tasks:
My idea is to pass the VueObject (this) through my method on click, and then define the index on my event handler and then updating the tasks array, using the index, like this:
HTML:
<input v-el="editInputField" type="text" value="{{ task.body }}" v-on="keyup: doneEdit(this) | key 'enter'"/>
<button v-on="click: editTask(this)">
Edit
</button>
JS:
methods: {
editTask: function (task) {
var taskIndex = this.tasks.indexOf(task.task);
this.tasks[taskIndex] = {
'body': document.querySelector('input').value,
'completed': false
};
console.log(task.task.body);
},
}
Here is my fiddle about it:
http://jsfiddle.net/doL46etq/3/
But the data object is not updated at all and I wonder how I would go about it and update it.
What is the best way to edit an element on the array, using Vue?
Edit: An easy way, would just be to delete the element, and add the new to the array, using the push method, but I really want just to update the element, because I like to keep the dataobject in sync with my backend.
The short answer: Use a component in an extended constructor, then pass the index to that component in HTML as property and use computed properties to link back and forth to your data.
But don't be satisfied with just the short answer. Here is the long one:
Situation: I am using your JSFiddle as base for this answer.
in HTML you have:
<td>{{ task.body }}</td>
<td>
<div>
<input v-el="editInputField" type="text" value="{{ task.body }}" v-on="keyup: doneEdit(this) | key 'enter'" v-model="newEdit"/>
</div>
</td>
<td>
<button v-on="click: editTask(this)" class="mdl-button mdl-js-button mdl-button--icon"> <i class="material-icons">create</i>
</button>
</td>
We want to replace this code with the component. Using this component allows us to identify the index/row we are working on in your set of data.
<td v-component="listitem" index="{{$index}}"></td>
Next step: defining the component.
In order not to cloud our instance with the component, we will create a separate constructor for the vue object, so we can assign the new element to our new object.
This is done using extend.
window.newVue = Vue.extend({
components:
{
'listitem': {
props: ['index'],
computed: {
// both get and set
body: {
get: function () {
return this.$parent.tasks[this.index].body;
},
set: function (v) {
this.$parent.tasks[this.index].body = v
}
}
},
template: '<td>{{ body }}</td><td><div><input type="text" v-model="body" value="{{ body }}"/></div></td><td></td>',
}
}
});
Since we can't define our data properly using an extend, we'll just assume the data already exists while writing the component.
Step 3: defining the actual data:
Instead of using Vue as our object constructor, we'll now use our newly created instantiator.
new newVue({
el: '#todoapp',
data: {
tasks: [{
'body': 'Eat breakfast',
'completed': false
}, {
'body': 'Drink milk',
'completed': false
}, {
'body': 'Go to the store',
'completed': false
}],
newTask: '',
},
});
That's it!
In case you couldn't follow what happened: Here's the Fiddle!
PS: More information about the working of these code can be found on vuejs.org
Actually the simplest way to update an array item, is to two-way bind the task body with the v-model directive.
Example:
http://jsfiddle.net/z7960up7/2/
<div id="demo">
{{ message }}
<div class="edit">
<input type="text" v-model="message">
<button v-on="click: editMessage">Edit</button>
</div>
<pre>{{ $data | json }}</pre>
</div>
And fire an event whenever you blur out of the input box or the edit button is hit.
Also hide the input field with css, by using the v-class directive.

Categories