I am very new to vuejs, and I am working with making a dynamic table where the left column will have a hidden input that will ultimately query a database and display the result in a pop up (for a quick cross referencing ability). I have made it to the point where it builds the table properly with a v-for, but I can't quite figure out how to bind the dynamic v-models to the js function that will run the process. If I am going about this the completely wrong way, please let me know. Cheers and thanks!
...
<tr v-for="tableRow in rtnUnsubs">
<td class="unsubCell">
<input name= "[tableRow.share_id]" v-model="[tableRow.share_id]" value="[tableRow.share_id]">{{ tableRow.share_id }}
<button v-on:click="getSub">view</button>
</td>
<td class="unsubCell">{{ tableRow.unsubscriber_type }}</td>
<td class="unsubCell">{{ tableRow.unsubscriber_id }}</td>
</tr>
...
<script>
...
getSub(/*v-model from input*/) {
window.alert(/*do some stuff with v-model*/)
return;
}
Ordinarily, the method bound to a click will get the event, but you can pass whatever you want it to get (include $event if you want to add that to other arguments).
When you say you want to "bind v-models", I take it to mean you want to pass the current data.
new Vue({
el: '#app',
data: {
rows: [{
share_id: 'IT ME'
},
{
share_id: 'THE OTHER ONE'
}
]
},
methods: {
getSub(data) {
console.log("Working with", data);
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<div v-for="tableRow in rows">
<input name="tableRow.share_id" v-model="tableRow.share_id" value="tableRow.share_id">{{ tableRow.share_id }}
<button v-on:click="getSub(tableRow.share_id)">view</button>
</div>
</div>
Related
Stackblitz
I have the following model
export class Items {
items: Map<string, string>;
createdBy: string;
deliveredBy: string;
}
I want to dynamically create input fields based on items in an array which I am able to I am unable to figure out how to populate data in those fields.
<form>
<tr>
<td *ngFor="let item of myStringArray">
Map {{item}}<input name="value" ([ngModel])="data.items.set(item)" (ngModelChange)="onChange()">
</td>
<td>
Always Present One <input ([ngModel])="data.createdBy" (ngModelChange)="onChange()" />
</td>
<td>
Always Present Two<input ([ngModel])="data.deliveredBy" (ngModelChange)="onChange()" />
</td>
</tr>
</form>
{{ data.items | json }}
Components.ts
export class AppComponent {
public myStringArray = ["First", "Second", "Third"];
data = new Items();
onChange() {
console.log(this.data);
}
}
I found a reference but I am unsure where I am going wrong
Reference Stackblitz
Thank you for your time and help
There are multiple errors in your Stackblitz:
You cannot directly bind to a Map within an ngModel. You need to get in the ngModel and set in the ngModelChange:
<td *ngFor="let item of myStringArray">
Map {{item}}<input name="value" [ngModel]="data.items.get(item)"
(ngModelChange)="data.items.set(item, $event)">
</td>
The syntax for the two way binding is "banana in a box" [(...)] not the opposite:
Always Present One <input [(ngModel)]="data.createdBy" />
Are per the console error "If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions." you need to either set ngModelOptions or add a name field:
Always Present One <input [(ngModel)]="data.createdBy" name="createdBy" (ngModelChange)="onChange()" />
Here is a new Stackblitz fixed.
I'm trying to create a table where the user can hide rows, and keep them hidden as they delete other rows.
In my html I use vuejs to bind a class when rendering the table:
<tr v-for="item in mylist" :class="{'noFruits': (item.fruits.length == 0)}">
There is a user checkbox to hide rows with that class:
<label><input type="checkbox" v-model="showBlankFruits" #change="setBlankDisplay">Show Blank Fruits</label>
In my Vue instance, the checkbox executes a method to hide/show rows with that class via jquery to attach the css display property:
methods: {
setBlankDisplay: function() {
if (this.showBlankFruits) {
$('.noFruits').css('display', '');
} else {
$('.noFruits').css('display', 'none');
}
},
In my jsfiddle, when a user deletes a row, the hidden row reappears. I see that attaching styles with jquery in this instance is not good... does anyone have a suggestion for a better method?
Mixing Vue and jQuery is not recommended, as you can do pretty much everything just using Vue and you don't get any conflicting operations that don't know what the other library/framework is doing.
The following will show the row if either the fruits array length is truthy, in other words not 0, or if showBlankFruits is true:
<tr v-for="item in mylist" v-show="item.fruits.length || showBlankFruits">
The following will toggle showBlankFruits when clicking the checkbox:
<label><input type="checkbox" v-model="showBlankFruits">Show Blank Fruits</label>
Full code example:
JSFiddle
You can also write something like this.
I've used computed and removed the jQuery part completely.
You must declare data as a function instead of an data object (https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function)
You do not need to call the mounted method to set the initial state. It's already set with your data object.
In your code, you have to call mounted, because jQuery can only hide the results, when the DOM is loaded.
new Vue({
el: '#app',
data() {
return {
showBlankFruits: true,
mylist: [
{'user': 'Alice', 'fruits': [{'name': 'apple'}]},
{'user': 'Bob', 'fruits': [{'name': 'orange'}]},
{'user': 'Charlie', 'fruits': []},
{'user': 'Denise', 'fruits': [{'name': 'apple'}, {'name': 'orange'}]},
]
}
},
computed: {
list() {
return this.mylist.filter(item => (item.fruits.length > 0 && !this.showBlankFruits) || (item.fruits.length === 0 && this.showBlankFruits))
},
},
methods: {
delItem(item) {
let index = this.mylist.indexOf(item);
if (index > -1) {
this.mylist.splice(index, 1);
}
}
}
})
<script src="https://unpkg.com/vue"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<div id="app">
<label><input type="checkbox" v-model="showBlankFruits">Show Blank Fruits</label>
<br> <br>
<table>
<tr> <th class="col">User</th> <th class="col">Fruits</th> <th class="col"></th> </tr>
<tr v-for="item in list">
<td>{{ item.user }}</td>
<td> <span v-for="f in item.fruits"> {{ f.name }} </span> </td>
<td> <button #click="delItem(item)">Delete</button> </td>
</tr>
</table>
</div>
This is a great example of the power for declarative rendering rather than using DOM manipulation in components. The problem you are running into is when the Vue engine re-renders your list it doesn't know about the manual manipulation of elemets you did in the setBlankDisplay method. The way to get around that is to use component logic in the definition of the view itself, sort of like you did to set the noFruits class in the first place.
So, I propose you get rid of setBlankDisplay and replace it with the method:
itemDisplay(item) {
if (item.fruits.length === 0 && !this.showBlankFruits) {
return 'none';
}
return '';
},
Then you can reference it in the definition of your tr elements linked to a css display property, like so:
<tr v-for="item in mylist" :class="{'noFruits': (item.fruits.length == 0)}" :style="{display: itemDisplay(item)}">
I've updated the jsfiddle with this modification, showing that the state of hidden fruits remains when other items are deleted.
Take this as a general example of the dangers of using jquery to change the state of the view. Every effort should be taken to define the entire view in terms of component logic.
I have loaded a JSON list into a table and I would like to parse 1 JSON result or multiple results into an object, so I can send it to the server.
My table looks like this so far:
HTML
<tr ng-repeat="t in student">
<td ng-model="herkanserNaam">{{ t.Name }}</td>
<td>{{ t.City }}</td>
<td>
<div class="checkbox" ng-click="laatzien(herkanserNaam, herkanserCheck)" ng-model="herkanserCheck">
<label>
<input type="checkbox">
</label>
</div>
</td>
</tr>
Controller
$scope.laatzien = function(name, active) {
var herkanser = [{
"name" : name,
"active" : false
}];
console.log(herkanser);
}
How would I be able to check one or multiple results and save the data(t.Name) into an object by using a checkbox? So far the function laatzien() is returning the empty values defined in herkanser.
The reason your laatzien method is failing is due to how you are using your directives. Let's work with the example you provided to get your laatzien method to fire.
HTML
<tr ng-repeat="student in students">
<td>{{ student.Name }}</td>
<td>{{ student.City }}</td>
<td>van</td>
<td>Huis</td>
<td>j.huis#student.han.nl</td>
<td>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="student.isActive" ng-change="laatzien(student)">
</label>
</div>
</td>
</tr>
Javascript
$scope.laatzien = function (student) {
var herkanser = [{
"name": student.name,
"active": student.isActive
}];
console.log(herkanser);
}
I have made some opinionated changes in your example for readability purposes, others were needed to get the directives to fire as expected. Below are the changes to your snippets.
Renamed the student array to students. This will require a change in your controller from $scope.student to $scope.students.
Renamed the t object to student.
Removed the ng-click directive from your div.
Added an ng-change directive on your checkbox. Now when you click the checkbox your laatzien method should fire.
Added an isActive property to your student. Inside of your laatzien method, you may now check the state of the checkbox. If the checkbox is checked, student.isActive = true. If the checkbox is not checked, student.isActive = false.
From your code, you seem to want to build the "list of checked students" and send that to the server. In other words, what you want, is to allow the user to check on multiple students and at the end collect everything that was checked and send it over to the server.
If that's the case then your strategy to put an ng-click over the checkbox is wrong.
What you need is to bind your checkbox to your $scope model. Such as this:
<input type="checkbox" ng-model="t.isChecked" ng-true-value="true" ng-false-value="false'">
When the user checks the checkbox for a student. Your model will automatically be updated.
To collect the data to send over the server you need to put an ng-click on a submit button. In the event handler, simply loop through every student in your $scope "students" model and only save the ones that have isChecked property to true to be sent over to the server.
Hope this helps!
You could make a function to push thet item into an obj like so...
$scope.students = [
{
"name":"John",
"city":"Boston"
},
{
"name":"Amy",
"city":"Dallas"
}
]
$scope.activeObj = [];
$scope.laatzien = function(obj) {
if($.inArray(obj, $scope.activeObj) == -1) {
$scope.activeObj.push(obj);
} else {
var index = $scope.activeObj.indexOf(obj);
$scope.activeObj.splice(index, 1);
}
}
http://jsfiddle.net/5fcnazb2/
I'm in the process of replacing one hell of a lot of javascript/jquery code with knockoutjs and I'm trying to figure out the best way forward. I have no time to replace everything at the same time so I will have to integrate the knockout logic with the existing javascript...
Is there a way to populate a knockout view model from javascript which is not called from a data-bind attribute? Any help would be nice since I've not been able to find this anywhere else (at least not anything that worked).
I know what I'm mentioning here isn't the "correct" way of doing things, but I'm trying to migrate parts of the javascript code... Doing it all in one go isn't an option at the moment.
(using knockout 3.2)
Edit:
Typically the existing javascript does something like:
$('#productlist').append(productItemHtmlCode);
And I would rather have it do something like:
ViewModel.productList.push(productItemObject);
If I understand correctly, currently you have something like this:
<div id='myDiv'>
current status is: <span id='statusSpan'>Active</span>
</div>
with some corresponding javascript that might be something like:
function toggleStatus() {
var s= document.getElementById('statusSpan');
s.innerHTML = s.innerHTML == 'Active' ? 'Inactive' : 'Active';
}
And you want to change it so that the javascript is updating the viewmodel rather than manipulating the DOM?
var app = (function() {
var vm = {
statusText: ko.observable('Active'),
toggleStatus: toggleStatus
}
return vm
function toggleStatus() {
vm.statusText = vm.statusText == 'Active' ? 'Inactive' : 'Active';
}
}) ();
ko.applyBindings(app,document.getElementById('myDiv'));
And then the html would be
<div id='myDiv'>
current status is: <span id='statusSpan' data-bind="text: statusText"></span>
</div>
If that's what you're talking about, that's what Knockout is designed for. The javascript updates the viewmodel, knockout manipulates the DOM.
The example you give is easy to represent in Knockout.
the HTML:
<div>
<table data-bind="foreach: products">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: name"></td>
<td data-bind="text: category"></td>
</tr>
</table>
</div>
and in the viewmodel:
vm = {
products: ko.observableArray(), // empty array to start
addProduct: addProduct
}
return vm;
function addProduct(id, name, category) {
products.push({id: id, name: name, category:category});
}
etc.
I have this HTML and am trying to use it to generate a table with the option to add more rows:
<thead>
<tr>
<th>Item</th>
<th>Cost</th>
<th>Amount</th>
<th>
<button class="btn btn-default" data-bind="click: addItem">Add Item</button>
</th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr>
<td data-bind="template: {name: $parent.dynTemplate, data: item }">
<td data-bind="template: {name: $parent.dynTemplate, data: cost() }"></td>
<td data-bind="template: {name: $parent.dynTemplate, data: amount() }">
<td></td>
</tr>
</tbody>
I'm using two different templates:
<script id="inpTmp" type="text/html">
<input data-bind="value: $data" />
</script>
<script id="dispTmp" type="text/html">
<p data-bind="text: $data"></p>
</script>
and am choosing which one to call based on the results of the dynTemplate function. The knockout that I have powering this is very simple:
function ItemAdd(name, icost, iamount) {
var self = this;
self.item = name;
self.cost = ko.observable(icost);
self.amount = ko.observable(iamount);
}
function TestModel() {
var self= this;
self.items= ko.observableArray([
new ItemAdd("a", 5, 10),
new ItemAdd("b", 6, 4)
]);
self.addItem= function() {
self.items.push(new ItemAdd("", 0, 0));
};
self.dynTemplate= function(init, s) {
if(init=== 0 || init=== '') {
return 'inpTmp';
}
return 'dispTmp';
};
}
ko.applyBindings(new TestModel());
The problem that I am running into is that when I enter values into newly created rows, the values in items do not change. They initialize properly, but when I run a function to log the values in items they stay as their defaults. If I use knockout if statements, then everything updates properly. However, using 6 sets of if statements didn't seem very effective so I wanted to see if I could pull it out into a function and then send back the proper template. I'm trying to have inputs there when the value is "" or 0, and then change them to <p> when something is entered.
I've tried changing how the data is passed into the template, and I've tried to assign context using with, but to no avail. Calling dynTemplate does not work unless prefixed by $root or $parent. If that is changing the context, is there a way to reset it?
Is this a problem of context, and if so, is there a way to assign context with the dynTemplate function? Or are the newly created elements from the template not properly binding? I've searched quite a bit, and have found templates within foreach loops, but have not seen functions being used to apply them. If there is a better way to do this, please let me know.
Thank you for the help
Your current sample doesn't work because ko dependency tacker doesn't see that your model field is changed. It happens because 'init' is unwrapped value (not an observable).
This fiddle shows how to make it work with single 'item' field.
http://jsfiddle.net/tabalinas/VXXqr/
In this changed version of dynTemplate we get the value of observable, and thus dependency tracker can see that value changed. Of course, we need to change the template.
self.dynTemplate= function(item, s) {
var val = item.item();
if(val=== 0 || val=== '') {
return 'inpTmp';
}
return 'dispTmp';
};
<script id="inpTmp" type="text/html">
<input data-bind="value: $data.item" />
</script>
<script id="dispTmp" type="text/html">
<p data-bind="text: $data.item"></p>
</script>
For your case, where you need universal template for all fields you can do the following: pass as data the name of the field. The template will pick up data from $parent. dynTemplate func is changed accordingly.
<tbody data-bind="foreach: items">
<tr>
<td data-bind="template: {name: $parent.dynTemplate, data: 'item' }">
</td>
<td data-bind="template: {name: $parent.dynTemplate, data: 'cost' }">
</td>
<td data-bind="template: {name: $parent.dynTemplate, data: 'amount' }">
</td>
</tr>
</tbody>
<script id="inpTmp" type="text/html">
<input data-bind="value: $parent[$data]" />
</script>
<script id="dispTmp" type="text/html">
<p data-bind="text: $parent[$data]"></p>
</script>
self.dynTemplate= function(field, context) {
var value = context.$parent[field]();
if(value=== 0 || value=== '') {
return 'inpTmp';
}
return 'dispTmp';
};
See fiddle http://jsfiddle.net/tabalinas/VXXqr/5/