I have a simple popup that I want to use in components. It doesn't bind the data on the popup, but in other parts of the components it does?
viewModel
ko.components.register('customer-search', {
viewModel: function(){
var self = this;
//data
self.search= ko.observable();
self.list= ko.observableArray([{supCode:"CHO024",supName:"supplier"}]);
self.next= ko.observable();
self.prev= ko.observable();
self.testText= ko.observable('test');
//general vars
ko.bindingHandlers.applyJQueryMobileStuff = {
init: function(elem) {
$(elem).trigger("create");
}
}
},
template:
'<div class="customer-search" data-bind="applyJQueryMobileStuff: true">\
<input type="text" data-bind="value: testText"/><br>\
\
Choose Customer\
<div data-role="popup" id="popupSearch1" data-overlay-theme="d" data-theme="a" class="ui-corner-all" style="max-width:600px;">\
Close\
<div data-role="header" data-theme="b" class="ui-corner-top">\
<h4>Customer Search</h4>\
</div>\
<div data-role="content" data-theme="d" class="ui-corner-bottom ui-content">\
<input type="text" name="searchSupplier" placeholder="Customer Name" data-bind="value: testText"/>\
</div>\
</div>\
<\div>\
'
});
view
<div data-bind='component: { name: "customer-search"}'></div>
Just put this in any part of content
I found the answer after lots and lots of reading.
ko.bindingHandlers.applyJQueryMobileStuff = { //refresh component to load with jqm fromat
init: function(elem, valueAccessor, allBindingsAccessor, data, context) {
//init logic
// Make a modified binding context, with a extra properties, and apply it to descendant elements
var innerBindingContext = context.extend(valueAccessor);
ko.applyBindingsToDescendants(innerBindingContext, elem);
$(elem).trigger("create");
// Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
return { controlsDescendantBindings: true };
//doesn't bind data to popup???
},
update: function(elem){
//update logic
alert('update bind');
}
};
Creating custom bindings that control descendant bindings
Related
The Environment
Building an app in PhoneGap and using Handlebars for the templating. Jquery-mobile is in the mix to provide widgets and some out of the box themes. The templates are packed with gulp into a build file and rendered onto the page through separate JS files.
The Route
The file structure
/-
--|templates/
----|login.hbs/
--|www/
----|js/
------|index.js
------|render/
--------|LoginView.js
The template before packing, login.hbs:
<div class="app" data-role="page">
<div data-role="header">
<h1>Index Page</h1>
</div>
<div data-role="main" class="ui-content">
</div>
<div data-role="footer">
<h1>Title on Page</h1>
<button id="login-btn" class="ui-btn ui-icon-plus ui-btn-icon-left">Login Page</button>
</div>
</div>
The file that handles the rendering, LoginView.js
var LoginView = function() {
this.initialize = function() {
this.el = $('<div data-role="page"/>');
this.el.on('click', '#index-btn', this.renderIndex);
};
this.render = function() {
this.el.html(LoginView.template());
return this;
};
this.renderIndex = function() {
console.log('button click');
$('body').html(new IndexView().render().el);
}
this.initialize();
}
LoginView.template = App.templates.login;
The place where this file is called index.js
var app = {
bindEvents: function() {
document.addEventListener('deviceready', this.onDeviceReady, false);
},
onDeviceReady: function() {
app.receivedEvent();
},
receivedEvent: function() {
console.log('Received Event!');
$('body').html(new LoginView().render().el);
$('body').trigger('create');
},
initialize: function() {
var self = this;
this.bindEvents();
}
};
The Question
Is very much like this one about Jquery not loading on handlebars inject. But maybe I'm not fully understanding where this method needs to be called. Currently the Jquery works when the rendering is disabled and simple html is present on the page... Once the rendering is wired up again: Blank page.
I've tried several different places so far:
In the this.render part of LoginView
In the place where it is now
tried both 'create' and 'pagecreate'
used the id of the rendered page instead of body
I've also moved the <div data-role="page"> around a bit.
I have a bootstrap nav-tab and I want to display dynamically content when I select a tab. Each tab must display a div with some text that is returned from ajax call at the controller's action GetSection().
<div class="tabbable">
<ul class="nav nav-tabs" data-bind="foreach: sections">
<li data-bind="css: { active: isSelected }">
<a href="#" data-bind="click: $parent.selectedSection">
<span data-bind="text: name" />
</a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: sections">
<div class="tab-pane" data-bind="css: { active: isSelected }">
<span data-bind="text: 'In section: ' + retValue" />
</div>
</div>
</div>
Javascript code:
var Section = function (name, selected) {
this.name = name;
this.retValue = "";
this.isSelected = ko.computed(function () {
return this === selected();
}, this);
}
var ViewModel = function () {
var self = this;
self.selectedSection = ko.observable();
self.sections = ko.observableArray([
new Section('Tab One', self.selectedSection),
new Section('Tab Two', self.selectedSection),
new Section('Tab Three', self.selectedSection)
]);
self.selectedSection(self.sections()[0]);
self.selectedSection.subscribe(function () {
$.ajax({
url: '#Url.Action("GetSection")',
data: { name: self.selectedSection().name },
type: 'GET',
success: function (data) {
self.selectedSection().retValue=data.text;
}
});
});
}
ko.applyBindings(new ViewModel());
The problem is that retValue from ajax is not displayed. The controller action is this:
public JsonResult GetSection(string name)
{
var ret = new { text = name + "abcd" };
return Json(ret, JsonRequestBehavior.AllowGet);
}
Knockout can only know to update the view for properties that are obsverable (hence the name), so you need to make retValue observable:
var Section = function (name, selected) {
this.name = name; // <-- consider similar change here too
this.retValue = ko.observable(""); // <-- change here
this.isSelected = ko.computed(function () {
return this === selected();
}, this);
}
Then, you need to remember to set an obsverable's value by calling it as a method with the new value as its only argument, e.g.:
$.ajax({
url: '#Url.Action("GetSection")',
data: { name: self.selectedSection().name },
type: 'GET',
success: function (data) {
self.selectedSection().retValue(data.text); // <-- change here
}
});
And finally, if you're binding to a complex expression in your view you need to invoke it as a function (with no arguments) to get its value:
<span data-bind="text: 'In section: ' + retValue()" />
As a side note, realize that you can leave off the parentheses (consider it syntactic sugar from knockout) if you bind straight to just the observable, e.g.:
<span data-bind="text: retValue" />
Which is effectively equivalent to:
<span data-bind="text: retValue()" />
On a foot note, I see you've used this syntax for a click binding:
...
This works... but only by coincidence. You should realize these things together:
$parent.selectedSection contains the result of ko.observable() which means it is in fact a function that can be invoked
the click data-binding will invoke the expression it gets as a function, passing the contextual data (in your case a Section) to that function
So bascially, when the click happens, this happens:
$parent.selectedSection($data) // where $data == the current Section
Which effectively selects the Section.
It would be more verbose though a lot clearer if the $parent had a function:
var self = this;
self.selectChild = function(section) {
// Possibly handle other things here too, e.g. clean-up of the old selected tab
self.selectedSection(section);
}
And then use the click binding in this clear way:
...
On click the selectChild method will be called, again with the contextual data as the argument.
Instead of this
self.selectedSection().retValue=data.text;
Do this
self.selectedSection(data);
This question is exposing that one: integrating jquery ui dialog with knockoutjs
I have Model with array of items like this:
var viewModel = {
items: ko.observableArray([])
}
viewModel.items.push(new DialogModel("title 1"));
viewModel.items.push(new DialogModel("title 2"));
viewModel.items.push(new DialogModel("title 3"));
Next I show these items in markup using foreach statement
<div data-bind="foreach: items">
<div data-bind="text: title"></div>
<button data-bind="click: open">Open</button>
<button data-bind="click: close" >Close</button>
</div>
I need to show JQueryUI dialog on clicking buttons and this dialog should be binded to ItemModel instance.
I do not want to include dialog code inside loop because it is copying in result DOM and makes it huge. I'd like to use dialog in template for example.
JSFiddle mockup here http://jsfiddle.net/YmQTW/8/
Any thoughts?
You can create an array that contains only the opened dialogs and bind this array to the template.
With this code only dom of opened dialogs are duplicated.
var DialogModel = function (title) {
var self = this;
self.title = ko.observable(title);
self.isOpen = ko.observable(false);
self.open = function () {
viewModel.shownDialogs.push(self);
setTimeout(function () { self.isOpen(true); }, 0);
};
self.close = function () {
this.isOpen(false);
};
self.isOpen.subscribe(function () {
if(self.isOpen() === false)
viewModel.shownDialogs.remove(self);
})
};
var viewModel = {
items: ko.observableArray([]),
shownDialogs: ko.observableArray([]),
};
The view :
<div data-bind="foreach: shownDialogs">
<div data-bind="template : 'tmpl'"></div>
</div>
See fiddle
I hope it helps.
Im trying to apply the JQuery UI highlight effect to an element when an item that is bound to a knockout observablearray is updated.
The highlight effect is applied but the highlight color used is always the elements current background color. even if I specify the highlight color using the { color: 'XXXXXXX' } option.
any ideas what might be happening?
Thanks,
Steve.
Code below: The element is the span.tag
<div class="row">
<div class="span12">
<div class="tagsinput favs span12" style="height: 100%;" data-bind="foreach: favs, visible: favs().length > 0">
<span class="tag" data-bind="css: $root.selectedFav() == userPrefID() ? 'selected-fav' : '', attr: { id: 'fav_' + userPrefID() }">
<span data-bind="text: name, click: $root.loadFav.bind($data)"></span>
<a class="tagsinput-fav-link"><i class="icon-trash" data-bind="click: $root.delFav.bind($data)"></i></a>
<a class="tagsinput-fav-link-two" data-bind="visible: $root.selectedFav() == userPrefID()"><i class="icon-save" data-bind=" click: $root.saveFav.bind($data)""></i></a>
</span>
</div>
</div>
</div>
// This is the code that does a save via ajax then highlights the element when done.
$.getJSON('#Url.Action("SaveFav","User")', { id: item.userPrefID(), fav: window.JSON.stringify(fav) }, function (result) {
var savedFav = ko.utils.arrayFirst(self.favs(), function (aFav) {
return aFav.userPrefID() == result.userPrefID; // <-- is this the desired fav?
});
// Fav found?
if (savedFav) {
// Update the fav!
savedFav.value(result.value);
}
}).done(function () {
var elementID = "#fav_" + item.userPrefID();
highlightElement(elementID);
});
// Function to highlight the element
function highlightElement(element) {
$(element).effect("highlight", {}, 1500);
}
I would do this the 'knockout' way... use a custom bindingHandler. You shouldn't be directly manipulating DOM in your viewModel, but only touching properties of your viewModel.
Taking this approach, you simply set a boolean value to true when your save is complete... this triggers the highlight effect (the jquery/dom manipulation neatly hidden away from your viewmodel) and when highlight effect completes, the handler sets the boolean back to false. Nice and tidy.
HTML:
<div id="#fav" data-bind="highlight: done">This is a test div</div>
<br />
<button data-bind="click: save">Simulate Save</button>
Javascript:
ko.bindingHandlers.highlight = {
update: function(element, valueAccessor) {
var obs = valueAccessor();
var val = ko.unwrap(obs);
if (val) {
$(element).effect("highlight", {}, 1500, function() {
obs(false);
});
}
}
};
var vm = function() {
var self = this;
self.done = ko.observable(false);
self.save = function() {
self.done(true);
};
}
ko.applyBindings(new vm());
Fiddle:
http://jsfiddle.net/brettwgreen/pd14q4f5/
Got a slight problem trying to have jquery UI and knockout js to cohoperate. Basically I want to create an accordion with items being added from knockout through a foreach (or template).
The basic code is as follows:
<div id="accordion">
<div data-bind="foreach: items">
<h3></h3>
<div><a class="linkField" href="#" data-bind="text: link"></a></div>
</div>
</div>
Nothing impressive here... The problem is that if I do something like:
$('#accordion').accordion();
The accordion will be created but the inner div will be the header selector (first child, as default) so the effect is not the wanted one.
Fixing stuff with this:
$('#accordion').accordion({ header: 'h3' });
Seems to work better but actually creates 2 accordions and not one with 2 sections... weird.
I have tried to explore knockout templates and using "afterRender" to re-accordionise the div but to no avail... it seems to re-render only the first link as an accordion and not the second. Probably this is due to my beginner knowldge of jquery UI anyway.
Do you have any idea how to make everything work together?
I would go with custom bindings for such functionality.
Just like RP Niemeyer with an example of jQuery Accordion binding to knockoutjs http://jsfiddle.net/rniemeyer/MfegM/
I had tried to integrate knockout and the JQuery UI accordion and later the Bootstrap collapsible accordion. In both cases it worked, but I found that I had to implement a few workarounds to get everything to display correctly, especially when dynamically adding elements via knockout. The widgets mentioned aren't always aware of what is happening with regards to knockout and things can get messed up (div heights wrongly calculated etc...). Especially with the JQuery accordion it tends to rewrite the html as it sees fit, which can be a real pain.
So, I decided to make my own accordion widget using core JQuery and Knockout. Take a look at this working example: http://jsfiddle.net/matt_friedman/KXgPN/
Of course, using different markup and css this could be customized to whatever you need.
The nice thing is that it is entirely data driven and doesn't make any assumptions about layout beyond whatever css you decide to use. You'll notice that the markup is dead simple. This is just an example. It's meant to be customized.
Markup:
<div data-bind="foreach:groups" id="menu">
<div class="header" data-bind="text:name, accordion: openState, click: toggle"> </div>
<div class="items" data-bind="foreach:items">
<div data-bind="text:name"> </div>
</div>
</div>
Javascript:
ko.bindingHandlers.accordion = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
$(element).next().hide();
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var slideUpTime = 300;
var slideDownTime = 400;
var openState = ko.utils.unwrapObservable(valueAccessor());
var focussed = openState.focussed;
var shouldOpen = openState.shouldOpen;
/*
* This following says that if this group is the one that has
* been clicked upon (gains focus) find the other groups and
* set them to unfocussed and close them.
*/
if (focussed) {
var clickedGroup = viewModel;
$.each(bindingContext.$root.groups(), function (idx, group) {
if (clickedGroup != group) {
group.openState({focussed: false, shouldOpen: false});
}
});
}
var dropDown = $(element).next();
if (focussed && shouldOpen) {
dropDown.slideDown(slideDownTime);
} else if (focussed && !shouldOpen) {
dropDown.slideUp(slideUpTime);
} else if (!focussed && !shouldOpen) {
dropDown.slideUp(slideUpTime);
}
}
};
function ViewModel() {
var self = this;
self.groups = ko.observableArray([]);
function Group(id, name) {
var self = this;
self.id = id;
self.name = name;
self.openState = ko.observable({focussed: false, shouldOpen: false});
self.items = ko.observableArray([]);
self.toggle = function (group, event) {
var shouldOpen = group.openState().shouldOpen;
self.openState({focussed: true, shouldOpen: !shouldOpen});
}
}
function Item(id, name) {
var self = this;
self.id = id;
self.name = name;
}
var g1 = new Group(1, "Group 1");
var g2 = new Group(2, "Group 2");
var g3 = new Group(3, "Group 3");
g1.items.push(new Item(1, "Item 1"));
g1.items.push(new Item(2, "Item 2"));
g2.items.push(new Item(3, "Item 3"));
g2.items.push(new Item(4, "Item 4"));
g2.items.push(new Item(5, "Item 5"));
g3.items.push(new Item(6, "Item 6"));
self.groups.push(g1);
self.groups.push(g2);
self.groups.push(g3);
}
ko.applyBindings(new ViewModel());
Is there any reason why you can't apply the accordion widget to the inner div here? For example:
<div id="accordion" data-bind="foreach: items">
<h3></h3>
<div><a class="linkField" href="#" data-bind="text: link"></a></div>
</div>
I attempted the accepted solution and it worked. Just had to make a little change since i was getting following error
Uncaught Error: cannot call methods on accordion prior to initialization; attempted to call method 'destroy'
just had to add following and it worked
if(typeof $(element).data("ui-accordion") != "undefined"){
$(element).accordion("destroy").accordion(options);
}
for details please see Knockout accordion bindings break
You could try this to template it, similar to this:
<div id="accordion" data-bind="myAccordion: { },template: { name: 'task-template', foreach: ¨Tasks, afterAdd: function(elem){$(elem).trigger('valueChanged');} }"></div>
<script type="text/html" id="task-template">
<div data-bind="attr: {'id': 'Task' + TaskId}, click: $root.SelectedTask" class="group">
<h3><b><span data-bind="text: TaskId"></span>: <input name="TaskName" data-bind="value: TaskName"/></b></h3>
<p>
<label for="Description" >Description:</label><textarea name="Description" data-bind="value: Description"></textarea>
</p>
</div>
</script>
"Tasks()" is a ko.observableArray with populated with task-s, with attributes
"TaskId", "TaskName","Description", "SelectedTask" declared as ko.observable();
"myAccordion" is a
ko.bindingHandlers.myAccordion = {
init: function (element, valueAccessor) {
var options = valueAccessor();
$(element).accordion(options);
$(element).bind("valueChanged", function () {
ko.bindingHandlers.myAccordion.update(element, valueAccessor);
});
...
}
What I did was, since my data was being loaded from AJAX and I was showing a "Loading" spinner, I attached the accordion to ajaxStop like so:
$(document).ajaxStart(function(){$("#cargando").dialog("open");}).ajaxStop(function(){$("#cargando").dialog("close");$("#acordion").accordion({heightStyle: "content"});});
Worked perfectly.