Marionette.js - can I detect onAppend? - javascript

I have a silly problem, where my only solution is a sloppy hack that is now giving me other problems.
See my fiddle,
or read the code here:
HTML:
<input id='1' value='input1' />
<template id='template1'>
<input id='2' value='input2' />
</template>
JS - Item View Declaration:
// Declare an ItemView, a simple input template.
var Input2 = Marionette.ItemView.extend({
template: '#template1',
onRender: function () {
console.log('hi');
},
ui: { input2: '#2' },
onRender: function () {
var self = this;
// Despite not being in the DOM yet, you can reference
// the input, through the 'this' command, as the
// input is a logical child of the ItemView.
this.ui.input2.val('this works');
// However, you can not call focus(), as it
// must be part of the DOM.
this.ui.input2.focus();
// So, I have had to resort to this hack, which
// TOTALLY SUCKS.
setTimeout(function(){
self.ui.input2.focus();
self.ui.input2.val('Now it focused. Dammit');
}, 1000)
},
})
JS - Controller
// To start, we focus input 1. This works.
$('#1').focus();
// Now, we make input 2.
var input2 = new Input2();
// Now we 1. render, (2. onRender is called), 3. append it to the DOM.
$(document.body).append(input2.render().el);
As one can see above, my problem is that I can not make a View call focus on itself after it is rendered (onRender), as it has not yet been appended to the DOM. As far as I know, there is no other event called such as onAppend, that would let me detect when it has actually been appended to the DOM.
I don't want to call focus from outside of the ItemView. It has to be done from within for my purposes.
Any bright ideas?
UPDATE
Turns out that onShow() is called on all DOM appends in Marionette.js, be it CollectionView, CompositeView or Region, and it isn't in the documentation!
Thanks a million, lukaszfiszer.

The solution is to render your ItemView inside a Marionette.Region. This way an onShow method will be called on the view once it's inserted in the DOM.
Example:
HTML
<input id='1' value='input1' />
<div id="inputRegion"></div>
<template id='template1'>
<input id='2' value='input2' />
</template>
JS ItemView
(...)
onShow: function () {
this.ui.input2.val('this works');
this.ui.input2.focus();
},
(...)
JS Controller
$('#1').focus();
var inputRegion = new Backbone.Marionette.Region({
el: "#inputRegion"
});
var input2 = new Input2();
inputRegion.show(input2);
More information in Marionette docs: https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#region-events-and-callbacks

Well, I managed to solve it by extending Marionette.js, but if anyone else has a better idea that doesn't involve extending a library, I will GLADLY accept it and buy you a doughnut.
// After studying Marionette.js' annotated source code,
// I found these three functions are the only places
// where a view is appended after rendering. Extending
// these by adding an onAppend call to the end of
// each lets me focus and do other DOM manipulation in
// the ItemView or Region, once I am certain it is in
// the DOM.
_.extend(Marionette.CollectionView.prototype, {
appendHtml: function(collectionView, itemView, index){
collectionView.$el.append(itemView.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.CompositeView.prototype, {
appendHtml: function(cv, iv, index){
var $container = this.getItemViewContainer(cv);
$container.append(iv.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.Region.prototype, {
open: function(view){
this.$el.empty().append(view.el);
if (view.onAppend) { view.onAppend() }
},
});

Related

If Knockout wraps a bounded function expression in a computed, why it doesn't always create a depndency?

This is a follow-up question to this one:
As explained in the above-linked answer:
When you provide an expression for a binding value rather than just a
reference to an observable, KO effectively wraps that expression in a
computed when applying the bindings.
Thus, I expected that when providing the changeCity as a binding expression (it is a function and not an observable), then changing the value on the input box would fire the changeCity function.
However, as you can see on the first snippet, it doesn't (Nor when binding it as changeCity()), but If changeCity is declared as a ko.computed, it does fire - see the second snippet.
Does it mean that a bounded function and a bounded computed are not completely the same with regard to dependency tracking?
First snippet - bounded function:
var handlerVM = function () {
var self = this;
self.city = ko.observable("London");
self.country = ko.observable("England");
self.changeCity = function () {
if (self.country() == "England") {
self.city("London");
} else {
self.city("NYC");
}
}
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h3 data-bind="text: city"> </h1>
<span data-bind="text: 'change the country, get out of focus, and nothing will happen...'"></span>
<br/>
<input data-bind="value: country" />
Second snippet - bounded computed:
var handlerVM = function () {
var self = this;
self.city = ko.observable("London");
self.country = ko.observable("England");
self.changeCity = ko.computed(function () {
if (self.country() == "England") {
self.city("London");
} else {
self.city("NYC")
}
});
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h3 data-bind="text: city"> </h1>
<span data-bind="text: 'change the country, get out of focus, and behold:'"> </span>
<br/>
<input data-bind="value: country" />
I take it that you're not just trying to solve a practical problem, but that you're mostly interested in the "theoretical difference" between passing a computed or a plain function to a binding. I'll try to explain the differences/similarities.
Let's start with an example
const someObs = ko.observable(10);
const someFn = () => someObs() + 1;
const someComp = ko.computed(someFn);
const dec = () => someObs(someObs() - 1);
ko.applyBindings({ someObs, someFn, someComp, dec });
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>Obs. value: <span data-bind="text: someObs"></span></div>
<div>Computed created in binding: <span data-bind="text: someFn()"></span></div>
<div>Computed created in vm: <span data-bind="text: someComp"></span></div>
<button data-bind="click: dec">-1</button>
The example above shows that both someFn and someComp do the same thing. By referencing someFn() in a binding handler's value, you've essentially created a computed with a dependency to someObs.
Why this doesn't work in your first example
You never referenced your changeCity method in any knockout related code, which means there'll never be the chance to create a dependency. Of course, you can force one, but it's kind of weird:
var handlerVM = function () {
var self = this;
self.city = ko.observable("London");
self.country = ko.observable("England");
self.changeCity = function () {
if (self.country() == "England") {
self.city("London");
} else {
self.city("NYC");
}
}
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h3 data-bind="text: city"> </h1>
<span data-bind="html: 'change the country, get out of focus, and <strike>nothing</strike> <strong>something</strong> will happen...'"></span>
<br/>
<input data-bind="value: (changeCity(), country)" />
Why a regular computed does work
In your second example, you use a ko.computed. Upon instantiating a ko.computed, the passed function is evaluated once (immediately) and dependencies to all used observables are created.
If you were to change the ko.computed to a ko.pureComputed, you'll see your second example will also stop working. A pureComputed only evaluates once its return value is actually used and won't create dependencies until then.
The internals
Knockout wraps your binding's value in a function as a string. You can read more about this in an answer I wrote earlier.
We also know that any observable that is called inside a binding-handler's init method, creates a dependency that calls the binding's update method when a change happens.
So, in the example I gave, this is what happens:
The text binding is parsed
The function function() { return someFn(); } is passed as a value accessor to the text binding's init method.
The value accessor is called to initialize the text field
someObs is asked for its value and a dependency is created
The correct value is rendered to the DOM
Then, upon pressing the button and changing someObs:
someObs is changed, triggering the text binding's update method
The update method calls the valueAccessor, re-evaluating someObs and correctly updating its text.
Practical advice
To wrap up, some practical advice:
Use a ko.pureComputed when you create a new value out of one or more observable values. (your example)
self.city = ko.pureComputed(
() => self.country() === "england"
? "london"
: "nyc"
);
Use a subscribe if you want to create side effects based on an observable value changing. E.g.: a console.log of a new value or a reset of a timer.
Use a ko.computed when you want to create side effects based on a change in any of several observables.
the expected behavior in both snippets is that once the text in the input box is changed (and the focus is out), changeCity is fired (Happens on the 2nd, not on the 1st).
Ahhh, now I understand. You are describing what a subscription does.
First off, rid your mind of DOM events. The <input> field does not exist. All there is is your viewmodel. (*)
With this mind-set it's clear what to do: React to changes in your country property, via .subscribe(). The following does what you have in mind.
var handlerVM = function () {
var self = this;
self.city = ko.observable("London");
self.country = ko.observable("England");
self.country.subscribe(function (newValue) {
switch (newValue.toLowerCase()) {
case "england":
self.city("London");
break;
case "usa":
self.city("NYC");
break;
default:
self.city("(unknown)");
}
});
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h3 data-bind="text: city"></h3>
<input data-bind="value: country" />
(*) Of course the <input> field still exists. But it helps to imagine the view (your HTML) as 100% dependent on your viewmodel. Knockout does all the viewmodel-view interaction for you. It takes care of displaying changes in the viewmodel data, and it takes care of feeding back user interactions into your viewmodel. All you should pay attention to is changes in your viewmodel.
Whenever you feel that you need to listen to a basic DOM event like "click", chances are that you are doing something wrong, i.e. chances are you are missing an observable, or a custom binding.

With KnockoutJS, how can I scroll to a component after it's rendered in a foreach?

I have deferred updates enabled.
I have two components.
The first is a list, which is simply implemented as a div with a foreach data binding:
<div class="list-people" data-bind="foreach: { data: people, afterRender: afterRenderPeople }">
<!-- ko component: { name: "listitem-person", params: { person: $data } } --><!-- /ko -->
</div>
The second is the list item:
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
afterRender is called for each item in the foreach.
My afterRenderPerson function is simple enough:
public afterRenderPerson = (elements: any[], data: Person) => {
let top = $(element[0]).offset().top;
scrollTo(top);
};
The problem is that when afterRenderPerson is called the sub-component listitem-person hasn't yet been rendered.
Which means the element array passed to afterRenderPerson has 4 nodes:
A text node containing \n i.e. a new line.
A comment node containing <!-- ko component: { name: "listitem-person", params: { person: $data } } -->.
A comment node containing <!-- /ko -->.
A text node containing \n i.e. a new line.
None of these are suitable for getting the top pixel, and even if they were, the sub-component being rendered could affect the layout at that location changing the value of the pixel I'm trying to get.
Unfortunately it seems that the documentation for foreach doesn't take in to account the delayed nature of components.
If you need to run some further custom logic on the generated DOM elements, you can use any of the afterRender/afterAdd/beforeRemove/beforeMove/afterMove callbacks described below.
Note: These callbacks are only intended for triggering animations related to changes in a list.
There are two workarounds I've come across, neither of which are great, but that's why they're workarounds and not solutions!
user3297291 gave the suggestion in a comment of making a scrollTo binding that's placed on the child components.
Only workaround I can think of is to define a custom scrollTo binding and include it in the component template... Quite easy to implement, but still feels hacky and makes your inner component harder to reuse. You might also want to track this feature request – user3297291
This would simply be a custom binding that conditionally executes some code based on a value provided to it.
The bindings aren't called until the HTML has been inserted in to the DOM. That's not perfect, as later changes to the DOM could affect the position of the inserted HTML elements, but it should work for many situations.
I wasn't very keen on having to modify the child components though, I preferred a solution when remained encapsulated in the parent component.
The second workaround is to check to see if the child component HTML element exists in the DOM by it's ID. Since I don't know when they will come in to existence this has to be done in some sort of loop.
A while loop isn't suitable as it'll run the check far too often, in a "tight" loop, so instead setTimeout is used.
setTimeout is a horrid hack, and it makes me feel dirty to use it, but it does work for this situation.
private _scrollToOffset = -100;
private _detectScrollToDelayInMS = 200;
private _detectScrollToCountMax = 40;
private _detectScrollToCount = 0;
private _detectScrollTo = (scrollToContainerSelector: string, scrollToChildSelector: string) => {
//AJ: If we've tried too many times then give up.
if (this._detectScrollToCount >= this._detectScrollToCountMax)
return;
setTimeout(() => {
let foundElements = $(scrollToChildSelector);
if (foundElements.length > 0) {
//AJ: Scroll to it
$(scrollToContainerSelector).animate({ scrollTop: foundElements.offset().top + this._scrollToOffset });
//AJ: Give it a highlight
foundElements.addClass("highlight");
} else {
//AJ: Try again
this._detectScrollTo(scrollToContainerSelector, scrollToChildSelector);
}
}, this._detectScrollToDelayInMS);
this._detectScrollToCount++;
};
I made sure to put a limit on how long it can run for, so if something goes wrong it won't loop forever.
It should probably be noted that there is an "Ultimate" solution to this problem, and that's TKO, AKA Knockout 4.
But that's not "production ready" yet.
How to know when a component has finished updating DOM?
brianmhunt commented on Jun 20
knockout/tko (ko 4 candidate) latest master branch has this.
More specifically, the applyBindings family of functions now return a Promise that resolves when sub-children (including asynchronous ones) are bound.
The API isn't set or documented yet, but the bones have been set up.
This appears to work. I made a binding handler that runs a callback in its init (it uses tasks.schedule to allow a rendering cycle). Attaching it at the parent level does not get the children rendered in time, but attaching it to the virtual element does.
I designed it to work with a function whose signature is like afterRender. Because it runs for each of the elements, the callback function has to test that the data is for the first one of them.
ko.options.deferUpdates = true;
ko.bindingHandlers.notify = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make it asynchronous, to allow Knockout to render the child component
ko.tasks.schedule(() => {
const onMounted = valueAccessor().onMounted;
const data = valueAccessor().data;
const elements = [];
// Collect the real DOM nodes (ones with a tagName)
for(let child=ko.virtualElements.firstChild(element);
child;
child=ko.virtualElements.nextSibling(child)) {
if (child.tagName) { elements.push(child); }
}
onMounted(elements, data);
});
}
};
ko.virtualElements.allowedBindings.notify = true;
function ParentVM(params) {
this.people = params.people;
this.afterRenderPeople = (elements, data) => {
console.log("Elements:", elements.map(e => e.tagName));
if (data === this.people[0]) {
console.log("Scroll to", elements[0].outerHTML);
//let top = $(element[0]).offset().top;
//scrollTo(top);
}
};
}
ko.components.register('parent-component', {
viewModel: ParentVM,
template: {
element: 'parent-template'
}
});
function ChildVM(params) {
this.Name = params.person;
}
ko.components.register('listitem-person', {
viewModel: ChildVM,
template: {
element: 'child-template'
}
});
vm = {
names: ['One', 'Two', 'Three']
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<template id="parent-template">
<div class="list-people" data-bind="foreach: people">
<!-- ko component: { name: "listitem-person", params: { person: $data } }, notify: {onMounted: $parent.afterRenderPeople, data: $data} -->
<!-- /ko -->
</div>
</template>
<template id="child-template">
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
</template>
<parent-component params="{ people: names }">
</parent-component>

Function is not defined, parameter formatting

I'm trying to create my own lightbox script where I can pass the variables (title, description, itemtype, itemid, etc.) in clean formatting like this (inspired by fancybox):
myFunction({
title: "My title",
description: "My description"
});
Clicking on a certain element prepends some HTML to a div with jQuery.
I have adapted a piece of code I found on Stackoverflow and "kind of" understand the code. The top function has not been changed and worked before I edited the bottom code, to that I added click(function() { } because in the example the code was executed on pageload.
However, when I click my H1 element the firebug console tells me ReferenceError: popup is not defined
This is my Javascript:
$(document).ready(function() {
(function ($) {
$.fn.popup = function (options) {
var settings = $.extend({
title: function (someData) {
return someData;
},
description: function (someData) {
return someData;
},
}, options);
$("#content").prepend(
"<div style=\"position:fixed;top:0;left:0;width:100%;height:100%;background:#FFFFFF;\">\
<h1>"+ settings.title +"</h1>\
<p>" + settings.description +"</p>\
</div>"
);
};
}(jQuery));
$(".openbox1").click(function() {
popup({
title: "Title 1",
description: "Description 1"
});
}));
$(".openbox2").click(function() {
popup({
title: "Title 2",
description: "Description 2"
});
}));
});
This is my HTML
<div id="content">
<h1 class="openbox1">open box 1</h1>
<h1 class="openbox2">open box 2</h1>
</div>
A. Wolff commented that I need to execute the function like this:
$(".openbox1").click(function() {
$(this).popup({
...
});
});
This fixed it, thanks!
First off, what you did, and I hope this helps:
// This, of course is same as "document.onload"
// Don't confuse it with "window.onload"
// wich will wait till WHOLE dom is loaded to run any script
$(document).ready(function() {
(function ($) {
// This is, in essence, the start of a jQuery plugin
// This is often referred to as the "quick and dirty setup"
// as it's a direct call to add a method to jQuery's
// element object. Meaning it can be recalled as
// $(element).popup().
// This should not be confused with $.popup = function
// which would just add a method to jQuery's core object
$.fn.popup = function (options) {
var settings = $.extend({
...
}(jQuery));
$(".openbox1").click(function() {
// here is where your issue comes in
// as previously noted, you did not create a
// method named "popup".
// you added a method to jQuery's Element Object
// called "popup".
// This is why `$(this).popup` works and
// plain `popup` does not.
// You're inside an "event" asigned to any element
// having class name `openbox1`. Thus, any call
// in here to `this`, will reference that element
popup({
Secondly, a different example of how to write it. I won't say better because, even if I say my way is better, it wouldn't make your "corrected" way wrong. In Javascript, as the old saying goes, There's more than one way to skin a cat.
My Example:
// Notice I'm adding this plugin BEFORE the document load.
// This means, you could easily add this to a file and load it
// in script tags like any other Javascript,
// as long as it's loaded AFTER jquery.
(function($) {
// this ensures that your plugin name is available and not previously added to jQuery library
if (!$.popup) {
// this also provides us "variable scope" within to work in
// here begin adding the plugin to jQuery
// I started with $.extend, so it can be added to the jQuery library and used in traditional format
// $.popup('element selector', { options })
// as well as the element.action format we'll add later
// $.(element selector).popup({ options })
// This should help give you a good idea of the whole of what all is going on
$.extend({
popup: function() {
var ele = arguments[0], // this is our jQuery element
args = Array.prototype.slice.call(arguments, 1); // this gets the rest of the arguments
// this next step is useful if you make the traditional call `$.popup(this, { options })`
if (!(ele instanceof jQuery)) ele = $(ele);
// now we have total control! Bwahahha!
// Fun aside, here is where it's good to check if you've already asigned this plugin
// if not, then make some "marker", so you can recall the element plugin and comment an
// action instead of reinitializing it
if (!ele.data('popup')) $.popup.init(ele, args);
else {
// at this point, you would know the element already has this plugin initialized
// so here you could change an initial options
// like how with jQueryUI, you might would call:
// $(element).popup('option', 'optionName', value)
}
return ele;
}
});
// here is where we add the $(element selector).popup method
// this simply adds the method to the element object
// If you don't fully understand what's going on inside (as I explain below),
// just know that it's some "fancy footwork" to pass the method onto our initial
// method creation, $.popup
$.fn.extend({
popup: function(/*no need for parameter names here as arguments are evaluated inside and passed on to initial method*/) {
// set this element as first argument to fit with initial plugin method
var args = [$(this)];
// if there are arguments/params/options/commands too be set, add them
if (arguments.length) for (x in arguments) args.push(arguments[x]);
// pass through jquery and our arguments, end result provides same arguments as if the call was:
// $.popup($(element), options)
return $.popup.apply($, args);
}
});
// This next part is not seen in many plugins but useful depending on what you're creating
$.popup.init = function(ele, opt) {
// here is where we'll handle the "heavy work" of establishing a plugin on this element
// Start with setting the options for this plugin.
// This means extending the default options to use any passed in options
// In the most simple of cases, options are passed in as an Oject.
// However, that's not always the case, thus the reason for this being
// a continued array of our arguments from earlier.
// We'll stick with the simplest case for now, your case, that the only options are an
// Object that was passed in.
// using the extend method, with true, with a blank object,
// allows us to added the new options "on top" of the default ones, without changing the default ones
// oh and the "true" part just tells extend to "dig deep" basically (multideminsional)
if (opt && typeof opt[0] == 'object') opt = $.extend(true, {}, $.popup.defaults, opt[0]);
var par = opt.parent instanceof jQuery ? opt.parent : $('body'),
tit = opt.title,
des = opt.description,
// this last one will be the wrapper element we put everything in
// you have this in yours, but it's written in a very long way
// this is jQuery simplified
wrap = $('<div />', { style: 'position:fixed;top:0;left:0;width:100%;height:100%;background:#FFFFFF;' }),
// much like the previous element, cept this is where our title goes
head = $('<h1 />', { text: tit }).appendTo(wrap),
content = $('<p />', { text: des }).appendTo(wrap);
$(par).append(wrap);
// finally, add our marker i mentioned earlier
ele.data('popup', opt);
// just adding the following cause i noticed there is no close
// fyi, i would change this plugin a little and make an actial "open" command, but that's another tutorial
var closer = $('<span />', { text: '[x]', style: 'cursor:pointer;position:absolute;bottom:1em;right:1em;' });
wrap.append(closer);
closer.click(function(e) { ele.data('popup', false); wrap.remove(); });
};
$.popup.defaults = { // establish base properties here that can be over-written via .props, but their values should never truly change
'parent': undefined, // added this to keep it dynamic, instead of always looking for an element ID'd as content
title: '',
description: ''
};
}
})(jQuery);
// the following is basically jQuery shorthand for document.ready
$(function() {
// i think you get the rest
$(".openbox1").on('click', function(e) {
$(this).popup({
title: "Title 1",
description: "Description 1",
parent: $("#content")
});
})
$(".openbox2").on('click', function(e) {
$(this).popup({
title: "Title 2",
description: "Description 2",
parent: $("#content")
});
})
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div id="content">
<h1 class="openbox1">open box 1</h1>
<h1 class="openbox2">open box 2</h1>
</div>

Backbone view losing event after rerender

I've checked this post, and this one, and this one , and numerous others and none of the solutions seem to help me at all. All I'm trying to do is replace the contents of a view with an array of html. Each element in the array is created by using the underscore templating engine. Here's my code:
The Template:
<script type="text/template" id="radioItemTemplate">
<li>
<div class="button price <% if(enabled){%>enabled<%}%>">$<%=price%></div>
<div class="title name"><%=name%></div>
</li>
</script>
The Javascript:
iHateThisView = Backbone.View.extend({
template: _.template($("#radioItemTemplate").html()),
events:{
"click .price": "_onRadioItemClick"
},
radioItems: null,
radioItem: null,
initialize: function (options) {
this.radioItems = options.radioItems;
this.radioItem = options.radioItem;
},
render: function () {
trace("rendering");
var radioItems = this.radioItems.first(3);
var activeRadioItem = this.radioItem.get('name');
var result = [];
var scope = this;
_.forEach(radioItems, function (radioItem) {
var option = {
name: radioItem.get('name'),
price: radioItem.get('price'),
enabled: activeRadioItem == radioItem.get('name')
};
result.push(scope.template(option));
});
//THE TRICKY ZONE -START
this.$el.html(result);
//THE TRICKY ZONE -END
return this;
},
_onRadioItemClick: function (event) {
$el = this.$el;
var clickedName = $el.find('price');
console.log('clickedName');
}
});
Aside from it wrapping my html with a <div> this does exactly what I want on the first render. However if I called my render function again, none of the events work. So based on all my readings, I figured this.delegateEvents() should fix the loss of events, so I tried this:
//THE TRICKY ZONE -START
this.$el.html(result);
this.delegateEvents();
//THE TRICKY ZONE -END
Which from what I can tell did nothing. On the first render when I click on the radioItems I'd get my console.log, but again not after a re-render
so then I read that I might have to do this:
//THE TRICKY ZONE -START
this.$el.html(result);
this.delegateEvents(this.events);
//THE TRICKY ZONE -END
Which also did nothing.
So then I tried a different method:
//THE TRICKY ZONE -START
this.setElement(result);
this.delegateEvents(); //with and without this line
//THE TRICKY ZONE -END
This added only the first item in the array, and the events didn't work even on the first render.
Please restore my sanity guys, I don't what else to do.

changing backbone views

I have a question about the way backbone handles it views.
Suppose I have the following code:
<div id="container">
<div id="header">
</div>
</div>
After this I change header into a backbone view.
How can I now remove that view from the header div again after I'm done with the view and add ANOTHER view to the same div?
I tried just overwriting the variable the view was stored in. This results in the view being changed to the new one...but it will have all the event handlers of the old one still attached to it.
Thanks in advance!
http://documentcloud.github.com/backbone/#View-setElement
This won't automatically remove the original div - you'll want to do that yourself somehow, but then by using setElement you'll have the view's element set to whatever you passed it.. and all of the events will be attached as appropriate. Then you'll need to append that element wherever it is that it needs to go.
--- Let's try this again ----
So, first thing to keep in mind is that views reference DOM elements.. they aren't super tightly bound. So, you can work directly with the jquery object under $el.
var containerView = new ContainerView();
var headerView = new HeaderView();
var anotherHeaderView = new AnotherHeaderView();
containerView.$el.append(headerView.$el);
containerView.$el.append(anotherHeaderView.$el);
anotherHeaderView.$el.detach();
containerView.$el.prepend(anotherHeaderView.$el);
Or you can create methods to control this for you.
var ContainerView = Backbone.View.extend({
addView: function (view) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.$el.append(el);
}
});
Maybe setting the views by view order?
var ContainerView = Backbone.View.extend({
initialize: function () {
this.types = {};
},
addView: function (view, type) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.types[type] = el;
this.resetViews();
},
removeView: function (type) {
delete this.types[type];
this.resetViews();
},
resetViews: function () {
this.$el.children().detach();
_.each(['main_header', 'sub_header', 'sub_sub_header'], function (typekey) {
if(this.types[typekey]) {
this.$el.append(this.types[typekey]);
}
}, this);
}
});

Categories