Dynamic knockout template with transition - javascript

I'm working on a list which is rendered with a template binding. The items have a collapsed and expanded view which is decided by an observable property on the individual items. This is done by providing a function to the template name (just like in the knockout docs). So far so good, everything is well so far.
Now.. to the problem. I want to animate the transition when changing templates. So far I have manage to animate the "In-transition" (with the afterRender event) i.e when the new template is loaded. But I also want to make an "Out-transition" for the old template before it is removed.
This is how far I am now.
http://jsbin.com/UvEraGO/15/edit?html,js,output
Any idea of how I can implement this "out-transition" ?
Here is the code:
[viewmodel.js]
var vm = {
items: [{name: 'John', age:'34', expanded: ko.observable(false)},
{name: 'David', age:'24', expanded: ko.observable(false)},
{name: 'Graham', age:'14', expanded: ko.observable(false)},
{name: 'Elly', age:'31', expanded: ko.observable(true)},
{name: 'Sue', age:'53', expanded: ko.observable(false)},
{name: 'Peter', age:'19', expanded: ko.observable(false)}]
};
vm.myTransition = function(el){
$(el[1]).hide().slideDown(1000);
};
vm.templateSelector = function(item){
return item.expanded() ? 'expanded_template' : 'collapsed_template';
}.bind(vm);
vm.toggleTemplate = function(item){
item.expanded(!item.expanded());
};
ko.applyBindings(vm);
And the html:
<div data-bind="template: { name: templateSelector, foreach: items, afterRender: myTransition }"></div>
<script type="text/html" id="collapsed_template">
<div style="min-height: 30px">
<strong>Name: <span data-bind="text: name"></span></strong>
<button data-bind="click: $parent.toggleTemplate">Expand</button>
<div>
</script>
<script type="text/html" id="expanded_template">
<fieldset style="height: 100px; min-height: 8px">
<legend>
<strong>Name: <span data-bind="text: name"></span></strong>
</legend>
<div>
Age: <span data-bind="text: age"></span>
<button data-bind="click: $parent.toggleTemplate">collapse</button>
</div>
</fieldset>
</script>

A thought would be to create something like a slideTemplate binding and use that inside of your template. It would look something like:
ko.bindingHandlers.slideTemplate = {
init: ko.bindingHandlers.template.init,
update: function(element, valueAccessor, allBindings, data, context) {
//ensure that we have a dependency on the name
var options = ko.unwrap(valueAccessor()),
name = options && typeof options === "object" ? ko.unwrap(options.name) : name,
$el = $(element);
if ($el.html()) {
$el.slideUp(250, function() {
ko.bindingHandlers.template.update(element, valueAccessor, allBindings, data, context);
$el.slideDown(1000);
});
}
else {
ko.bindingHandlers.template.update(element, valueAccessor, allBindings, data, context);
}
}
};
Then, you would bind something like:
<ul data-bind="foreach: items">
<li data-bind="slideTemplate: type">
</li>
</ul>
Sample: http://jsfiddle.net/rniemeyer/6J67k/

Related

checkbox knockout click binding not working properly

I know that knockout expects us to return true in the function bound to click event in order to check/uncheck a checkbox.
I tried the following code but it is not checking the check boxes. I can display the value using an anonymous computed function but my array can be huge and I don't want to keep performance overhead.
Is there any other way of doing it? or Am I doing it wrong?
Edit: Adding the code
HTML
<div data-bind="foreach: array">
<div data-bind="foreach: $data.child">
<input type="checkbox" data-bind="checked: isChecked, click: function(data, event){$parentContext.$parent.clickBox(data, event, $parent)}">
<span data-bind="text: $data.value"></span>
</div>
<p data-bind="text: label"></p>
</div>
JS
var mainModel = function(){
var self = this;
self.isChecked = ko.observable(true);
self.clickBox = function(data, event, $parent){
var j=0;
for(var i=0; i<$parent.length; i++){
if($parent[i].isChecked()){
j++;
}
}
$parent.label(j);
return true;
}
self.array = ko.observable(
[
{child: [
{value: 'a', isChecked: ko.observable(false)},
{value: 'b', isChecked: ko.observable(false)}],
label: ko.observable(0)
},
{child: [
{value: 'c', isChecked: ko.observable(false)},
{value: 'd', isChecked: ko.observable(false)}],
label: ko.observable(0)
}
]);
}
ko.applyBindings(new mainModel());
You are missing return, change following code
<input type="checkbox" data-bind="checked: isChecked,
click: function(data, event){
$parentContext.$parent.clickBox(data, event, $parent)}">
To
<input type="checkbox" data-bind="checked: isChecked,
click: function(data, event){
return $parentContext.$parent.clickBox(data, event, $parent)}">
if you don't pass return handler, by default knockout will prevent the action by calling
if (handlerReturnValue !== true) {
if (event.preventDefault)
event.preventDefault();
else
event.returnValue = false;
}

Knockout bind multiple checkedValue to input

I have custom dropdown (made with using divs and list)
<div class="primary-tags-wrapper">
<div id="primaryTag" class="primary-tags-dropdown ui-dropdown fl">
<div class="fl">
<div class="primary-tag-selected-value" data-bind="text: showPrimaryTag"></div>
</div>
<div class="fr" data-primary="tag">
<div class="fa fa-caret-down"></div>
<ul class="primary-tags-list">
<li class="primary-tags-item">
<input class="primary-tags-item-radio" type="radio" name="primary-tag" id="primary-tag-default" data-bind="checkedValue: null, checked: primaryTag"/>
<label class="primary-tags-item-label" for="primary-tag-default">Set Primary Tag</label>
</li>
<!-- ko foreach: tags -->
<li class="primary-tags-item">
<input class="primary-tags-item-radio" type="radio" name="primary-tag" data-bind="attr: { 'id': 'primary-tag-' + $index() }, checkedValue: $data, checked: $parent.primaryTag"/>
<label class="primary-tags-item-label" data-bind="attr: { 'for': 'primary-tag-' + $index() }, text: $data"></label>
</li>
<!-- /ko -->
<li class="primary-tags-item">
<input type="button" class="btn green-btn" data-bind="click: savePrimaryTag" value="Save"/>
</li>
</ul>
</div>
</div>
</div>
To it I have binded knockout ViewModel
var TagsViewModel = function (inputModel) {
var vm = this;
vm.tags = ko.observableArray(inputModel.tags);
vm.allTags = ko.observableArray(inputModel.allTags);
vm.primaryTag = ko.observable(inputModel.primaryTag);
vm.refreshTags = function () {
var data = vm.tags().slice(0);
vm.tags([]);
vm.tags(data);
};
vm.savePrimaryTag = function() {
var data = {
locationId: inputModel.locationId,
reviewId: inputModel.reviewId,
tag: vm.primaryTag()
};
initializeAjaxLoader();
$.post('/data/reviews/primaryTag',
data,
function(response) {
if (!response.status) {
vm.primaryTag('');
} else {
vm.primaryTag(response.tag);
}
removeAjaxLoader();
});
}
vm.showPrimaryTag = ko.pureComputed(function() {
var primaryTagVal = vm.primaryTag();
if (primaryTagVal) {
return 'Primary Tag: ' + primaryTagVal;
}
return DEFAULT_PRIMARY_TAG;
},
vm);
vm.noPrimaryTagSelected = ko.pureComputed(function() {
var primaryTagVal = vm.primaryTag();
if (primaryTagVal) {
return false;
}
return true;
},
vm);
}
In dropdown I have default option : "Set Primary Tag" which should be selected when primaryTag is null or string.Empty. Currently it is what I can't achive.
So is it possible to set multiple checkedValue to radio button, or there are another way to support this "feature"
When knockout handles the checked binding, it compares primitives using ===. This means, as you've noticed, that a checked value of null doesn't work with "", false, undefined or 0.
If you somehow can't prevent your selected value to be initialized as an empty string, you could bind to a computed layer that "sanitizes" the output.
All of the radio inputs write their value to a computed observable.
The computed observable has a private backing field to store raw input
The read method makes sure all falsey values are returned as null
var VM = function() {
// Notice this can be initialized as any falsey value
// and the checkedValue=null binding will work.
const _selectedTag = ko.observable("");
this.selectedTag = ko.computed({
read: function() {
// Explicitly "cast" all falsey values
// to `null` so it can be handled by
// knockout's `checked` binding:
return _selectedTag() || null;
},
write: _selectedTag
});
this.tags = [
{ label: "one" },
{ label: "two" },
{ label: "three" },
{ label: "four" },
]
};
ko.applyBindings(new VM());
label { display: block }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<label>
<input type="radio" data-bind="checked: selectedTag, checkedValue: null">
Don't use a tag
</label>
<!-- ko foreach: tags -->
<label>
<input type="radio" data-bind="checked: $parent.selectedTag, checkedValue: $data">
<span data-bind="text: label"></span>
</label>
<!-- /ko -->
</div>

Calling a function defined within knockout js ViewModel

I have a html 5 view and want to perform Drag and Drop. In my html I have a knockout foreach as given below. The li element is being dragged on.
<ul class="games-list" data-bind="foreach: games">
<li data-bind="text: name, attr: { 'data-dragdrop': id }"
ondragstart='setTransferProperties(event)'
draggable="true">
</li>
</ul>
and here is the javascript with knockout ViewModel
<script type="text/javascript">
var AppScope = function () {
...//plain js object here
//knockout js View Model
function PlayersViewModel() {
var self = this
self.games = ko.observableArray([
new Game({ id: 0, name: "Cricket" }),
new Game({ id: 1, name: "Football" }),
new Game({ id: 2, name: "Hockey" })
]);
...
//Drag and Drop
self.setTransferProperties = function (event) { //Not invoked
event.dataTransfer.setData("Text", event.target.getAttribute('data-dragdrop'));
};
}
}
With the above the setTransferProperties(event) is looked for in AppScope, instead of inside the knockout ViewModel, and hence not found.
What would be the way to invoke the setTransferProperties(event) defined in the knockout ViewModel when performing the Drag.
You need to do the following
<ul class="games-list" data-bind="foreach: games">
<li data-bind="text: name, attr: {
'data-dragdrop': id ,
'ondragstart':$parent.setTransferProperties
}"
draggable="true">
</li>
</ul>

Ember.js hasMany as list of checkboxes

I have the following two models:
App.Child = DS.Model.extend({
name: DS.attr('string')
});
And:
App.Activity = DS.Model.extend({
children: DS.hasMany('child',{async:true}),
name: DS.attr('string')
});
I want to use checkboxes to choose between the existing children, for the hasMany relation.
For example, I have these three children:
App.Child.FIXTURES = [
{ id: 1, name: 'Brian' },
{ id: 2, name: 'Michael' },
{ id: 3, name: 'James' }
];
The user should be able to use checkboxes, while creating or editing an activity, for choosing which children, to add to the hasMany relation.
I've created a JSFiddle to illustrate my question: http://jsfiddle.net/Dd6Wh/. Click 'Create a new activity' to see what I'm trying to do.
Basically it's the same as Ember.Select [ ... ] multiple="true", but for checkboxes.
What's the correct approach for something like this with Ember.js?
You can use an itemController in your each view helper to manage the selection. In the code below I created one called ChildController:
App.ChildController = Ember.ObjectController.extend({
selected: function() {
var activity = this.get('content');
var children = this.get('parentController.children');
return children.contains(activity);
}.property(),
selectedChanged: function() {
var activity = this.get('content');
var children = this.get('parentController.children');
if (this.get('selected')) {
children.pushObject(activity);
} else {
children.removeObject(activity);
}
}.observes('selected')
});
With a itemController you can expose some properties and logics, without add it directlly to your models. In that case the selected computed property and the selectedChanged observer.
In your template, you can bind the selection using checkedBinding="selected". Because the itemController proxy each model, the selected property of the itemcontroller will be used, and the {{name}} binding, will lookup the name property of the model:
<script type="text/x-handlebars" data-template-name="activities/new">
<h1>Create a new activity</h1>
{{#each childList itemController="child"}}
<label>
{{view Ember.Checkbox checkedBinding="selected"}}
{{name}}
</label><br />
{{/each}}
{{view Ember.TextField valueBinding="name"}}
<button {{action create}}>Create</button>
</script>
The same aproach in edit template:
<script type="text/x-handlebars" data-template-name="activities/edit">
<h1>Edit an activity</h1>
{{#each childList itemController="child"}}
<label>
{{view Ember.Checkbox checkedBinding="selected"}}
{{name}}
</label><br />
{{/each}}
{{view Ember.TextField valueBinding="name"}}
<button {{action update}}>Update</button>
</script>
This is a fiddle with this working http://jsfiddle.net/marciojunior/8EjRk/
Component version
Template
<script type="text/x-handlebars" data-template-name="components/checkbox-select">
{{#each elements itemController="checkboxItem"}}
<label>
{{view Ember.Checkbox checkedBinding="selected"}}
{{label}}
</label><br />
{{/each}}
</script>
Javascript
App.CheckboxSelectComponent = Ember.Component.extend({
/* The property to be used as label */
labelPath: null,
/* The model */
model: null,
/* The has many property from the model */
propertyPath: null,
/* All possible elements, to be selected */
elements: null,
elementsOfProperty: function() {
return this.get('model.' + this.get('propertyPath'));
}.property()
});
App.CheckboxItemController = Ember.ObjectController.extend({
selected: function() {
var activity = this.get('content');
var children = this.get('parentController.elementsOfProperty');
return children.contains(activity);
}.property(),
label: function() {
return this.get('model.' + this.get('parentController.labelPath'));
}.property(),
selectedChanged: function() {
var activity = this.get('content');
var children = this.get('parentController.elementsOfProperty');
if (this.get('selected')) {
children.pushObject(activity);
} else {
children.removeObject(activity);
}
}.observes('selected')
});
Updated fiddle http://jsfiddle.net/mgLr8/14/
I hope it helps

Passing options to templates in knockout 1.3

In knockoutjs 1.2.1 I could do:
<div data-bind="template: {name: 'Bar', foreach: persons, templateOptions:{fooMode: true} }"/>
<script id='Bar'>
{{if $item.fooMode}} FOO! {{/if}}
</script>
Which I have tried to translate to knockout 1.3.0beta as
<div data-bind="template: {name: 'Bar', foreach: persons, templateOptions:{fooMode: true} }"/>
<script id='Bar'>
<span data-bind="if: $item.fooMode">FOO!</span>
</script>
But the new native template engine doesn't respect templateOptions.
Is there some other way I can pass arbitrary data into a template?
As you discovered, the native template engine does not support templateOptions which was a wrapper to the jQuery Template plug-in's options functionality.
Two ways that you could go:
Place your data on your view model and use $root.fooMode or $parent.fooMode inside of your template. This would be the easiest option.
Otherwise, if you don't want the value in your view model, then you can use a custom binding to manipulate the context like:
ko.bindingHandlers.templateWithOptions = {
init: ko.bindingHandlers.template.init,
update: function(element, valueAccessor, allBindingsAccessor, viewModel, context) {
var options = ko.utils.unwrapObservable(valueAccessor());
//if options were passed attach them to $data
if (options.templateOptions) {
context.$data.$item = ko.utils.unwrapObservable(options.templateOptions);
}
//call actual template binding
ko.bindingHandlers.template.update(element, valueAccessor, allBindingsAccessor, viewModel, context);
//clean up
delete context.$data.$item;
}
}
Here is a sample in use: http://jsfiddle.net/rniemeyer/tFJuH/
Note that in a foreach scenario, you would find your options on $parent.$item rather than just $item.
I would suggest Sanderson's proposal where you would pass new literal to template data that contains model and extra data (template options).
data-bind="template: { name: 'myTemplate', data: { model: $data, someOption: someValue } }"
Working Demo http://jsfiddle.net/b9WWF/
Source https://github.com/knockout/knockout/issues/246#issuecomment-3775317

Categories