As binding knockout undefined nested foreach - javascript

So I have html:
<div class="body" data-bind="foreach: { data: Sections }">
<span data-bind="text: '(' + OrderQualifier + ') ' + Text">
</span>
<p data-bind="foreach: { data: Children, as: 'child' }">
<fieldset class="section-edit" data-bind="visible: IsEditing">
<input type="text" data-bind="attr: {value: child.EditedText}" /><!-- child is undefined here even though I have it as my as binding on the above foreach-->
<button data-bind="event: {click: $root.addEdit}">Submit</button>
</fieldset>
</p>
</div>
I tried to do this without the as binding but it was pulling the value from the parent section which also has an EditedText property and get the same result using $data.
The data (Sections) i'm trying to bind looks like:
[
{
"SectionID":1,
"Text":"Parent text",
"Html":null,
"OrderQualifier":"1",
"IsUserCreated":false,
"Children":[
{
"SectionID":2,
"Text":"Child text",
"Html":null,
"OrderQualifier":"1",
"IsUserCreated":false,
"EditCount":0,
"ExplanationCount":0,
"EvidenceCount":0,
"IsEditing":true,
"EditedText":"Child text"
}
],
"EditCount":0,
"ExplanationCount":0,
"EvidenceCount":0,
"IsEditing":true,
"EditedText":"Parent text"
}
]
Any ideas?

Or use virtual elements if you want to keep using the <p> tags
<!-- ko foreach: { data: Children, as: 'child' } -->
<p>
...
</p>
<!-- /ko -->
http://jsfiddle.net/cvtw3b2h/2/

Pretty weird, but try changing
<p data-bind="foreach: { data: Children, as: 'child' }">
....
</p>
to
<div data-bind="...">...</div>
http://jsfiddle.net/cvtw3b2h/1/

Related

Knockout JS show hide div based on page URL

I want to split two different section based on page URL. I'm not aware how to do it using knockout.
The below example I have tried so far.
<!-- ko if: attr: { href: https://stackoverflow.com } -->
<p>Div 1</p>
<!-- /ko -->
<!-- ko if: attr: { href: https://getbootstrap.com } -->
<p>Div 2</p>
<!-- /ko -->
Any clarification please drop a comment. Thanks in Advance.
You can use the template system to perform this kind of switch :
ko.applyBindings({
stackoverflowData : {
totalQuestions : 321
},
bootstrapData : {
version : '5.0.2'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<div data-bind="template: { name: 'stackoverflow-template', data: stackoverflowData }"></div>
<hr />
<div data-bind="template: { name: 'bootstrapData-template', data: bootstrapData }"></div>
<script type="text/html" id="stackoverflow-template">
stackoverflow specific view <br />
Questions: <span data-bind="text: totalQuestions"></span>
</script>
<script type="text/html" id="bootstrapData-template">
bootstrapData specific view <br />
Version <span data-bind="text: version"></span>
</script>
If you want something dynamic you can create a function that returns the template to use based on the viewmodel.
function getTemplate(url) {
// use reg ex
if (url == 'https://stackoverflow.com')
return 'stackoverflow';
if (url == 'https://getbootstrap.com')
return 'bootstrap';
return null;
}
var websites = [{
url: 'https://stackoverflow.com',
specificData: {
totalQuestions: 321
}
},
{
url: 'https://getbootstrap.com',
specificData: {
version: '5.0.2'
}
}
];
websites.map(function(website) {
website.template = ko.computed(function() {
return getTemplate(this.url);
}, website);
return website;
})
ko.applyBindings({
websites: websites
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<div data-bind="foreach: websites">
<div data-bind="template: { name: (template()+'-template'), data: specificData }">
<hr />
</div>
</div>
<script type="text/html" id="stackoverflow-template">
stackoverflow specific view <br /> Questions: <span data-bind="text: totalQuestions"></span>
</script>
<script type="text/html" id="bootstrap-template">
bootstrapData specific view <br /> Version <span data-bind="text: version"></span>
</script>
To make this work, you'll need to bring the url into your viewmodel. You could do this with a simple custom url bindinghandler. When initialized, it stores the current url in the bound observable (in this case myUrl) and adds an event listener for storing a changed url (in this case I use hash change to be able to make the example work).
Now, in HTML you can show or hide divs based on this observable value. Have a look at the example below.
ko.bindingHandlers.url = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
valueAccessor()(location.href);
window.addEventListener('hashchange', function(ev) {
valueAccessor()(ev.newURL);
});
}
};
ko.applyBindings({
myUrl: ko.observable(),
showDiv1: function() {
window.location.hash = 'div1'
},
showDiv2: function() {
window.location.hash = 'div2'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>
Current url: <em data-bind="text: myUrl"></em>
</p>
<button data-bind="click: showDiv1">Show Div 1</button>
<button data-bind="click: showDiv2">Show Div 2</button>
<div data-bind="url: myUrl">
<!-- ko if: myUrl() == "https://stacksnippets.net/js#div1" -->
<p>Div 1</p>
<p data-bind="text: myUrl"></p>
<!-- /ko -->
<!-- ko if: myUrl() == "https://stacksnippets.net/js#div2" -->
<p>Div 2</p>
<p data-bind="text: myUrl"></p>
<!-- /ko -->
</div>

Why isn't the v-bind attribute working properly?

So I'm creating a simple To-Do List app using VueJS:
<template>
<div>
<br/>
<div id="centre">
<div id="myDIV" class="header">
<h2 style="margin:5px">My To Do List</h2>
<input type="text" id="myInput" v-model="text" v-on:keyup.enter="AddNote()" placeholder="Title...">
<span v-on:click="AddNote()" class="addBtn">Add</span>
</div>
<ul id="myUL">
<li v-on:click="ToggleClass(index)" v-for="(item, index) in array" v-bind:class="{ checked: isChecked[index] }">
{{item}}
<span class="close">×</span>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "Notepad",
data() {
return {
array: [],
text: "",
isChecked: []
}
},
methods: {
AddNote: function() {
if(this.text!=="") {
this.array.push(this.text);
this.isChecked.push(false);
this.text = "";
}
},
ToggleClass(index) {
console.log(index);
this.isChecked[index]=!this.isChecked[index];
console.log(this.isChecked);
}
}
}
</script>
However when I click on an item the v-bind attribute doesn't bind the class when I click on it. Instead it binds it when I type something in the text field above.
Can anyone please help?
The isChecked array is not reactive and vue cannot detect changes.
You have to trigger it, for example via $set or splice.
Read more about it here: https://v2.vuejs.org/v2/guide/list.html#Caveats
You can change your code like this:
ToggleClass(index) {
console.log(index);
this.isChecked.splice(index, 1, !this.isChecked[index])
// or this.$set(this.isChecked, index, !this.isChecked[index])
console.log(this.isChecked);
}

Vue.js change model attached to a form upon clicking a list

I have an array of objects. These objects are loaded into a list in vue.js.
Aside from this list, I have a form that displays data from one of these objects. I want to, when clicking one of the list's elements, it will bind this specific object to the form and show its data.
How can do this in Vue.js?
My list code is:
<div id="app-7">
<ul id="food-list" v-cloak>
<food-item v-for="item in foodList" v-bind:food="item" v-bind:key="item.id" inline-template>
<li class="food">
<div class="food-header">
<img :src="'img/' + food.slug +'.png'">
<div class="food-title">
<p>{{food.name}} |
<b>{{food.slug}}</b>
</p>
<p>quantity: {{food.quantity}}</p>
</div>
<div class="food-load"> // load into form upon clicking this
</div>
</div>
</li>
</food-item>
</ul>
</div>
Since I do not have the code for the form, this is my best guess without clarification.
You can add a click handler to the item you want to be clicked. It will pass the value of the food item into the method.
<div class="food-load" #click="setFoodItem(item)">
</div>
And when that method is called, it can assign the clicked item to a data property. I'm not sure where your form is, and if it is in a different component. If it is in a child component, you would have to pass it in as a prop, or emit an event to pass it to a parent component.
data() {
return {
//create a reactive field to store the current object for the form.
foodItemForm: null
};
},
methods: {
//method for setting the current item for the form.
setFoodItem(item) {
this.foodItemForm = item;
}
}
Missing quite a bit of info in your sample code, your script is very important to see to make sense of what you would like to accomplish and where things might be going wrong.
Here's a quick list of the issue I came across with your code:
v-for refers to an individual food item as 'item', inside the loop you're trying to access properties as 'food'
You don't wrap your code in a component unless you're importing the component
When binding a value to 'v-bind:src' (or shorthand ':src') only pass the url, you should be specifying this in your script not inline.
You're better off using a button and the 'v-on:click' (or shorthand '#click') to load your selected food item into your form
You should also include your Javascript
Regardless, here's how I would handle this (took the liberty in filling in some blanks):
<template>
<div id="app">
<ul id="food-list">
<!--<food-item v-for="item in foodList" v-bind:food="item" v-bind:key="item.id" inline-template>-->
<li v-for="item in foodList" class="food">
<div class="food-header">
<img :src="item.slug" v-bind:alt="item.slug" width="250px" height="auto">
<div class="food-title">
<p>{{item.name}} | <b>{{item.slug}}</b></p>
<p>quantity: {{item.quantity}}</p>
</div>
<button class="food-load" #click="loadFoodItem(item.id)">Load Food Item</button>
</div>
</li>
<!--</food-item>-->
</ul>
<form v-if="activeFoodId != null" id="foodItemForm" action="#">
<h3>Food Form</h3>
<label for="food-id">Id:</label>
<input id="food-id" type="number" v-bind:value="foodList[activeFoodId].id"><br/>
<label for="food-slug">Slug:</label>
<input id="food-slug" type="text" v-bind:value="foodList[activeFoodId].slug"><br/>
<label for="food-name">Name:</label>
<input id="food-name" type="text" v-bind:value="foodList[activeFoodId].name"><br/>
<label for="food-quantity">Quantity:</label>
<input id="food-quantity" type="number" v-bind:value="foodList[activeFoodId].quantity">
</form>
</div>
</template>
<script>
export default {
name: 'app',
data: function () {
return {
activeFoodId: null,
foodList: [
{
id: 1,
slug: 'http://3.bp.blogspot.com/-QiJCtE3yeOA/TWHfElpIbkI/AAAAAAAAADE/Xv6osICLe6E/s320/tomato.jpeg',
name: 'tomatoes',
quantity: 4
}, {
id: 2,
slug: 'https://img.purch.com/rc/300x200/aHR0cDovL3d3dy5saXZlc2NpZW5jZS5jb20vaW1hZ2VzL2kvMDAwLzA2NS8xNDkvb3JpZ2luYWwvYmFuYW5hcy5qcGc=',
name: 'bananas',
quantity: 12
}, {
id: 3,
slug: 'https://media.gettyimages.com/photos/red-apples-picture-id186823339?b=1&k=6&m=186823339&s=612x612&w=0&h=HwKqE1MrsWrofYe7FvaevMnSB89FKbMjT-G1E_1HpEw=',
name: 'apples',
quantity: 7
}
]
}
},
methods: {
loadFoodItem: function (foodItemId) {
console.log(foodItemId)
this.activeFoodId = foodItemId
}
}
}
</script>
<style>
/# Irrelevant #/
</style>
Hope it helps!

Knockout.js dynamically selecting template error: "Cannot find template with ID''"

My target is to show "noSets" template if observableArray length less 0 and render template with item details - "showSets", if observableArray length greater 0. I'd like to use templates for this purpose, but FireBug show error: Cannot find template with ID "templatename".
Here is ViewModel:
function SetsViewModel() {
var self = this;
self.usersets = ko.observableArray();
self.getTemplate = function () {
return self.usersets().length > 0 ? "showSets" : "noSets";
}
}
$(document).ready(function () {
ko.applyBindings(new SetsViewModel(), document.getElementById('user_sets'));
});
And here is HTML markup:
<div data-bind="template: { name: $root.getTemplate, foreach: usersets }" id="user_sets">
<script type="text/html" id="noSets">
<p>You do not have items yet.</p>
</script>
<script type="text/html" id="showSets">
<div class="block">
<input type="hidden" data-bind="value: $data.SetId" />
<div class="fav" data-bind="css: { fullop: $data.IsFavorite == true }">
<img alt="" src="img/fav.png" data-bind="click: $root.setFavorite">
</div>
<div>
<img alt="" data-bind="attr: { src: $data.SetImg }">
</div>
<div class="txt">
<h3 data-bind="text: $data.SetName, click: $root.go"></h3>
<p><span data-bind="text: $data.ItemsNumber + ' вещей,'"></span><span data-bind=" text: ' общая цена ' + $data.SetPrice + ' руб'"></span></p>
</div>
</div>
</script>
</div>
How can I fix it?
You can declare the templates outside the div that you're binding to as a work around. As #JeffMercado states:
The actual problem was that since the user_sets uses a template binding, the body is discarded (and the templates along with it)
function SetsViewModel() {
var self = this;
self.usersets = ko.observableArray([{SetId: 1, SetName: 'Name 1'}]);
self.getTemplate = function () {
return self.usersets().length > 0 ? "showSets" : "noSets";
}
}
$(document).ready(function () {
ko.applyBindings(new SetsViewModel(), document.getElementById('user_sets'));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script type="text/html" id="noSets">
<p>You do not have items yet.</p>
</script>
<script type="text/html" id="showSets">
<div class="block">
<input data-bind="value: $data.SetId" />
<div class="txt">
<h3 data-bind="text: 'Set name:' + $data.SetName"></h3>
</div>
</div>
</script>
<div data-bind="template: { name: getTemplate, foreach: usersets }" id="user_sets">
</div>
I think named templates are potentially the wrong approach to this specific issue - they're more designed for when you want a different template for each item, which isn't the case here. Instead, it would be more appropriate to show an entirely different div when there are 0 items:
<div data-bind="visible: usersets().length == 0">
You have no sets
</div>
<div data-bind="visible: usersets().length > 0, foreach: usersets" id="user_sets">
<div class="block">
<input type="hidden" data-bind="value: $data.SetId" />
<div class="fav" data-bind="css: { fullop: $data.IsFavorite == true }">
<img alt="" src="img/fav.png" data-bind="click: $root.setFavorite">
</div>
<div>
<img alt="" data-bind="attr: { src: $data.SetImg }">
</div>
<div class="txt">
<h3 data-bind="text: $data.SetName, click: $root.go"></h3>
<p><span data-bind="text: $data.ItemsNumber + ' вещей,'"></span><span data-bind=" text: ' общая цена ' + $data.SetPrice + ' руб'"></span></p>
</div>
</div>
</div>

KnockoutJS virtual/ wildcard (*) element or similar behavior?

Wondering if there's any way to bind an observable array to an element that is defined by a value (object, array, string, ..) in that array. For example, if I had:
var elements = ko.observableArray(['h1','p','blockquote']);
After applying the viewmodel bindings, I'd have a button for inserting each one of them, how would I go about doing the template? Instinctively, I wanted something like:
<div id="container" data-bind="foreach: elements">
<!-- ko html: '<' + $data + '>' + '</' + $data + '>' -->
<!-- /ko -->
</div>
But as I suspected, KnockoutJS tells me:
Message: The binding 'html' cannot be used with virtual elements
Solution?
I think with a custom binding this is completely possible.
So we just build our own binding called changeTag:
ko.bindingHandlers.changeTag = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
$(element).replaceWith('<'+bindingContext.$data+'></'+bindingContext.$data+'>');
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
}
};
Then apply that binding to the html.
<div data-bind="foreach: elements">
<div data-bind="changeTag:{}"><div>
</div>
Let me know if I misunderstood the question but I think that's what you want.
Here is a fiddle:
http://jsfiddle.net/H5rk6/1/
Extending from my comments to your question. Let's assume your data represents a series of blog posts and each one is broken up into items.
var blogData = {
"title": "My Blog",
"posts": [
{
"title": "First Post",
"items": [
{
"type": "paragraph",
"content": "The first paragraph."
},
{
"type": "paragraph",
"content": "The second paragraph."
},
{
"type": "quote",
"by": "Somebody important",
"content": "Quote text"
}
]
}
]
};
You could work with dynamic templates, like this:
<h1 data-bind="text: title"></h1>
<div data-bind="foreach: posts">
<div class="post">
<h2 data-bind="text: title"></h2>
<div data-bind="foreach: items">
<!-- ko template: {name: 'item-' + type} --><!-- /ko -->
</div>
</div>
</div>
<script type="text/html" id="item-paragraph">
<p data-bind="text: content"></p>
</script>
<script type="text/html" id="item-quote">
<blockquote data-bind="text: content, attr: {title: by}"></blockquote>
</script>
and a plain
ko.applyBindings(blogData);
This way you can decouple the actual presentation from your data.
Minimal fiddle over here.
You could use a "ko if" to check what kind of element it is. I know it'd be a bit verbose, but that's the only way I know of to do something like that.
<!-- ko if: $data == "h1" -->
<h1></h1>
<!-- /ko -->
etc. etc.
Or rather...
<h1 data-bind="if: $data == 'h1'"></h1>
<p data-bind="if: $data == 'p'"></p>
<blockquote data-bind="if: $data == 'blockquote'"></blockquote>
You can use the $data property of your array. Here is a similar working example in jsfiddle: http://jsfiddle.net/wrathchild77/YbLv6/2/
<div data-bind="foreach: elements">
<h1 data-bind="if: $data == 'h1'">Heading 1</h1>
<p data-bind="if: $data == 'p'">paragraph</p>
<blockquote data-bind="if: $data == 'blockquote'">blockquote</blockquote>
</div>
$(function () {
var baseModel = {
// data
elements: ko.observableArray(['h1', 'p', 'blockquote'])
};
ko.applyBindings(baseModel);
});

Categories

Resources