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
Related
I'm using Marionette with Handlebars templates and I can't get my itemView to render inside a CollectionView.
Here is the CollectionView code:
define( [ 'App', 'marionette', 'handlebars', 'models/Model', 'collections/Collection', 'text!templates/welcome.html'],
function(App, Marionette, Handlebars, Model, Collection, template) {
//ItemView provides some default rendering logic
var ItemView = Marionette.ItemView.extend( {
//Template HTML string
template: Handlebars.compile(template),
//model: new Model(),
// View Event Handlers
events: {
},
initialize: function(o) {
console.log('init itemView');
}
});
return Marionette.CollectionView.extend( {
issues: new Collection(),
itemView: ItemView,
onRender: function() {this.issues.fetch()},
initialize: function(o) { console.log('init collectionView')}
});
});
here is the template
<div class="hero-unit">
<h1>Marionette-Require-Boilerplate Lite</h1>
<p>Lightweight Marionette Boilerplate application to get you off the ground fast.</p>
<p class="muted">
You are viewing this application on
</p>
<br/>
<table>
{{#each items}}
<tr><td>{{title}} - {{number}}</td></tr>
{{/each}}
</table>
<a class="btn btn-primary btn-large" href="https:github.com/BoilerplateMVC/">See more Boilerplates</a>
The only thing I get from this code is that the CollectionView does trigger its initialize method and that the collection is fetched from GitHub.
There are multiple reasons this could not be working, depending on the Marionette version you are using:
For the latest Marionette version, you have to use 'childView' instead of 'itemView'.
The items to display are expected in the property 'collection' not 'issues'.
example:
var IssuesView = Marionette.CollectionView.extend({
childView: IssueView,
onRender: function () {
this.collection.fetch();
},
initialize: function (o) {
console.log('init collectionView');
}
});
new IssuesView({'collection': new Backbone.Collection()});
Now, based on the code you provided, I assume your goal is to display the issues inside 'items', if that is correct, I will suggest to use a 'CompositeView' instead, and then you can provide a 'container' and render the 'issues' inside the items. For example:
var IssueView = Marionette.ItemView.extend({
itemTag: 'tr',
//Template HTML string
template: Handlebars.compile($("#item-template").html()),
//model: new Model(),
// View Event Handlers
events: {
},
initialize: function (o) {
console.log('init itemView');
}
});
var IssuesView = Marionette.CompositeView.extend({
childView: IssueView,
childViewContainer: "#issues",
template: Handlebars.compile($("#some-template").html()),
onRender: function () {
//this.issues.fetch();
},
initialize: function (o) {
console.log('init collectionView');
}
});
Where your templates are:
<script id="item-template" type="text/x-handlebars-template">
<td>
{{title}} - {{number}}
</td>
</script>
<script id="some-template" type="text/x-handlebars-template">
<div class = "hero-unit" >
<h1>Marionette - Require - Boilerplate Lite </h1>
<p>Lightweight Marionette Boilerplate ...</p>
<p class = "muted"> You are viewing this application on </p>
<br/>
<table id="issues">
</table>
</script>
Here is jsfiddle with a working version of this:
http://jsfiddle.net/gvazq82/v5yj6hp4/2/
Your problem is that you're not specifying a collection in your CollectionView. You want to instead do
var collectionView = Marionette.CollectionView.extend({
collection: new issues
...
});
According to official documentation, way to create itemcontroller is:
App.PostsController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
// the `title` property will be proxied to the underlying post.
titleLength: function() {
return this.get('title').length;
}.property('title')
});
But I'm not setting my ArrayController to App. It is set to a local variable behind a function scope. And the itemController property can only be string (according to documentation). So how do I set the itemController property?
My code looks like this:
var Channels=Ember.Object.extend({
list:Ember.ArrayController.create(
{
"model":[
{
"id":"display",
"label":"Display",
},{
"id":"social",
"label":"Social",
},{
"id":"email",
"label":"Email",
}
]
}
)
});
App.ChannelController=Ember.Controller.extend({
channels:Channels,
}));
<script type="text/x-handlebars" data-template-name='channel'>
<div>
{{#each channel in channels.list}}
{{channel.label}}
{{/each}}
</div>
</script>
I don't want to pollute App namespace with itemControllers that is to be used locally.
Update
Suppose my channels is like this:
var Channels=Ember.Object.extend({
list:Ember.ArrayController.create(
{
"model":[
{
"id":"display",
"label":"Display",
},{
"id":"social",
"label":"Social",
},{
"id":"email",
"label":"Email",
}
]
}
),
selected:"display"
});
and I want to something like this in template:
<script type="text/x-handlebars" data-template-name='channel'>
<h1>{{channels.selected}}</h1>
<div>
{{#each channel in channels.list}}
<div {{bind-attr class="channel.isselected:active:inactive"}}>{{channel.label}}</div>
{{/each}}
</div>
</script>
so that it outputs:
<h1>display</h1>
<div>
<div class="active">Display</div>
<div class="inactive">Social</div>
<div class="inactive">Email</div>
</div>
How do I do it with components?
You'll likely want to read the guide of components to get the full picture, but the gist of it is that you want to replace all item controllers with components. However, components will also replace the template inside of the each block as well. I don't entirely understand what's going on in your code, but here's an example roughly based on your code.
// Component
App.ChannelDisplayComponent = Ember.Component.extend({
channel: null,
isSelected: function() {
// Compute this however you want
// Maybe you need to pass in another property
}.property('channel')
});
{{! Component Template }}
<div {{bind-attr class="channel.isSelected:active:inactive"}}>
{{channel.label}}
</div>
{{!Channels Template}}
{{#each channel in channels.list}}
{{channel-component channel=channel}}
{{/each}}
The component is essentially your item controller, only it gets its own template as well.
You really shouldn't be worried about polluting the app namespace (unless you're having naming collisions, but that's a different issue). And as Kitler said, you should move to components instead of item controllers. But if you want to do this, the best way I can think of is overridding the (private) controllerAt hook.
var ItemController = Ember.Controller.extend({});
App.PostsController = Ember.ArrayController.extend({
controllerAt: function(idx, object, controllerClass) {
var subControllers = this._subControllers;
if (subControllers.length > idx) {
if (subControllers[idx]) {
return subControllers[idx];
}
}
var parentController = (this._isVirtual ? this.get('parentController') : this);
var controller = ItemController.create({
target: parentController,
parentController: parentController,
model: object
});
subControllers[idx] = controller;
return controller;
}
})
I've made a jsbin to illustrate my issue.
the binding seems KO with lastname property defined inside the itemController and the fullname value is not updated in my items loop.
What am I doing wrong ?
Controller for item in list is different than one you edit property lastname for, so it will never get updated. Propery lastname has to be specified as Model's property (if using Ember Data you simply don't use DS.attr for it and it won't be persisted). If you use custom library for data persistence you have to manually remove lastname property. You can use Ember Inspector extension to see that there are 5 controllers when you click on item. 4 for each item in list and one is being generated when you click. You edit property lastname for this fifth controller. To solve this you can use:
JavaScript:
App = Ember.Application.create();
App.Router.map(function() {
this.resource('items', function() {
this.resource('item', {path: '/:item_id'});
});
});
App.Model = Ember.Object.extend({
firstname: 'foo',
lastname: 'bar',
fullname: function() {
return this.get('firstname') + ' ' + this.get('lastname');
}.property('firstname', 'lastname')
});
App.ItemsRoute = Ember.Route.extend({
model: function() {
return [App.Model.create({id: 1}), App.Model.create({id: 2}), App.Model.create({id: 3}), App.Model.create({id: 4})];
}
});
App.ItemRoute = Ember.Route.extend({
model: function(params) {
return this.modelFor('items').findBy('id', +params.item_id);
}
});
Templates:
<script type="text/x-handlebars">
<h2>Welcome to Ember.js</h2>
{{link-to "items" "items"}}
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="items">
<ul>
{{#each item in model}}
<li>
{{#link-to 'item' item.id}}
{{item.fullname}} {{item.id}}
{{/link-to}}
</li>
{{/each}}
</ul>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="item">
{{input value=model.firstname}}
{{input value=model.lastname}}
{{model.fullname}}
</script>
Please keep in mind that ArrayController and ObjectController aren't recommended to use, because they will be deprecated in future. Demo.
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/
I am having a viewmodel and an associated template as below.
var AilmentItem = function () {
this.SelectedAilment = ko.observable();
}
function AilmentsViewModel() {
this.Ailments = ko.observableArray([new AilmentItem()]);
this.AilmentsType = ko.observableArray([{ Name: 'Diabetes' }, { Name: 'Arthritis' }, { Name: 'High BP'}]);
}
HTML script
<script type="text/javascript">
$(function () {
var AilmentsVM = new AilmentsViewModel();
ko.applyBindings(AilmentsVM, $('#Ailments')[0]);
});
</script>
<div id="Ailments">
<div>
<table>
<tbody data-bind='template: { name: "ailmentRowTemplate", foreach: Ailments }'>
</tbody>
</table>
</div>
</div>
<script type="text/html" id="ailmentRowTemplate">
<tr>
<td><select data-bind="options: AilmentsVM.AilmentsType(), optionsText: 'Name', value: SelectedAilment"></select></td>
</tr>
</script>
In the HTML template I need to bind AilmentsType to one of the columns as drop down. Can someone guide me how to achieve it? Thanks.
Your AilmentsVM does not have global scope, because it is being created in your jQuery ready block, so you can't access it directly in a data-bind.
If you are using 1.3 beta, then you can use either the $root or $parent special variables that Knockout provides. In this case, they would be the same, as you are only one level in from the top-level scope. So, just do: $root.AilmentsType.
If you are using an earlier version, then you can use the templateOptions functionality to pass options to a jQuery template. It would look like this:
<tbody data-bind='template: { name: "ailmentRowTemplate", foreach: Ailments, templateOptions: { types: AilmentsType } }'>
</tbody>
Then, access it like:
<select data-bind="options: $item.types, optionsText: 'Name', value: SelectedAilment"></select>