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

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);
});

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>

How to use ng-repeat only if object type is array?

I have a complex object as shown below:
$scope.document =
{
"GENERAL_FIELDS": {
"Source_Type": "custom",
"Annotations": [
"216/content/Factiva_CM_001/Proteins",
"216/content/Factiva_CM_001/Fact"
],
"Content": [
" Baculovirus; Budded virus; Ultrastructure; Cryo-EM;"
],
"Title": [
"Budded baculovirus particle structure revisited"
]
},
"stn": {
"Document_Type": [
"Journal",
"Article"
]
}
}
I want to display all the fields present in "GENERAL_FIELDS" and "stn". Fields' value can either be string or array of strings. If it is array, I further want to ng-repeat on it and display the content. Following is my html:
<div id="titsec" class="comdocdet" ng-repeat="(category, group) in document">
<div ng-repeat="(key, value) in group">
<div class="pTitle">
{{key}}
</div>
<div class="contdesc">
<div ng-if="Array.isArray(value)">
<div ng-repeat="v in value">
{{v}}
</div>
</div>
<div ng-if="!Array.isArray(value)">
{{value}}
</div>
</div>
</div>
</div>
But ng-if="Array.isArray(value)" is never true and array fields are being displayed in object form: ["Journal","Article"]. What am I missing ?
Or add this in your controller and leave rest like it is.
$scope.isArray = angular.isArray;
html would be like this :
<div ng-if="isArray(value)">
<div ng-repeat="v in value">
{{v}}
</div>
</div>
<div ng-if="!isArray(value)">
{{value}}
</div>
Instead of accessing a method on the Array object directly in the template, you should do in your controller. So for example:
<div ng-if="vm.isValueAnArray(value)">
// Some html
</div>
Your controller:
function isValueAnArray(val) {
return Array.isArray(val);
}
I haven't tested it, but logic should be in the controller, not in the template.
This is an issue of Scoping
The scope of the template is relative to $scope in the controller, so when it looks for Array, it will look for that in the controller scope (e.g. $scope.Array).
One option is to use ng-if="window.Array.isArray(value)". See the working example below.
Another option is to set $scope.Array = Array.prototype in the controller. That way there is no need to reference window before calling Array.isArray().
Another option is to create an alias for Array.isArray() in the controller scope:
$scope.isValueAnArray = Array.isArray;
Then call that function to determine if the value is an array.
angular.module('ang', [])
.controller('cont', function($scope) {
//use this to avoid referencing window in the template
//$scope.Array = Array.prototype;
$scope.document = {
"GENERAL_FIELDS": {
"Source_Type": "custom",
"Annotations": [
"216/content/Factiva_CM_001/Proteins",
"216/content/Factiva_CM_001/Fact"
],
"Content": [
" Baculovirus; Budded virus; Ultrastructure; Cryo-EM;"
],
"Title": [
"Budded baculovirus particle structure revisited"
]
},
"stn": {
"Document_Type": [
"Journal",
"Article"
]
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="ang" ng-controller="cont">
<div id="titsec" class="comdocdet" ng-repeat="(category, group) in document">
<div ng-repeat="(key, value) in group">
<div class="pTitle">
{{key}}
</div>
<div class="contdesc">
<div ng-if="window.Array.isArray(value)">
<div ng-repeat="v in value">
{{v}}
</div>
</div>
<div ng-if="!window.Array.isArray(value)">
{{value}}
</div>
</div>
</div>
</div>
</div>

How can I output a KnockoutJs value in an HTML comment

I have a foreach loop that looks something like this in a slimmed version.
<div data-bind="foreach: articles">
<h1 data-bind="text: title"></h1>
</div>
Now I want to add a HTML comment with a value from the binding. The resulting HTML should be rendered like this.
<div data-bind="foreach: articles">
<h1 data-bind="text: myTitle">My title</h1>
<!-- My property value -->
</div>
I want "< ! -- My property value - - >" to come from a property in the current foreach binding. I hoped it would be possible with something simple as
<!-- myProperty -->
Is this possible and if it is, how can I accomplish this?
Thanks.
EDIT:
My solution that I don't like and try to replace with a "good" solution.
<div data-bind="foreach: articles">
<h1 data-bind="text: myTitle">My title</h1>
<p style="display:none;" data-bind="html: $root.commentValue(myProperty)"></p>
</div>
self.commentValue = function (valueToComment) {
return '<!-- ' + valueToComment + ' -->';
}
The only thing that works is this one:
<div data-bind="html: '<!--' + WeightInGramms() + '-->'"></div>
But it has an obvious side effect: there is also a div rendered.
The solution would be using a virtual element like this:
<!-- ko html: "<!--" + WeightInGramms() + '--' + '>' -->
<!-- /ko -->
It nearly works, but there is a big problem: you cannot use html binding in a virtual element (apart from the hack of converting '-->' into '--' + '>' so that it's not confused with the virtual element comment closing).
So, the only possible solution is to create your own custom binding, but making it valid to be used as a virtual element binding.
ko.bindingHandlers['comment'] = {
'init': function(elem, valueAccessor) {
var value = ko.unwrap(valueAccessor());
var comment = $('<!--'+value+'-->')[0];
ko.virtualElements.setDomNodeChildren(elem, [comment]);
},
'update': function (elem, valueAccessor) {
var value = ko.unwrap(valueAccessor());
var comment = $('<!--'+value+'-->')[0];
ko.virtualElements.setDomNodeChildren(elem, [comment]);
}
};
ko.virtualElements.allowedBindings.comment = true;
var vm = {
aComment: ko.observable("This is a comment")
}
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="text" data-bind="value: aComment"></div>
<!--ko comment: aComment --><!-- /ko -->
It's still no perfect because you cannot delete the virtual binding tags, but it's much cleaner than adding a real tag to include the comment. Note that the custom binding implementation uses the special ko.virtualElements API to support virtual elements.

As binding knockout undefined nested foreach

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/

Render container conditionally

Is it possible to render container for a template based on condition with knockout.js?
This does not work, but shows what i want to do:
<div data-bind="foreach: items">
<!-- ko if: $data.startContainer -->
<div class="container">
<!-- ko -->
<div data-bind="html: $data.contentElement"></div>
<!-- ko if: $data.endContainer -->
</div>
<!-- ko -->
</div>
Found a thread on knockout.js github site that indicates this as not possible with the native templating model:
https://github.com/SteveSanderson/knockout/issues/307
Apparently, the closing comment is understand as internal to the not closed div tag.
My hopes were on the dynamic templates, but failed also like shown in the fiddle.
http://jsfiddle.net/XbdGs/3/
<script type="text/html" id="withContainer">
<div class="container">
<!-- ko template: 'withoutContainer' -->
<!-- /ko -->
</div>
</script>
From that i conclude you can try the 3 foreachs solution, use Posthuma suggestion or fallback to another templating engine like jquery.tmpl or Underscore as mentioned on knockout documentation.
http://knockoutjs.com/documentation/template-binding.html
You can do this through a custom binding.
Update:
If you want to open a div and close from another item, the custom binding would look like this:
ko.bindingHandlers.myCustomBinding = {
update: function(element, valueAccessor, allBindings, data, context){
var value = valueAccessor();
var items = ko.utils.unwrapObservable(value);
var currentElement = element;
ko.utils.arrayForEach(items, function(item){
if(item.startContainer){
var container = document.createElement('div');
$(container).append(item.displayContent);
$(container).addClass("container");
currentElement = container;
}
else if(item.endContainer){
$(currentElement).append(item.displayContent);
$(element).append(currentElement);
currentElement = element;
}
else{
$(currentElement).append(item.displayContent);
}
});
}
};
HTML:
<div data-bind='myCustomBinding: items'></div>
There are probably better ways to write this code and possibly use knockouts built-in bindings, but this should be enough to get you started.
http://jsfiddle.net/posthuma/f5wG4/2

Categories

Resources