I am totally new to knock-out custom binding, I am trying to integrate ckeditor with knock-out biding, I have the following binding got from Google search,
ko.bindingHandlers.wysiwyg = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor();
var valueUnwrapped = ko.unwrap(value);
var allBindings = allBindingsAccessor();
var $element = $(element);
$element.attr('contenteditable', true);
if (ko.isObservable(value)) {
var isSubscriberChange = false;
var isEditorChange = true;
$element.html(value());
var isEditorChange = false;
$element.on('input, change, keyup, mouseup', function () {
if (!isSubscriberChange) {
isEditorChange = true;
value($element.html());
isEditorChange = false;
}
});
value.subscribe(function (newValue) {
if (!isEditorChange) {
isSubscriberChange = true;
$element.html(newValue);
isSubscriberChange = false;
}
});
}
}
}
I have the following code to bind,
$(function () {
$.getJSON("/getdata", function (data) {
ko.applyBindings({
testList: [{
test: ko.observable()
},
{
test: ko.observable()
}]
}, document.getElementById('htmled'));
});
});
HTML as follows
<div id="htmled" data-bind="foreach:testList">
Data
<div class="editor" data-bind="wysiwyg: test">Edit this data</div>
</div>
The binding works and show the toolbar when I call the ko.applyBindings outside the $.getJSON method. But when I call applyBindings inside, the toolbars not appearing. Can any body help me on this? I must be missing something for sure, any help on this is greatly appreciated.
Jsfiddle Added
Working :http://jsfiddle.net/jogejyothish/h4Lt3/1/
Not Working : http://jsfiddle.net/jogejyothish/Se8yR/2/
Jyothish
What's happening is this:
Your page loads with the single div. KO has yet to be applied to this div.
document.ready() fires. The CKEditor script applied CKEditor to any matching divs (none).
You make your ajax call.
The Ajax call completes. You apply bindings.
KO inserts two new divs, neither of which has CKEditor.
In order to fix it, you need to add some code inside your ajax success function to manually initialise the CKEditors, like:
$(".editor").each(function(idx, el) {
CKEDITOR.inline(el)
});
Here it is, working in your fiddle:
http://jsfiddle.net/Se8yR/5/
The reason your working version works is because the bindings are applied in document.ready, so KO renders the two div elements in time, and the CKEditor is successfully applied to them.
CKEditor takes some time to load.
In your first example, it loads after ko applies, which works fine.
In the second example, it loads before ko applies. The problem is that CKEditor looks for the contenteditable attribute which you set with ko, so the editor is not created.
You can create it manually with:
CKEDITOR.inline(element).setData(valueUnwrapped || $element.html());
Doc
Demo
Related
I am using $.observable(array).insert() to append items to a list. This is updating my view as it should: new list items are rendered to the DOM. However, I would like to issue a click event on the new DOM node (I'm relying on the event to add a class to expand the item and attach another listener to the body so the area can be closed).
I have tried both
$.observable(_model.leadTimes).insert(leadTime);
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();
...and
function watchLeadTimes() {
var changeHandler = function (ev, eventArgs) {
if (eventArgs.change === 'insert') {
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();
}
};
$.observe(_model.leadTimes, changeHandler);
}
And neither of them worked, however, if I wrap the jQuery method in a setTimout, like setTimeout(function () { $leadTimes.find('.lead-time-data').last().find('.start-editing').click(); }, 400);, it does work, leading me to believe this is an issue of timing with the DOM render somehow not finishing before my jQuery click() method is invoked.
Since the odds are decent that you will see this, Borris, thank you for the library and all that you do! I think jsViews is an excellent middle ground between the monolithic frameworks out there and plain old jQuery noodling!
Edit 02/09/17
It turns out my issue was overlapping click events--I was inadvertently handling a click to deselect my element immediately after it was selected. However I took the opportunity to rewrite things to use a more declarative approach following Borris' linked example.
Now in my template I am using a computed observable, isSelected to toggle the .editing class:
{^{for leadTimes}}
<tr class="lead-time-data" data-link="class{merge:~isSelected() toggle='editing'}">
<span>{^{:daysLead}}</span>
</tr>
{{/for}}
And this JS:
function addNewLeadTimeClickHandler() {
var onNewLeadTimeClick = function () {
e.stopPropagation(); // this is what I was missing
var leadTime = {
daysLead: 1,
description: ''
};
$.observable(_model.activityMapping.leadTimes).insert(leadTime);
selectLeadtime(_model.activityMapping.leadTimes.length -1);
}
$leadTimes.on('click', '.add', onNewLeadTimeClick);
}
function selectLeadtime(index) {
var addStopEditingClickHandler = function () {
var onClickHandler = function (event) {
if ($(event.target).closest('tr').hasClass('editing')) {
setHandler();
return;
}
selectLeadtime(-1)
};
function setHandler() {
var clickEvent = 'click.ActivityChangeRequestDetailController-outside-edit-row';
$('html:not(.edit)').off(clickEvent).one(clickEvent, onClickHandler);
};
setHandler();
}
if (_model.selectedLeadtimeIndex !== index) {
$.observable(_model).setProperty('selectedLeadtimeIndex', index)
addStopEditingClickHandler();
}
}
function isSelected() {
var view = this;
return this.index === _model.selectedLeadtimeIndex;
}
// isSelected.depends = ["_model^selectedLeadtimeIndex"];
// for some reason I could not get the above .depends syntax to work
// ...or "_model.selectedLeadtimeIndex" or "_model.selectedLeadtimeIndex"
// but this worked ...
isSelected.depends = function() {return [_model, "selectedLeadtimeIndex"]};
The observable insert() method is synchronous. If your list items are rendered simply using {^{for}}, then that is also synchronous, so you should not need to use setTimeout, or a callback. (There are such callbacks available, but you should not need them for this scenario.)
See for example http://www.jsviews.com/#samples/editable/tags (code here):
$.observable(movies).insert({...});
// Set selection on the added item
app.select($.view(".movies tr:last").index);
The selection is getting added, synchronously, on the newly inserted item.
Do you have other asynchronous code somewhere in your rendering?
BTW generally you don't need to add new click handlers to added elements, if you use the delegate pattern. For example, in the same sample, a click handler to remove a movie is added initially to the container "#movieList" with a delegate selector ".removeMovie" (See code). That will work even for movies added later.
The same scenario works using {{on}} See http://www.jsviews.com/#link-events: "The selector argument can target elements that are added later"
I am bit new to knockout and jquery mobile, There was a question which is already answered, I need to optimize the PageStateManager class to use generic bindings, currently PageStateManager can only use for one binding,I would really appreciate if someone can guide me to create a generic class to manage page states with knockout bindings Heere is the working code,http://jsfiddle.net/Hpyca/14/
PageStateManager = (function () {
var viewModel = {
selectedHospital: ko.observable()
};
var changePage = function (url, viewModel) {
console.log(">>>>>>>>" + viewModel.id());
$.mobile.changePage(url, {viewModel: viewModel});
};
var initPage = function(page, newViewModel) {
viewModel.selectedHospital(newViewModel);
};
var onPageChange = function (e, info) {
initPage(info.toPage, info.options.viewModel);
};
$(document).bind("pagechange", onPageChange);
ko.applyBindings(viewModel, document.getElementById('detailsView'));
return {
changePage: changePage,
initPage: initPage
};
})();
Html
<div data-role="page" data-theme="a" id="dashBoardPage" data-viewModel="dashBoardViewModel">
<button type="button" data-bind="click: goToList">DashBoard!</button>
</div>
New dashboard model
var dashBoardViewModel = function() {
var self = this;
self.userName = ko.observable('Welcome! ' + "UserName");
self.appOnline = ko.observable(true);
self.goToList = function(){
//I would like to use PageStateManager here
// PageStateManager.changePage($("#firstPage"),viewModel);
ko.applyBindings(viewModel,document.getElementById("firstPage"));//If I click Dashbord button multiple times it throws and multiple bind exception
$.mobile.changePage($("#firstPage"));
}
}
ko.applyBindings(dashBoardViewModel,document.getElementById("dashBoardPage"));
update url : http://jsfiddle.net/Hpyca/14/
Thank you in advance
I would probably go for creating a NavigationService which only handles changing the page and let knockout and my view models handle the state of the pages.
An simple example of such a NavigationService could be:
function NavigationService(){
var self = this;
self.navigateTo = function(pageId){
$.mobile.changePage($('#' + pageId));
};
}
You could then, in your view models just call it when you want it to navigate to a new page. One example would be upon selection of a hospital (which could be done either via a selection function or by manually subscribing to changes to the selectedHospital observable):
self.selectHospital = function(hospital){
self.selectedHospital(hospital);
navigationService.navigateTo('detailsView');
};
Other than the call to the navigationService to navigate, it's just ordinary knockout to keep track of which viewmodel should be bound where. A lot easier than having jquery mobile keeping track of which viewmodel goes where, if you ask me.
I have updated your jsfiddle to show a sample of how this could be done, making as few changes as possible to the HTML code. You can find the updated fiddle at http://jsfiddle.net/Hpyca/15/
I have the TinyMCE WYSiWYG Editor presenting text depending on a selected object, but experience problem with the binding.
The first "instanciation" seems to work, but when choosing a new text from the drop down list of available text the editor goes blank and Firebug console tells me:
TypeError: D.hasChildNodes is not a function
...ute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r...
and
NS_ERROR_UNEXPECTED: Unexpected error
.../,"$1"));return false}});if(!u.getParam("accessibility_focus")){g.add(i.add(k,"a...
I have tried to recreate my code here: http://jsfiddle.net/xc4sz/1/
It´s not 100% but at least it does´t work. ;)
If I instead of clicking directly from text 1 to text 2 go via the "Choose option" the text is presented properly.
I guess it has to do with the "init" section below:
ko.bindingHandlers.tinymce = {
init: function (element, valueAccessor, allBindingsAccessor, context) {
var options = allBindingsAccessor().tinymceOptions || {};
var modelValue = valueAccessor();
var value = ko.utils.unwrapObservable(valueAccessor());
var el = $(element)
//handle edits made in the editor. Updates after an undo point is reached.
options.setup = function (ed) {
console.log(ed)
ed.onChange.add(function (ed, l) {
if (ko.isWriteableObservable(modelValue)) {
modelValue(l.content);
}
});
};
//handle destroying an editor
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
setTimeout(function () { $(element).tinymce().remove() }, 0)
});
//$(element).tinymce(options);
setTimeout(function () { $(element).tinymce(options); }, 0);
el.html(value);
},
update: function (element, valueAccessor, allBindingsAccessor, context) {
var $element = $(element),
value = ko.utils.unwrapObservable(valueAccessor()),
id = $element.attr('id');
//handle programmatic updates to the observable
// also makes sure it doesn't update it if it's the same.
// otherwise, it will reload the instance, causing the cursor to jump.
if (id !== undefined) {
var tinymceInstance = tinyMCE.get(id);
if (!tinymceInstance)
return;
var content = tinymceInstance.getContent({ format: 'raw' });
if (content !== value) {
$element.val(value);
//this should be more proper but ctr+c, ctr+v is broken, above need fixing
//tinymceInstance.setContent(value,{ format: 'raw' })
}
}
}
};
Depending on the versions of TinyMCE and jQuery that you are dependent on, you might like to try the custom binding I've recently rolled myself.
It's available on GitHub and NuGet
I found the issue. What happened was this:
you select Textbatch #1 and make some changes
you switch to Textbatch #2
the binding changes correctly from Textbatch #1 to #2
THEN the ed.onChange.add event handler kicks in and overwrites the content of the previous Textbatch #1 with that of the new Textbatch #2
Take a look at this updated fiddle (remove /show/light from the URL to get back to the editor). I had to inline select2.js, because Github does not allow files it hosts to be included remotely, causing your fiddle to fail.
The important part is in ko.utils.domNodeDisposal.addDisposeCallback:
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).tinymce().onChange.remove(changeHandler);
setTimeout(function () { $(element).tinymce().remove() }, 0)
});
I do not know why the removal of the editor is minimally delayed with a 0-second timeout, but there is probably a good reason for that. So all we do is remove the 'change' handler, so that the old editor cannot update the bound valueAccessor in your viewmodel anymore.
EDIT: I just noticed that I fixed your fiddle, but not necessarily your original exception… here is to hoping that the two were related.
i was trying to organize my jquery code so i created an object literal, but now the focusTextArea is not working and my textarea value is not updating.
Thanks for your help.
html
<textarea id="test"></textarea>
javascript
(function($,window,document,undefined){
var TEX = {
inputField: $("textarea#test"),
/* Init all functions */
init: function()
{
this.focusTextArea();
},
/* Function update textarea */
focusTextArea: function()
{
this.inputField.text('test');
},
}
$(document).ready(function(){
TEX.init();
});
})(jQuery,window,document);
jsfiddle
http://jsfiddle.net/vBvZ8/1/
First of all, you haven't included jQuery correctly in the fiddle. Also, I think you mean to place the code in the head of the document (because of the document.ready handler).
More importantly perhaps the selector $("textarea#test") is run before the document is ready and therefore won't actually find the element correctly. I would recommend assigning inputField in TEX.init:
(function($,window,document,undefined){
var TEX = {
/* Init all functions */
init: function()
{
this.inputField = $("#test");
this.focusTextArea();
},
/* Function update textarea */
focusTextArea: function()
{
this.inputField.text('test');
},
}
$(document).ready(function(){
TEX.init();
});
})(jQuery,window,document);
Updated example: http://jsfiddle.net/xntA2/1/
As a side note, textarea#test should be changed to just #test. The textarea bit is superfluous since there should be only one element on the page with id=test.
Alternative syntax to avoid looking for an element before it exists is to return the element from a function:
(function($,window,document,undefined){
var TEX = {
/* function won't look for element until called*/
inputField:function(){
return $("textarea#test")
},
init: function()
{
this.focusTextArea();
},
focusTextArea: function()
{
this.inputField().text('test');
},
}
$(document).ready(function(){
TEX.init();
});
})(jQuery,window,document);
DEMO: http://jsfiddle.net/vBvZ8/5/
I realize this is a simplified example...but you are also very close to creating a jQuery plugin and that may also be of benefit. Following provides same functionality as example:
(function($, window, document, undefined) {
$.fn.focusTextArea = function() {
return this.each(function(){
$(this).text('test');
})
};
})(jQuery, window, document);
$(function() {
$('textarea').focusTextArea()
});
DEMO: http://jsfiddle.net/vBvZ8/8/
How is it possible to make knockout data-bind work on dynamically generated elements? For example, I insert a simple html select menu inside a div and want to populate options using the knockout options binding. This is what my code looks like:
$('#menu').html('<select name="list" data-bind="options: listItems"></select>');
but this method doesn't work. Any ideas?
If you add this element on the fly after you have bound your viewmodel it will not be in the viewmodel and won't update. You can do one of two things.
Add the element to the DOM and re-bind it by calling ko.applyBindings(); again
OR add the list to the DOM from the beginning and leave the options collection in your viewmodel empty. Knockout won't render it until you add elements to options on the fly later.
Knockout 3.3
ko.bindingHandlers.htmlWithBinding = {
'init': function() {
return { 'controlsDescendantBindings': true };
},
'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
element.innerHTML = valueAccessor();
ko.applyBindingsToDescendants(bindingContext, element);
}
};
Above code snippet allows you to inject html elements dynamically with the "htmlWithBinding" property. The child elements which are added are then also evaluated... i.e. their data-bind attributes.
rewrite html binding code or create a new. Because html binding prevents "injected bindings" in dynamical html:
ko.bindingHandlers['html'] = {
//'init': function() {
// return { 'controlsDescendantBindings': true }; // this line prevents parse "injected binding"
//},
'update': function (element, valueAccessor) {
// setHtml will unwrap the value if needed
ko.utils.setHtml(element, valueAccessor());
}
};
For v3.4.0 use the custom binding below:
ko.bindingHandlers['dynamicHtml'] = {
'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// setHtml will unwrap the value if needed
ko.utils.setHtml(element, valueAccessor());
ko.applyBindingsToDescendants(bindingContext, element);
}
};
EDIT: It seems that this doesn't work since version 2.3 IIRC as pointed by LosManos
You can add another observable to your view model using myViewModel[newObservable] = ko.observable('')
After that, call again to ko.applyBindings.
Here is a simple page where I add paragraphs dynamically and the new view model and the bindings work flawlessly.
// myViewModel starts only with one observable
var myViewModel = {
paragraph0: ko.observable('First')
};
var count = 0;
$(document).ready(function() {
ko.applyBindings(myViewModel);
$('#add').click(function() {
// Add a new paragraph and make the binding
addParagraph();
// Re-apply!
ko.applyBindings(myViewModel);
return false;
});
});
function addParagraph() {
count++;
var newObservableName = 'paragraph' + count;
$('<p data-bind="text: ' + newObservableName + '"></p>').appendTo('#placeholder');
// Here is where the magic happens
myViewModel[newObservableName] = ko.observable('');
myViewModel[newObservableName](Math.random());
// You can also test it in the console typing
// myViewModel.paragraphXXX('a random text')
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js"></script>
<div id="placeholder">
<p data-bind="text: paragraph0"></p>
</div>
<a id="add" href="#">Add paragraph</a>
It's an old question but here's my hopefully up-to-date answer (knockout 3.3.0):
When using knockout templates or custom components to add elements to prebound observable collections, knockout will bind everything automatically. Your example looks like an observable collection of menu items would do the job out of the box.
Based on this existing answer, I've achived something similar to your initial intentions:
function extendBinding(ko, container, viewModel) {
ko.applyBindings(viewModel, container.children()[container.children().length - 1]);
}
function yourBindingFunction() {
var container = $("#menu");
var inner = $("<select name='list' data-bind='options: listItems'></select>");
container.empty().append(inner);
extendBinding(ko, container, {
listItems: ["item1", "item2", "item3"]
});
}
Here is a JSFiddle to play with.
Be warned, once the new element is part of the dom, you cannot re-bind it with a call to ko.applyBindings- that is why I use container.empty(). If you need to preserve the new element and make it change as the view model changes, pass an observable to the viewModel parameter of the extendBinding method.
Checkout this answer: How do define a custom knockout 'options binding' with predefined Text and Value options
ko.applyBindingsToNode is particularly useful.