I'm building a wizard widget with Durandal, and I'd like to use it like so:
<div data-bind="wizard: options">
<!-- Step 1 -->
<span data-part="step-header-1">
Step 1
</span>
<div data-part="step-content-1">
step content here
</div>
<!-- Step 2 -->
<span data-part="step-header-2">
Step 2
</span>
<div data-part="step-content-2">
step content here
</div>
</div>
This is the actual widget (cut down for brevity):
<div class="wizard-container">
<ul class="steps" data-bind="foreach: steps">
<li>
<span data-bind="html: heading"></span>
</li>
</ul>
<!-- ko foreach: steps -->
<div class="wizard-step" data-bind="css: { active: isActive }">
<div data-bind="html: content">
</div>
</div>
<!-- /ko -->
</div>
I've sort of gotten it working, using jQuery to grab the data-parts, assign the data-part's inner HTML to a property on my step model, and then use the html-binding to bind the content to each step. This works on the DOM side of things, but doing it this way means that my step content won't get data-bound.. I am pretty sure it's because I use the html binding, which does not bind the content.
Is there a way to do this with Durandal widgets, without separating each step into a new view?
Here's an implementation that uses a traditional Durandal master/detail approach in combination with a Tab widget. The tab widget only implements the tabbing functionality, while the Master controls what's pushed into it and the Detail controls the behavior/layout of itself.
Master
Viewmodel
define(['./tab', 'plugins/widget', 'knockout'], function (Tab, widget, ko) {
return {
tabs: ko.observableArray([
new Tab('Durandal', 'A ...', true),
new Tab('UnityDatabinding', 'A ...'),
new Tab('Caliburn.Micro', 'C ...')
]),
addNewTab: function() {
this.tabs.push(new Tab('New Tab ', 'A test tab.'));
}
};
});
View
<div>
<h1>Tabs sample</h1>
<!-- ko widget : {kind: 'tabs', items : tabs} -->
<!-- /ko -->
<button class="btn" data-bind="click: addNewTab">Add</button>
</div>
Detail
Viewmodel
define(['durandal/events', 'knockout'], function(events, ko) {
return function(name, content, isActive) {
this.isActive = ko.observable(isActive || false);
this.name = name;
this.content = content;
};
});
view
<div>
<div data-bind="html: description"></div>
</div>
Tab widget
Viewmodel
define(['durandal/composition', 'jquery'], function(composition, $) {
var ctor = function() { };
ctor.prototype.activate = function(settings) {
this.settings = settings;
};
ctor.prototype.detached = function() {
console.log('bootstrap/widget/viewmodel: detached', arguments, this);
};
ctor.prototype.toggle = function(model, event){
this.deactivateAll();
model.isActive(true);
};
ctor.prototype.deactivateAll = function(){
$.each(this.settings.items(), function(idx, tab){
tab.isActive(false);
});
};
return ctor;
});
View
<div class="tabs">
<ul class="nav nav-tabs" data-bind="foreach: { data: settings.items }">
<li data-bind="css: {active: isActive}">
<a data-bind="text: name, click: $parent.toggle.bind($parent)"></a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: { data: settings.items}">
<div class="tab-pane" data-bind="html: content, css: {active: isActive}"></div>
</div>
</div>
Live version available at: http://dfiddle.github.io/dFiddle-2.0/#extras/default. Feel free to fork.
As I suspected, the problem with my bindings not applying, was due to the fact that I used the html binding to set the step content. When Knockout sets the HTML, it does not apply bindings to it.
I wrote my own HTML binding handler, that wraps the HTML and inserts it as a DOM-node - Knockout will hapily apply bindings to this.
(function(window, $, ko) {
var setHtml = function (element, valueAccessor) {
var $elem = $(element);
var unwrapped = ko.utils.unwrapObservable(valueAccessor());
var $content = $(unwrapped);
$elem.children().remove().end().append($content);
};
ko.bindingHandlers.htmlAsDom = {
init: setHtml,
update: setHtml
};
}(window, jQuery, ko));
Please note, this only works when the binding value is wrapped as a node - e.g within a div tag. If not, it won't render it.
Related
I am running into an issue where I can't use functions I've declared in my models inside my view. To me, it seems like the functions do exist, so I'm not sure why the console tells me they don't exist.
I have extracted relevant parts of my view here:
<div class="col-lg-6">
<div class="page-header">
<h3>Examens</h3>
</div>
<!-- ko foreach: examAttempts -->
<div class="panel-header clickable" data-bind="click: () => toggleOpen()">
<h3 class="d-block">
Examen poging #<span data-bind="text: $index"></span>
<span class="pull-right" data-bind="css: { fa: true, 'fa-chevron-left': !open(), 'fa-chevron-down': open() }"></span>
</h3>
</div>
<div class="panel-body" data-bind="visible: open">
<!-- ko foreach: questions -->
<p>
<b><span data-bind="text: formatQuestion($index)"></span></b>
<br />
<span data-bind="text: answer"></span>
<br />
<i>Beantwoord in: <span data-bind="text: time"></span> seconde(s)</i>
</p>
<!-- /ko -->
</div>
<br /><br /><br />
<!-- /ko -->
</div>
These are my view models (ommitted irrelevant fields):
class ExamAttempt {
questions: KnockoutObservableArray<ExamQuestion>;
open: KnockoutObservable<boolean>;
constructor(questions: any) {
this.questions = ko.observableArray<ExamQuestion>(questions);
this.open = ko.observable<boolean>(false);
}
public toggleOpen(): void {
this.open(!this.open());
}
}
class ExamQuestion {
question: KnockoutObservable<string>;
answer: KnockoutObservable<string>;
time: KnockoutObservable<number>;
constructor(question: string, answer: string, time: number) {
this.question = ko.observable<string>(question);
this.answer = ko.observable<string>(answer);
this.time = ko.observable<number>(time);
}
public formatQuestion(index: number): string {
return `${index + 1}. ${this.question()}`;
}
}
class EditUserProfileModel {
examAttempts: KnockoutObservableArray<ExamAttempt>;
constructor(params: any) {
this.examAttempts = ko.observableArray<ExamAttempt>(params.examAttempts);
}
}
Applying bindings (params is the page model serialized):
ko.applyBindings(new EditUserProfileModel(params));
The errors I am getting (well, they just say the functions are undefined, but to me it seems like they shouldn't be):
Any suggestions would be greatly appreciated.
Yes I figured it out, it was a problem with types. Apparantly, even though params.examAttempts has the same structure and same fields as ExamAttempt class, js/ts does not turn it into an instance of ExamAttempt, which was the problem here (because the functions were defined in ExamAttempt class). My bad!.
#adiga, thank you for taking the time to create a fiddle.
To fix it, I changed this line:
this.examAttempts = ko.observableArray<ExamAttempt>(params.examAttempts)
Into:
this.examAttempts = ko.observableArray<ExamAttempt>(params.examAttempts.map((ea: any) => {
let questions = ea.questions.map((eaq: any) => {
return new ExamQuestion(eaq.question, eaq.answer, eaq.time);
});
return new ExamAttempt(questions);
}));
I have my own knockout's component:
ko.components.register("library-link-form",
{
viewmodel: LibraryLinkViewModel,
template: { controller: "PartialViews", action: "LibraryLinkPartial" }
//This is custom template loader, which loads asp.net partial view from controller via ajax request.
});
My LibraryLinkViewModel.js:
function LibraryLinkViewModel() {
var self = this;
self.OtherLibrary = ko.observable("");
self.Type = ko.observable("");
}
Partial view _LibraryLinkForm:
#{
var libraryDropdownId = $"dropdown-{Guid.NewGuid().ToString().Substring(0, 8)}";
var typeDropdownId = $"dropdown-{Guid.NewGuid().ToString().Substring(0, 8)}";
var scriptId = $"script-{Guid.NewGuid().ToString().Substring(0, 8)}";
var contextId = $"context-{Guid.NewGuid().ToString().Substring(0, 8)}";
var librariesList = //some list with predefined libraries
var typeList = // some list with predefined library's types
}
<!-- ko template: { afterRender: function()
{
eval($('##scriptId').html());
}
}
-->
<!-- /ko -->
<div id ="#contextId">
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-body">
<div class="col-md-12">
<div class="row">
<form class="form-horizontal">
<div class="form-group">
<div class="col-md-6">
#(Html.Kendo().DropDownList()
.Name(libraryDropdownId)
.DataValueField("Value").DataTextField("Text")
.HtmlAttributes(new
{
style = "width: 100%",
data_bind = "value: OtherLibrary"
}).BindTo(librariesList).Deferred()
)
</div>
<div class="col-md-4">
#(Html.Kendo().DropDownList()
.Name(typeDropdownId)
.DataValueField("Value").DataTextField("Text")
.HtmlAttributes(new
{
style = "width: 100%",
data_bind = "value: Type"
}).BindTo(typeList).Deferred()
)
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<deferred-script class="hidden" id="#scriptId">
#(Html.Kendo().DeferredScripts(false))
</deferred-script>
</div>
And finally, how I combine it all:
<button type="button" data-bind="click: addLibraryLink"></button>
<ul class="list-unstyled" data-bind="foreach: LibraryLinks">
<li><library-link-form></library-link-form></li>
</ul>
<script type="text/javascript">
function LibraryViewModel() {
var self = this;
self.LibraryLinks = ko.observableArray();
self.addLibraryLink = function () {
ko.components.clearCachedDefinition();
self.LibraryLinks.push(new LibraryLinkViewModel());
};
}
ko.applyBindings(new LibraryViewModel());
</script>
I'm using Knockout v.3.4, Asp.Net Core v.1.0.0.
So, the problem is that when I'm trying to add new library link to list, knockout bindings simply don't work, maybe because of error:
Uncaught ReferenceError: Unable to process binding "value: function
(){return OtherLibrary }" Message: OtherLibrary is not defined
What should I do with this error? How can I properly add my knockout's component to the list?
The answer is simple. Let's look at the example, and check contexts in it:
<div data-bind="foreach: LibraryLinks"> // here we have LibraryViewModel context
<library-link-form> // here we have LibraryLinkViewModel context
//inside component we have THIRD context, which is empty!
</library-link-form>
</div>
So, the problem is, that OtherLibrary in data_bind = "value: OtherLibrary" refers to the third context, which is empty and nowhere defined.
Simply calling parent's context solves the problem.
For example: data_bind = "value: $parent.OtherLibrary"
I'm just starting learning knockout.js and am a bit stuck on managing observables and updating them correctly. See the jsfiddle for the code.
self.addItem = function(item, event){
ptitle = $(event.currentTarget).prev('h3').text();
self.items.push({
productQty: self.productQty(),
productClip: self.productClip(),
productTitle: ptitle
});
}
http://jsfiddle.net/L61qhrc1/
What I have is a list of existing html elements. I want to create another list from that list with some input fields that can be set. It's mostly working but I cannot figure out from the examples around the net I've been looking at.
when one field updates all the fields in the list update but I only want the field I'm currently updating to be updated not the whole list.
Can any kind person point me in the right direction?
Cheers.
As I said in my comment, your user interface has to be a logical consequence of your data and viewmodel. Your viewmodel must never be concerned with the details of the view.
Also, your fiddle looks pretty over-engineered to me.
The following is what I have gathered from your sample, but in a less byzantine way.
Make separate self-contained viewmodels. Ideally make them so that they can bootstrap themselves from the data you pass to the constructor.
Working with templates keeps the HTML of the view clean and improves modularity and reusability.
For more complex data, consider the the mapping plugin to bootstrap your models.
Consult Unique ids in knockout.js templates for a way to create working <input> / <label> pairs.
function ListItem(data, parent) {
var self = this;
data = data || {};
self.productQty = ko.observable(data.productQty);
self.productClip = ko.observable(!!data.productClip);
}
function Product(data) {
var self = this;
data = data || {};
self.title = ko.observable(data.title);
self.id = ko.observable(data.id);
self.items = ko.observableArray();
self.newItem = new ListItem();
self.addItem = function () {
self.items.push(new ListItem(ko.toJS(self.newItem), self));
};
self.removeItem = function (item) {
self.items.remove(item);
};
}
function ProductList(data) {
var self = this;
data = data || {};
self.products = ko.observableArray(ko.utils.arrayMap(data.products, function (p) {
return new Product(p);
}));
}
var vm = new ProductList({
products: [
{title: "ProductName 1", id: "ProductId 1"},
{title: "ProductName 2", id: "ProductId 2"}
]
});
ko.applyBindings(vm);
ul {
list-style-type: none;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="container">
<div class="m-col-6">
<ul data-bind="foreach: products">
<li data-bind="template: 'productTemplate'"></li>
</ul>
</div>
</div>
<hr />
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
<script type="text/html" id="productTemplate">
<h1 data-bind="text: title"></h1>
<p data-bind="text: id"></p>
<div data-bind="with: newItem">
<input type="checkbox" data-bind="checked: productClip" />
<label>has pump clips</label>
<input type="number" data-bind="value: productQty" />
<button data-bind="click: $parent.addItem">add to list</button>
</div>
<ul data-bind="foreach: items">
<li>
<!-- ko template: 'itemsTemplate' --><!-- /ko -->
<button data-bind="click: $parent.removeItem">Remove</button>
</li>
</ul>
</script>
<script type="text/html" id="itemsTemplate">
<b data-bind="text: $parent.title"></b>
<span data-bind="text: productClip() ? 'has' : 'does not have'"></span> pump clips
(<span data-bind="text: productQty"></span>)
</script>
I can't make an accordion with KnockoutJS, and Bootstrap to work properly. I have defined it like so:
<div class="panel-group" id="accordion" data-bind="foreach: Advertisers()">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<span data-toggle="collapse" data-bind="html: $data, attr: { 'data-target': '#' + $data }"></span>
</h4>
</div>
</div>
<div class="panel-collapse collapse" data-parent="#accordion" data-bind="attr: { id: $data }">
<div class="panel-body">
...content...
"Advertisers" is an observable array of strings, and hence $data is a string. I get one "row" for each advertiser.
All rows are initially collapsed, and clicking a row expands the content below. So far so good.
The problem is that when I click another row I would expect the previous expanded to collapse, but that's not happening. (I couldn't make a fiddle to work either, with Bootstrap and KnockoutJS...)
Edited the code.
What about a simple custom binding, which also allows you to unclutter your view a bit:
ko.bindingHandlers.bootstrapAccordion = {
init: function(elem, value, allBindings) {
var options = ko.utils.unwrapObservable(value()),
handleClass = '[data-toggle]',
contentClass = '.collapse',
openItem = ko.utils.unwrapObservable(options.openItem) || false,
itemClass = '.' + ko.utils.unwrapObservable(options.item) || '.accordion-group',
items = $(elem).find(contentClass);
// toggle: false required to hide items on load
items.collapse({ parent: elem, toggle: false });
if (openItem > -1) items.eq(openItem).collapse('show');
// if the array is dynamic, the collapse should be re-initiated to work properly
var list = allBindings.get('foreach');
if (ko.isObservable(list)) {
list.subscribe(function() {
$(elem).find(contentClass).collapse({ parent: elem, toggle: false });
});
}
$(elem).on('click', handleClass, function() {
$(elem).find(contentClass).collapse('hide');
$(this).closest(itemClass).find(contentClass).collapse('show');
});
}
};
This binding takes 2 parameters (className for container, and optionally, an item to open on load), eg: bootstrapAccordion: {item: 'panel-group', openItem: 0}, and should be set on the same element which has a foreach binding. It assumes that collapsible sections have a collapse class, and the handles to toggle them have a data-toggle attribute.
See it in action here:
http://jsfiddle.net/pkvn79h8/22/
I extended Tyblitz's example above to include support for changing an icon (eg, +/-, up/down arrow) and support for moving to the next panel by applying data-open-next attribute to whatever should move to next panel on click.
ko.bindingHandlers.bootstrapAccordion = {
init: function (elem, value, allBindings) {
var options = ko.utils.unwrapObservable(value()),
handleClass = '[data-toggle]',
contentClass = '.collapse',
openedClass = ko.utils.unwrapObservable(options.openedClass) || 'fa-minus',
closedClass = ko.utils.unwrapObservable(options.closedClass) || 'fa-plus',
openCloseToggleClasses = openedClass + ' ' + closedClass,
openItem = ko.utils.unwrapObservable(options.openItem) || false,
itemClass = '.' + (ko.utils.unwrapObservable(options.item) || 'accordion-group'),
items = $(elem).find(contentClass);
var initializeItems = function(items) {
// toggle: false required to hide items on load
items.collapse({ parent: elem, toggle: false });
if (openItem > -1) {
items.eq(openItem).collapse('show');
items.eq(openItem).closest(itemClass).find('.panel-heading').find('i').toggleClass(openCloseToggleClasses);
items.eq(openItem).closest(itemClass).find('.panel-heading').addClass('active');
}
}
initializeItems(items);
// if the array is dynamic, the collapse should be re-initiated to work properly
var list = allBindings.get('foreach');
if (ko.isObservable(list)) {
list.subscribe(function () {
initializeItems($(elem).find(contentClass));
});
}
$(elem).on('click', handleClass, function () {
$(elem).find(contentClass).collapse('hide');
$(this).closest(itemClass).find(contentClass).collapse('show');
$(this).closest(itemClass).parent().find('.panel-heading i').removeClass(openCloseToggleClasses);
$(this).closest(itemClass).parent().find('.panel-heading i').addClass(closedClass);
$(this).closest(itemClass).parent().find('.panel-heading').removeClass('active');
if ($(this).closest(itemClass).find('.panel-collapse').attr('aria-expanded') === "true") {
$(this).closest(itemClass).find('.panel-heading i').toggleClass(openCloseToggleClasses);
$(this).closest(itemClass).find('.panel-heading').addClass('active');
}
});
$(elem).on('click', '[data-open-next]', function () {
$next = $(this).closest(itemClass).next(itemClass).find(handleClass);
if ($next.length) {
$next.click();
} else {
$same = $(this).closest(itemClass).find(contentClass);
$same.collapse('hide');
$same.parent().find('.panel-heading i').removeClass(openCloseToggleClasses);
$same.parent().find('.panel-heading i').addClass(closedClass);
$same.parent().find('.panel-heading').removeClass('active');
}
});
}
};
Sample markup to use with this binding:
<div data-bind="foreach: listOfThings, bootstrapAccordion: { openItem: 0 }">
<div class="accordion-group">
<div class="panel panel-default" style="cursor: pointer;" data-toggle>
<div class="panel-heading">
<i class="fa fa-plus fa-pull-left fa-2x"></i>
<h3 data-bind="text: name">Title of expander</h3>
</div>
</div>
<div class="panel-collapse collapse">
<div class="panel-body">
<div class="clearfix" data-accordion-content>
<!-- content goes here -->
<!-- ko if: $index() < $parent.listOfThings().length -1 -->
<button data-open-next>Next Thing</button>
<!-- /ko -->
</div>
</div>
</div>
</div>
</div>
I would feel bad not contributing back :)
i have a view model with an observable array. Its populated with some json:
this.socialTiles = ko.observableArray([]);
ko.computed(function () {
jQuery.getJSON( this.apiURL+"&callback=?", function (data) {
var theData = data.entries;
tilesModel.socialTiles(theData);
console.dir(theData);
});
}, tilesModel);
for each item in the model, i build an li using template:
<ul id="tiles-ul" data-bind="template: {name:'twitter_template', foreach:socialTiles}">
<script type="text/html" id="twitter_template">
<li class="social-tile box-shadow">
<div class="header">
<div class="header-img">
<img data-bind="attr: { src: actor.avatar}">
</div>
<div class="name_and_time">
<span class="full-name" data-bind="text: actor.title"></span>
<span class="twitter-name" data-bind="text: actor.id"></span>
<span class="how-long-ago" > 5 minutes ago </span>
</div>
</div>
<div class="message-content" data-bind="html: object.content">
</div>
<div class="footer">
<div class="social-icon-twitter">
</div>
</div>
<span data-bind="text: $index"></span>
</li>
</script>
id like to data-bind the text of an element to be a result of a function, with the current data from the model used as an argument . example:
actor.id is a string containing a twitter url of a user (like "http://twitter.com/iamdiddy")
i'd like to pass that string to a function and return a "#iamdiddy" representation.
<span class="twitter-name" data-bind="text: getTwitterTag(actor.id)"></span>
in the view model
function getTwitterTag("twURL"){
return ... whatever;
}
how can I do this (call a function with argument, not extract the #... )? Does knockout support this functionality?
Try changing
<span class="twitter-name" data-bind="text: getTwitterTag(actor.id)"></span>
to
<span class="twitter-name" data-bind="text: $root.getTwitterTag($data)"></span>