Can't bind or render Rich Text from REST with Knockout - javascript

I'm getting data back and it does render Title, but can't seem to render rich text.
markup:
<div id="bodyarea">
<div data-bind=foreach:list>
<span data-bind="text:Title" />
<div data-bind="html: RichData"></div>
</div>
</div>
<p id="myarea"></p>
ko:
function LoadLists() {
var listItems = [];
var count = 0;
$.getJSON("https://myserver.com/sites/knockout/_api/lists/getbytitle('List%20One')/items?$filter=Title eq
'zzzz'",
function (data, textstatus, jqXHR) {
$(data.value).each(function (index, item) {
count++;
var koItem = {};
koItem.Title = item.Title;
koItem.RichData = item.Rich;
listItems.push(koItem);
if (data.value.length == count) {
var vm =
{
list: ko.observableArray(listItems)
};
ko.applyBindings(vm, document.getElementById("bodyarea"));
}
})
});
}
$(document).ready(function () { LoadLists(); });

In general, with knockout, you should not:
Call applyBindings more than once
Call applyBindings inside an ajax call
Create a viewModel as a plain object (usually, sometimes it's alright)
Do knockout databinding inside jQuery event handlers.
Code:
// this is a reusable view model for each item. it takes a raw item from the ajax return, and creates observables for each property.
var ListItem = function (item) {
var self = this;
self.Title = ko.observable(item.Title);
self.RichData = ko.observable(item.Rich);
}
// Your viewModel. Constructor-esque syntax is pretty standard.
var ViewModel = function () {
var self = this;
// this is your list array.
self.list = ko.observableArray();
// This is a your reusable function to load lists, when it returns, it maps each item
// in data.value to a ListItem viewModel and puts them all in the lists observableArray
self.loadList = function() {
$.getJSON('yourUrl', function(data) {
var items = data.value.map(function(item) { return new ListItem(item); });
self.list(items);
}
};
};
// When the document is ready, create a view model and apply bindings once. Then call loaLists to initialize
$(document).ready(function () {
var vm = new ViewModel();
ko.applyBindings(vm);
vm.loadList();
});

Related

KnockoutJs call function in other viewmodel of applyBindings

On a page I'm calling ko.applyBindings twice to iniate 2 view models. When viewModelOne saves successfully, I want to reload the other view model as some data is added in the backend as they are loosely linked.
Now I'm trying to call viewModelTwo.reloadData in saveSuccess() but I keep getting the error that it can't find the function whatever I try.
(Uncaught TypeError: viewModelTwo.reloadData is not a function)
What is the correct way of calling a function from the other viewmodel in KnockoutJs? Could anyone point me in the right direction?
var viewModelOne = (function () {
function reloadData(url) {
...
}
function saveSuccess(){
viewModelTwo.reloadData('');
}
});
var viewModelTwo = (function () {
function reloadData(url) {
...
}
});
ko.applyBindings(viewModelOne, document.getElementById("modelOneContainer"));
ko.applyBindings(viewModelTwo, document.getElementById("modelTwoContainer"));
You could use a constructor function:
function ViewModelOne() {
var vm = this;
vm.reloadData = function() {
console.log('vm1 reloaddata');
}
}
var vm1 = new ViewModelOne();
function ViewModelTwo() {
var vm = this;
vm.reloadData = function() {
vm1.reloadData();
console.log('vm2 reloaddata');
}
}
var vm2 = new ViewModelTwo();
ko.applyBindings(vm1, document.getElementById("modelOneContainer"));
ko.applyBindings(vm2, document.getElementById("modelTwoContainer"));
vm2.reloadData();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div id="modelOneContainer"></div>
<div id="modelTwoContainer"></div>

Observable is changed only for one viewModel

I have collection of Tags (array of some string)
For each of Tags I create knockout viewModel TagsViewModel
var TagsViewModel = function() {
var vm = this;
vm.showTags = ko.observable(false);
window.shouter.subscribe(function(newValue) {
vm.showTags(newValue);
}, vm, 'toggleReviewTags');
}
And I have another "toggler" to show/hide tags in another partial view. For it I've created separate viewModel TagFiltersViewModel and use knockout pubSub to communicate with TagsViewModel
var TagFiltersViewModel = function() {
var vm = this;
vm.isTagsVisible = ko.observable(false);
vm.isTagsVisible.subscribe(function(newValue) {
window.shouter.notifySubscribers(newValue, 'toggleReviewTags');
});
vm.toggleTags = function() {
vm.isTagsVisible(!vm.isTagsVisible());
}
}
Each TagsViewModel I bind to container with calculated id "tag-container-"+ {tagId}
and for each do next thing
var element = document.getElementById(tagModel.tagsContainer);
ko.cleanNode(element);
ko.applyBindings(new TagsViewModel(tagModel), element);
Problem - locally only one tag from collection is shown after click on toggle button. I have created jsFiddle, but there I can't reproduce my problem.
Any thoughts what is the problem in my case?
I would suggest doing something like the following, it should make it much easier to manage.
There may be a specific reason you are binding each tag seperately using theapplyBindings method but you'll have to elaborate on that.
var arrayOfTags = ['tag1', 'tag2', 'tag3'];
var ViewModel = function() {
var self = this;
// Return an array TagsViewModels using the map method
this.Tags = ko.observableArray(arrayOfTags.map(function(tag) {
return new TagsViewModel(tag);
}));
// Observable to track if all tags are hidden/shown
this.TagsVisible = ko.observable(true);
// Loop through tags, show and set flag to true
this.ShowTags = function() {
self.Tags().forEach(function(tag) {
tag.Visible(true);
})
self.TagsVisible(true);
};
// Loop through tags, hide and set flag to false
this.HideTags = function() {
self.Tags().forEach(function(tag) {
tag.Visible(false);
})
self.TagsVisible(false);
};
};
var TagsViewModel = function(name) {
this.Name = ko.observable(name)
this.Visible = ko.observable(true);
};
var model = new ViewModel();
ko.applyBindings(model);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="click: ShowTags, visible: !TagsVisible()">Show Tags</button>
<button data-bind="click: HideTags, visible: TagsVisible">Hide Tags</button>
<hr>
<!-- ko if: Tags() -->
<!-- ko foreach: Tags -->
<span data-bind="text: Name, visible: Visible"></span>
<!-- /ko -->
<!-- /ko -->

Expand one element and collapse all other element in knockout js

In knockout is it possible to collapse all other opened row and expand only clicked row.
I am referring this Fiddle example for it.
view -
<ul data-bind="foreach: items">
<button data-bind="text:name"></button>
<div data-bind="visible:expanded">
<input data-bind="value:name"></input>
</div>
</ul>
viewModel -
function Sample(item) {
var self = this;
self.name = ko.observable(item.name);
self.id = ko.observable(item.id);
self.expanded = ko.observable(false);
self.toggle = function (item) {
self.expanded(!self.expanded());
};
self.linkLabel = ko.computed(function () {
return self.expanded() ? "collapse" : "expand";
}, self);
}
var viewModel = function () {
var self = this;
var json = [{
"name": "bruce",
"id": 1
}, {
"name": "greg",
"id": 2
}]
var data = ko.utils.arrayMap(json, function (item) {
return new Sample(item); // making things independent here
});
self.items = ko.observableArray(data);
};
ko.applyBindings(new viewModel());
Here its not collapsing already opened row. I tried to fetch complete items in toggle function but it did not work.
I am new to knock out. please suggest.
Update -
I tried this code to make first one extended by default -
var index=0;
var data = ko.utils.arrayMap(json, function(item) {
if(index++===0){
return new Sample(item,true);
}else{
return new Sample(item,false);
}
});
But above given code is not working as expected.
This is very common "problem" when you're working with knockout. You want to keep your Sample instances independent, while their behavior might still influence the behavior of any siblings... I usually pick one of three options:
Move the functionality that influences siblings to the parent viewmodel. For example:
var viewModel = function() {
/* ... */
self.toggle = function(sample) {
self.items().forEach(function(candidateSample) {
candidateSample.expanded(sample === candidateSample);
});
}
};
With data-bind:
<a data-bind="click: $parent.toggle"></a>
Personally, I'd go with this option. Here's it implemented in your fiddle: http://jsfiddle.net/cxzLsz56/
Pass siblings to each item:
self.items = ko.observableArray();
var data = ko.utils.arrayMap(json, function (item) {
return new Sample(item, self.items);
});
self.items(data);
And in Sample:
function Sample(item, siblings) {
self.toggle = function() {
siblings().forEach(/* collapse */);
self.expanded(true); // Expand
};
};
Create some sort of postbox/eventhub/mediator mechanism and make a Sample trigger an event. Each Sample listens to this event and collapses when another Sample triggers it.

knockout.js 3.3 - rerendering component in foreach binding

My viewModel consist of observable array with observable elements.
// viewmodel
var viewModel = function () {
this.o = ko.observableArray();
for (var i = 0; i < 3; i++)
this.o.push(ko.observable(0));
};
I need to change the values of these elements. And for this purpose I create the component. Simple example of it is below:
//custom element <component>
ko.components.register("component", {
viewModel: function (params) {
var self = this;
this.value = params.value;
console.log("init component");
this.i = 1;
this.change = function () {
self.value(self.i++);
console.log("change to " + self.value());
}
},
template: "<span data-bind='text: value'></span> <button data-bind='click:change'>Change</button>"
});
This component can change value of observable element which come in params.value.
My view is very simple:
<!--ko foreach:o-->
<component params="value: $rawData"></component>
<!--/ko-->
Full example: http://jsfiddle.net/tselofan/xg16u5cg/7/
Problem is when value of observable element in observable array is changed, component is rendered again, because it is located inside foreach binding. You can see this in logs. What the best practice can I use in this situation? Thank you
The component is being recreated each time the number changes because the context of the component is the number.
http://jsfiddle.net/Crimson/xg16u5cg/8/
<!-- ko foreach: o -->
<component params="value: $data.myNumber"></component>
<!-- /ko -->
//Test how components work in foreach binding
//custom element <component>
ko.components.register("component", {
viewModel: function (params) {
var self = this;
this.value = params.value;
console.log("init component");
this.i = 1;
this.change = function () {
self.value(self.i++);
console.log("change to " + self.value());
}
},
template: "<span data-bind='text: value'></span> <button data-bind='click:change'>Change</button>"
});
// viewmodel
var viewModel = function () {
this.o = ko.observableArray();
for (var i = 0; i < 3; i++)
this.o.push({myNumber: ko.observable(0)});
};
ko.applyBindings(new viewModel());
Knockout-Repeat (https://github.com/mbest/knockout-repeat) is an iterative binding that does not create a new binding context, and has a foreach mode, so it should work as you expect with your component.

Change Views Content based on different modules event

My content box module enumerates a collection and creates a container view for each item ( passing the model to the view). It sets the initial content to the content property of its model. Base on a layout property in the model the container view is attached to the DOM. This is kicked off by the “_contentBoxCreate” method.
The content box module responds to clicks to sub items in a sidemenu. The sidemenu is implemented in a different module. The sidemenu sub click event passes an object along as well that contains a sub_id and some text content. I want to take the content from this object and use it to update container view(s).
Currently I’m doing this via the “_sideMenuClick” method. In backbonejs is there a best practice for updating a views content, given that no data was changed on its model?
thanks,
W.L.
APP.module("contentbox", function(contentbox) {
//Model
var Contentbox = Backbone.Model.extend({});
//Collection
var Contentboxes = Backbone.Collection.extend({
model: Contentbox,
url: 'ajax/contentboxResponse/tojson'
});
/*
* View:
*/
var Container = Backbone.View.extend({
initialize: function() {
contentbox.on('update', jQuery.proxy(this.update, this));
contentbox.on('refresh', jQuery.proxy(this.render, this));
var TemplateCache = Backbone.Marionette.TemplateCache;
this.template = TemplateCache.get("#contentbox-container");
},
render: function() {
var content = this.model.get('content').toString();
var html = this.template({content: content});
this.$el.html(html);//backbone element
return this;
},
update: function(fn) {
var content = fn.apply(this);
if (content !== null) {
var html = this.template({content: content});
this.$el.html(html);
}
}
});
//collection
var contentboxes = new Contentboxes();
var _sideMenuToggle = function() {
contentbox.trigger('refresh');
};
var _sideMenuClick = function(sideMenu) { //view contex
var fn = function() {
// this fn will have the context of the view!!
var linksub = this.model.get('linksub').toString();
if (linksub === sideMenu.id.toString()) {
return sideMenu.content.toString();
}
return null;
};
contentbox.trigger('update', fn);
};
var _contentBoxCreate = function() {
var create = function(cboxes) {
cboxes.each(function(model) {
var layout = "#body-" + model.get('layout');
var $el = jQuery(layout);
var container = new Container({model: model});
$el.append(container.render().$el);
});
};
contentboxes.fetch({
success: create
});
};
this.on("start", function() {
_contentBoxCreate();
});
this.addInitializer(function() {
APP.vent.on('sidemenu:toggle', _sideMenuToggle);
APP.reqres.setHandler('sidemenu:submenu', _sideMenuClick);//event and content...
//from another module
});
});
UPDATE:
Changed the view...
/*
* View
*/
var Container = Backbone.View.extend({
initialize: function() {
this.renderableModel = this.model; // Define renderableModel & set its initial value
contentbox.on('update', this.update, this);
contentbox.on('refresh', this.reset, this); // 3rd param gives context of view
var TemplateCache = Backbone.Marionette.TemplateCache;
this.template = TemplateCache.get("#contentbox-container");
},
render: function() {
var content = this.renderableModel.get('content').toString();
var html = this.template({content: content});
this.$el.html(html);//backbone element
return this;
},
update: function(fn) {
/**
* The "update" event is broadcasted to all Container views on the page.
* We need a way to determine if this is the container we want to update.
* Our criteria is in the fn
*/
var content = fn.apply(this); //criteria match return content, else null.
/*
* The render method knows how to render a contentbox model
*/
if (content !== null) {
this.renderableModel = new Contentbox();
this.renderableModel.set({content: content}); //add content to new contentbox model
this.render(); //Rerender the view
}
},
reset: function() {
this.renderableModel = this.model;
this.render(); // restore view to reflect original model
}
});

Categories

Resources