Is there a way to have transitional animations between "page" changes while using knockout for changing templates? I'm looking for something similar to Knockback-Navigators. I cant figure out a way to do this? Is there a package I can use to make this easier? Here is a JSFiddle with the same type of binding my project uses. And a sample of my javascript here:
var View = function (title, templateName, data) {
var self = this;
this.title = title;
this.templateName = templateName;
this.data = data;
this.url = ko.observable('#' + templateName);
};
var test1View = {
test: ko.observable("TEST1")
};
var test2View = {
test: ko.observable("TEST2")
};
var viewModel = {
views: ko.observableArray([
new View("Test 1", "test1", test1View),
new View("Test 2", "test2", test2View)]),
selectedView: ko.observable(),
}
//Apply knockout bindings
ko.applyBindings(viewModel);
//Set up sammy url routes
Sammy(function () {
//Handles only groups basically
this.get('#:view', function () {
var viewName = this.params.view;
var tempViewObj = ko.utils.arrayFirst(viewModel.views(), function (item) {
return item.templateName === viewName;
});
//set selectedView
viewModel.selectedView(tempViewObj);
});
}).run('#test1');
There are plenty of ways of doing this, here is one
ko.bindingHandlers.withFade = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var $element = $(element);
var observable = valueAccessor();
var wrapper = ko.observable(observable());
observable.subscribe(function(value) {
var current = wrapper();
fadeIn = function() {
wrapper(value);
$element.fadeIn();
};
if(current) {
$element.fadeOut(fadeIn);
} else {
$element.hide();
fadeIn();
}
});
ko.applyBindingsToNode(element, { with: wrapper }, bindingContext);
return { controlsDescendantBindings: true };
}
};
http://jsfiddle.net/7E84t/19/
You can abstract the effect like this
ko.transitions = {
fade: {
out: function(element, callback) {
element.fadeOut(callback);
},
in: function(element) {
element.fadeIn();
}
},
slide: {
out: function(element, callback) {
element.slideUp(callback);
},
in: function(element) {
element.slideDown();
}
}
};
html
<div data-bind="withFade: { data: selectedView, transition: ko.transitions.slide }">
http://jsfiddle.net/7E84t/23/
Related
I'm trying to map a nested ViewModel (three levels of depth) with Knockout's mapping plugin. When running this code, only the first Level will be mapped correctly. What am I doing wrong here?
Thanks in advance
Here is my Code:
var mapping = {
create: function (options) {
var levelOneItems = new levelOneModel(options.data)
//Some computed observables for level one here...
return levelOneItems;
},
'levelTwoItemList': {
create: function (options) {
var levelTwoItems = new levelTwoModel(options.data)
//Some computed observables for level two here...
return levelTwoItems;
},
'levelThreeItemList': {
create: function (options) {
var levelThreeItems = new levelThreeModel(options.data)
//Some computed observables for level three here...
return levelThreeItems;
}
}
}
}
var levelOneModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelTwoModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelThreeModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var data = [
{
'LevelOneName': 'Apple1',
'levelTwoItemList': [
{
'LevelTwoName': 'Apple2.1',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.1' },
{ 'LevelThreeItemName': 'Apple3.2' }
]
}, {
'LevelTwoName': 'Apple2.2',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.3' },
{ 'LevelThreeItemName': 'Apple3.4' }
]
},
]
}
]
var viewModel = ko.mapping.fromJS(data, mapping);
I've just figured it out myself while playing around with this objects. I hope this helps someone who got into the same trouble.
Here the code:
var mapping1 = {
create: function (options) {
var levelOneItems = ko.mapping.fromJS(options.data, mapping2)
//Some computed observables for level one here...
return levelOneItems;
}
}
var mapping2 = {
'levelTwoItemList': {
create: function (options) {
var levelTwoItems = ko.mapping.fromJS(options.data, mapping3)
//Some computed observables for level two here...
return levelTwoItems;
}
}
}
var mapping3 = {
'levelThreeItemList': {
create: function (options) {
var levelThreeItems = new levelThreeModel(options.data)
//Some computed observables for level three here...
return levelThreeItems;
}
}
}
var levelOneModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelTwoModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelThreeModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var data = [
{
'LevelOneName': 'Apple1',
'levelTwoItemList': [
{
'LevelTwoName': 'Apple2.1',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.1' },
{ 'LevelThreeItemName': 'Apple3.2' }
]
}, {
'LevelTwoName': 'Apple2.2',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.3' },
{ 'LevelThreeItemName': 'Apple3.4' }
]
},
]
}
]
var viewModel = ko.mapping.fromJS(data, mapping1)
I know Im pretty close to figuring this out. Im trying to filter out my collection based on if favorite eq true. If I console.log - I can see it's doing its job. But it's not updating my view.
Anyone have any idea what I'm missing or doing wrong?
Here is my code:
var Products = Backbone.Model.extend({
// Set default values.
defaults: {
favorite: false
}
});
var ProductListCollection = Backbone.Collection.extend({
model: Products,
url: '/js/data/wine_list.json',
parse: function(data) {
return data;
},
comparator: function(products) {
return products.get('Vintage');
},
favoritesFilter1: function(favorite) {
return this.filter(function(products) {
return products.get('favorite') == true;
});
},
favoritesFilter: function() {
return this.filter(function(products) {
return products.get('favorite') == true;
});
},
});
var products = new ProductListCollection();
var ProductListItemView = Backbone.View.extend({
el: '#wine-cellar-list',
initialize: function() {
products.bind('reset', this.render, this);
products.fetch();
this.render();
},
render: function() {
console.log(this.collection);
var source = $('#product-template').html();
var template = Handlebars.compile(source);
var html = template(this.collection.toJSON());
this.$el.html(html);
return this;
},
});
// Create instances of the views
var productView = new ProductListItemView({
collection: products
});
var CellarRouter = Backbone.Router.extend({
routes: {
'': 'default',
"favorites": "showFavorites",
"purchased": "showPurchased",
"top-rated": "showTopRated",
},
default: function() {
productView.render();
},
showFavorites: function() {
console.log('Favorites');
productView.initialize(products.favoritesFilter());
},
showPurchased: function() {
console.log('Purchased');
},
showTopRated: function() {
console.log('Top Rated');
}
});
$(function() {
var myCellarRouter = new CellarRouter();
Backbone.history.start();
});
There's many mistakes in your code, I'll try to clarify the most I can :
Your collection should be just like this :
var ProductListCollection = Backbone.Collection.extend({
model: Products,
url: '/js/data/wine_list.json',
comparator: 'Vintage' // I guess you want to sort by this field
});
Your view like this :
var ProductListItemView = Backbone.View.extend({
el: '#wine-cellar-list',
initialize: function() {
this.collection.bind('reset', this.full, this);
this.collection.fetch();
},
full: function() {
this.render(this.collection.models);
},
favorites: function(favorite) {
this.render(this.collection.where(favorite)); // here's the answer to your question
},
render: function(models) {
console.log(models);
var source = $('#product-template').html();
var template = Handlebars.compile(source);
var html = template(models.toJSON()); // You may have to change this line
this.$el.html(html);
return this;
},
});
And in your router :
showFavorites: function() {
console.log('Favorites');
productView.favorites(true); // or false, as you like
}
I am trying to create a knockout template for my text count tracker so that I can repeat this for each text area on the page I am creating.
I have created two fiddles that demonstrate different approaches I have been taking:
1. http://jsfiddle.net/ajdisalvo/9W84x/3/ and
2. http://jsfiddle.net/ajdisalvo/ex2uJ/
In both, I am have created a bindinghandler that is a wrapper for the template that I am attempting to bind to.
In the first approach I create a separate viewModel, attach it to the existing main viewModel and it seems to behave until it attempts to update the value 'successes' in the main viewModel. At this point, it sets the value to [Object object]. Although, this fiddle seems to be really close to working, I am concerned that I could be creating a recursive loop that might be an inherent flaw to this approach.
(function (ko) {
ko.bindingHandlers.templateWrapper = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
var existingOptions = ko.utils.unwrapObservable(valueAccessor());
viewModel[existingOptions.itemName] = new subModel(existingOptions.dataInput(), existingOptions.maxLength);
viewModel[existingOptions.itemName].itemText.subscribe(function (value) {
existingOptions.dataInput(value);
});
newOptions = ko.bindingHandlers.templateWrapper.buildTemplateOptions(existingOptions, viewModel[existingOptions.itemName]);
//return ko.bindingHandlers.template.init(element, function () { return newOptions; }, allBindingsAccessor, viewModel, context);
return { controlsDescendantBindings: true };
},
'update': function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
newOptions = ko.bindingHandlers.templateWrapper.buildTemplateOptions(valueAccessor(), viewModel[valueAccessor().itemName]);
ko.bindingHandlers.template.update(element, function () { return newOptions; }, allBindingsAccessor, viewModel, context);
},
//extend the existing options by adding a name
buildTemplateOptions: function (options, vm) {
return { data: vm, name: ko.bindingHandlers.templateWrapper.templateName };
},
templateName: "textTrackerTemplate"
};
var viewModel = {
successes: ko.observable("Test input")
};
var subModel = function (value, maxLength) {
var self = this;
self.itemText = ko.observable(value);
self.maxLength = ko.observable(maxLength);
self.currentLength = ko.observable(self.itemText().length);
self.remainingLength = ko.computed(function () { return self.maxLength() - self.currentLength() });
self.hasFocus = ko.observable(false);
self.updateRemaining = function (data, event) {
var e = $(event.target || event.srcElement);
self.currentLength(e.val().length);
if (self.currentLength() > self.maxLength()) {
e.val(e.val().substr(0, self.maxLength()));
self.ItemText(e.val());
self.currentLength(self.itemText().length);
}
};
};
ko.applyBindings(viewModel);
} (ko));
In my second approach, I am using an extender in my bindinghandler to add the necessary properties to populate my count tracker, but it seems that the objects created in the extender are not instantiated at the time the knockout renders the page.
(function (ko) {
ko.bindingHandlers.templateWrapper = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
var existingOptions = ko.utils.unwrapObservable(valueAccessor());
newOptions = ko.bindingHandlers.templateWrapper.buildTemplateOptions(existingOptions, valueAccessor);
return ko.bindingHandlers.template.init(element, function () { return newOptions; }, allBindingsAccessor, viewModel, context);
},
'update': function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
var existingOptions = ko.utils.unwrapObservable(valueAccessor());
newOptions = ko.bindingHandlers.templateWrapper.buildTemplateOptions(existingOptions, valueAccessor);
ko.bindingHandlers.template.update(element, function () { return newOptions; }, allBindingsAccessor, viewModel, context);
},
buildTemplateOptions: function (options, valueAccessor) {
valueAccessor().dataInput = valueAccessor().dataInput.extend({ textTracker: options.maxLength });
return { data: valueAccessor, name: ko.bindingHandlers.templateWrapper.templateName };
},
templateName: "textTrackerTemplate"
};
var viewModel = {
successes: ko.observable("Test input")
};
ko.extenders.textTracker = function (target, maxLength) {
target.itemText = ko.computed({
read: function () {
return target();
},
write: function (value) {
target(value);
}
});
target.maxLength = ko.observable(maxLength);
target.currentLength = ko.observable(target.itemText().length);
target.remainingLength = ko.computed(function () {
return target.maxLength() - target.currentLength(); });
target.hasFocus = ko.observable(false);
target.updateRemaining = function (data, event) {
var e = $(event.target || event.srcElement);
target.currentLength(e.val().length);
if (target.currentLength() > target.maxLength()) {
e.val(e.val().substr(0, target.maxLength()));
target(e.val());
target.currentLength(target.itemText().length);
}
};
return target;
};
ko.applyBindings(viewModel);
} (ko));
Thanks in advance for any help/suggestions that you might provide..
I figured it out. I knew there were issues with my bindinghandler. I was actually making my binding more complicated that it needed to be. I just needed to extend the the value accessor only in the init method and then pass it on to the template init binding method. In the update statement, I just needed to use the existing value accessor, since it had already been extended. Here is the new binding handler:
ko.bindingHandlers.textTracker = {
init: function (element, valueAccessor, allBindings, viewModel, context) {
var options = ko.utils.unwrapObservable(allBindings());
var observable = valueAccessor();
var newValueAccessor = observable.extend({ textTracker: options });
return ko.bindingHandlers.template.init(element,
function () {
return { data: newValueAccessor,
name: ko.bindingHandlers.textTracker.templateName
};
},
allBindings, viewModel, context);
},
update: function (element, valueAccessor, allBindings, viewModel, context) {
return ko.bindingHandlers.template.update(element,
function () {
return { data: valueAccessor,
name: ko.bindingHandlers.textTracker.templateName
};
},
allBindings, viewModel, context);
},
templateName: "textTrackerTemplate"
};
Special thanks to RPNiemeyer's answer to 19732545 which helped me re-look at what I was doing which helped helped simplify the problem.
When a new model is added (via "set" function of the collection), I want the model be inserted at the index maintaining sort order, instead at the end.
Thanks
var Ts = (function () {
var Result = Backbone.Model.extend({
idAttribute : 'PAG_ID'
});
var ResultList = Backbone.Collection.extend({
model: Result,
comparator: function(result) {
//console.log(result);
return result.get('SORT_KEY');
},
});
var resultsCollection = new ResultList(data);
data = undefined;
var TableView = Backbone.View.extend({
tagName: 'table',
initialize : function() {
_.bindAll(this, 'render', 'renderRow');
this.collection.on("add", this.renderRow, this);
},
render: function() {
$(this.el).attr('id', 'tsTable').addClass('resulttable');
this.renderHeader(this.collection.shift());
this.collection.each(this.renderRow);
return this;
},
renderHeader : function(model) {
var col=new HeaderView({model:model});
this.$el.append(col.render().$el);
return this;
},
renderRow : function(model) {
var row=new RowView({model:model});
this.$el.append(row.render().$el);
return this;
}
});
var HeaderView = Backbone.View.extend({
tagName: 'tr',
model: resultsCollection.models,
initialize: function() {
this.model.on('change',this.render,this);
},
render: function() {
var html=_.template(colTemplate,this.model.toJSON());
this.$el.html(html);
return this;
}
});
var RowView = Backbone.View.extend({
tagName: 'tr',
initialize: function() {
this.model.on('all',this.render,this);
},
remove: function () {
debug.log("Called remove event on model");
$(this.el).remove();
},
model: resultsCollection.models,
render: function() {
var html=_.template(rowTemplate,this.model.toJSON());
this.$el.html(html);
return this;
},
attributes : function () {
return {
id : this.model.get('PAG_ID')
};
}
});
var tableView = new TableView({collection: resultsCollection});
$("body").append( tableView.render().$el );
resultsCollection.set(initialdata);
resultsCollection.set(someotherdata, {merge: true});
I have changed to as below and it works.Not sure if this is the best implementation
renderRow : function(model) {
var row = new RowView({model:model});
var index = model.get('SORT_KEY') - 1;
row.render().$el.insertAfter(this.$el.find('tr:eq('+ index +')'));
return this;
}
If you provide a comparator function on your collection, Collection.set will perform a silent sort after the new models have been spliced in.
From backbones source http://backbonejs.org/docs/backbone.html:
set: function(models, options) {
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
...
if (toAdd.length) {
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
splice.apply(this.models, [at, 0].concat(toAdd));
} else {
push.apply(this.models, toAdd);
}
}
if (sort) this.sort({silent: true});
Here is a fiddle demonstrating that collection.set respects a comparator.
http://jsfiddle.net/puleos/sczV3/
I have a select option that gets its initial value from EmailField and its options from allEmailFields:
<select data-bind="options: $parent.allEmailFields, value: EmailField()"></select>
When I change the value of the select my model doesn't get updated. Isn't this something two way binding should take care of? Or I need to write handler for the change event?
Module is here:
define('mods/fieldmapping', ["knockout", "libs/knockout.mapping", "datacontext", "mods/campaigner", "text!templates/fieldmapping.html", "text!styles/fieldmapping.css"],
function (ko, mapping, datacontext, campaigner, html, css) {
'use strict';
var
fieldMappingItem = function (data) {
var self = this;
self.CrmField = ko.observable(data.CrmField);
self.EmailField = ko.observable(data.EmailField);
},
dataMappingOptions = {
key: function (data) {
return data.PlatformFieldName;
},
create: function (options) {
return new fieldMappingItem(options.data);
}
},
fieldMappingViewModel = {
contentLoading: ko.observable(false)
},
showFieldMapping = function () {
campaigner.addStylesToHead(css);
campaigner.addModalInnerPanel(html);
},
init = function (connectionId, connectionName) {
fieldMappingViewModel.fieldMappings = mapping.fromJS([]);
fieldMappingViewModel.allEmailFields = mapping.fromJS([]);
fieldMappingViewModel.originatorConnectionName = ko.observable();
fieldMappingViewModel.originatorConnectionName(connectionName);
fieldMappingViewModel.saveFieldMappings = function () {
console.log(ko.toJSON(fieldMappingViewModel.fieldMappings));
amplify.request("updateExistingFieldMappings",
{
cid: connectionId,
RequestEntity: ko.toJSON(fieldMappingViewModel.fieldMappings)
},
function (data) {
console.log(data);
});
};
showFieldMapping();
amplify.request('getExistingFieldMappings', { cid: connectionId }, function (data) {
amplify.request("getCampaignerFields", function (breezerData) {
mapping.fromJS(breezerData.ResponseEntity, fieldMappingViewModel.allEmailFields);
});
mapping.fromJS(data.ResponseEntity, dataMappingOptions, fieldMappingViewModel.fieldMappings);
ko.applyBindings(fieldMappingViewModel, $('#fieldMapping')[0]);
});
};
return {
init: init,
fieldMappingViewModel: fieldMappingViewModel,
html: html,
css : css
}
});
Replace:
<select data-bind="options: $parent.allEmailFields, value: EmailField()"></select>
With:
<select data-bind="options: $parent.allEmailFields, value: EmailField"></select>
if you want to create bi-derectinonal dependency, so you should pass into binding observable.
P.S.: http://knockoutjs.com/documentation/observables.html#observables