Extend Coffeescript subclass function from parent in marionette - javascript

I've got a "FormView" class in my Marionette application that sets up a lot of things for form submissions around my app. I use it every time there is a form. I've also got some helpers that I want to pass into every form template via the templateHelpers method, but I also want to be able to add additional templateHelpers in the children. Like so:
class Views.FormView extends Marionette.ItemView
templateHelpers: ->
helpers: Marionette.Concerns.Helpers
class Views.NewUser extends Views.FormView
templateHelpers: ->
variable: #something
I want to be able to access both #variable and #helpers from the template for NewUser. Additionally, I know how to handle this (via _.extends) if templateHelpers is an object, but I need it to be a function.
Is it possible? And if so, how?

In Backbone, when you inherit from another class, Backbone will give the subclass a __super__ property (double underscore at both ends) which is the prototype of the parent class. So from within your Views.NewUser class, you can get the prototype via View.NewUser.__super__. Then, you can call templateHelpers on that object. Here's how I did it (live example at JSbin):
Views = {}
class Views.FormView extends Marionette.ItemView
templateHelpers: ->
helpers: "Parent Value"
logHelpers: ->
console.log #templateHelpers()
class Views.NewUser extends Views.FormView
templateHelpers: ->
variable: "Child Value"
logHelpers: ->
console.log #templateHelpers()
console.log Views.NewUser.__super__.templateHelpers()
formView = new Views.FormView()
formView.logHelpers() // { helpers: "Parent Value" }
newUser = new Views.NewUser()
newUser.logHelpers()
// { variable: "Child Value" }, { helpers: "Parent Value" }
You could then use _.extend to extend one with the other; maybe like this:
initialize: ->
#helpers = _.extend #templateHelpers(),
Views.NewUser.__super__.templateHelpers()

Assuming that when you say "parent" you mean "parent view" rather than OO ancestor.
What you want to achieve can be done by doing the following:
1. Pass the data object from parent to child view using the itemViewOptions (or childViewOptions in newer Marionette). It can contain functions.
2. In you child view you can do the following:
templateHelpers: ->
_.extend super(), #options.someOptionsFromParent

Related

Understanding extend and method overwriting

This is a follow-up question from the previous: Why is the parent prototype method still executable after overriding?.
As we see, the literal object {metadata, aaa, init} was initialized by the extend function as a 2nd import variable, so this object should be the object automatically automatically instantiated and represents the extended sub-class. Then inside the object, it inherited the init method from its parent class and overrode it. first call the parent init method to trigger some necessary things, then setup the data for this extended component.
My question is: the init method in this literal object is its own init method, because this refered to the literal object, right? if it's yes, why I can't see it under this proprieties in my debugger tool.
enter image description here
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/model/json/JSONModel",
"sap/ui/model/resource/ResourceModel",
], function (UIComponent, JSONModel, ResourceModel) {
"use strict";
return UIComponent.extend("sap.ui.demo.walkthrough.Component", {
metadata: {
"interfaces": [
"sap.ui.core.IAsyncContentCreation",
],
"rootView": {
"viewName": "sap.ui.demo.walkthrough.view.App",
"type": "XML",
/*"async": true, // implicitly set via the sap.ui.core.IAsyncContentCreation interface*/
"id": "app",
},
},
aaa: function () {
console.log("aaa");
},
init: function () {
// call the init function of the parent
UIComponent.prototype.init.apply(this, arguments);
// set data model
var oData = {
recipient: {
name: "World",
},
};
var oModel = new JSONModel(oData);
this.setModel(oModel);
// set i18n model
var i18nModel = new ResourceModel({
bundleName: "sap.ui.demo.walkthrough.i18n.i18n",
});
this.setModel(i18nModel, "i18n");
},
});
});
Hope this other answer clears some things up:
__proto__ is the actual object that is used in the lookup chain to resolve methods, etc. prototype is the object that is used to build __proto__ when you create an object with new
This means when you see methods in the prototype section it doesn't mean that the original parent class is overwritten. Your new "class" sap.ui.demo.walkthrough.Component also has/is a prototype and this (which is an instance of your new "class") has it's own __proto__. This new prototype/proto constains methods form the parent prototype and can also define new methods.
And from the documentation of sap.ui.base.Object.extend:
Creates a subclass of class sap.ui.base.Object with name sClassName and enriches it with the information contained in oClassInfo.
oClassInfo might contain three kinds of information:
[...]
any-other-name: any other property in the oClassInfo is copied into the prototype object of the newly created class. Callers can thereby add methods or properties to all instances of the class. [...]
So when you do this.__proto you will see the build plan for the current instance.
Do this.__proto__.__proto__ to get the build plan from the parent class. Do this.__proto__.__proto__.__proto__ to get the grand parent class (and so on).
The protos of the parent class are not affected by your child class. But your child classes include everything from the parent class. When you "override" a method you are are adding a method to your new prototype. The parent method still exists in the UIComponent prototype and can therefore be called by explicitly stating UIComponent.prototype.init.apply.

Use Backbone.View events method through a mixin

I'm using Backbone with Coffeescript in an app. Now the example that I'll use is made as trivial as possible. I have a header in my app that all the views share it. This header has a logout link with #logout index. Now what I'm trying to do is to put the events method and the method that the event will use it when it is triggered in a mixin object and extend the prototype of my Backbone View. Here is the code:
Mixin.Header =
events: ->
'click #logout': 'logout'
logout: (e)->
e.preventDefault()
$.ajax(
type: 'DELETE'
url: '/logout'
).then(
->
document.location = ''
)
class View extends Backbone.View
initialize: (options)->
_.extend(View.prototype, Mixin.Header)
I've been looking through the Backbone source code and I simply can't find the problem why this is not working. The events get delegated to the View through the delegateEvents() method and when the View is initialized the initialize method is being called first.
From the fine manual:
events view.events or view.events()
[...]
Backbone will automatically attach the event listeners at instantiation time, right before invoking initialize.
You're trying to add events in initialize but the events are bound before initialize is called.
You could call delegateEvents yourself to rebind the events after you've updated the prototype:
initialize: (options)->
_.extend(View.prototype, Mixin.Header)
#delegateEvents() # <----------
but the structure would be a little weird because you'd be modifying the class inside an instance.
I think you'd be better off modifying the class before you have any instances:
class View extends Backbone.View
_.extend(View.prototype, Mixin.Header)
or you could use the CoffeeScript shortcut for prototype:
class View extends Backbone.View
_.extend(View::, Mixin.Header)
# -----------^^
or even:
class View extends Backbone.View
_.extend(#::, Mixin.Header)
You will still run into problems if View has its own events as _.extend blindly overwrites properties rather than merging them. You'd need something smarter than _.extend to properly handle this and it would need to be able to figure out how to merge events functions and objects.

Backbone: view's model is undefined when passing it a model

Here's the entirety of my simple Backbone app. When I instantiate a new view, I'm passing it a model, but when the view calls attributes on the model, it says model is undefined:
class App.Recipe extends Backbone.Model
class App.RecipeList extends Backbone.Collection
url: '/users/4/recipes'
model: App.Recipe
#recipeList = new App.RecipeList
#recipeList.fetch()
class App.RecipeView extends Backbone.View
template: _.template(#model.attributes)
render: ->
#$el.html(#template)
#
#recipeView = new App.RecipeView(model: recipeList.models[0])
$ ->
$('#featured_recipes').html(window.recipeView.render())
In the console, recipeList.models[0] returns a JSON object that does indeed have an attributes property. So when I instantiate the view and pass it a model, why do I get Uncaught TypeError: Cannot read property 'attributes' of undefined?
My guess is that #model is getting evaluated at runtime, before I pass in the model. Is there a way to defer that? Or am I wrong?
I've also tried the _.bindAll # in the initialize function of the view. No dice.
PS — I'm obviously a noob, so if you see other antipatterns or things I'm going to run into here, feel free to mention them.
Edit
I've tried the answer outlined here of adding the template inline in my initialize method like this:
class #App.RecipeView extends Backbone.View
tagName: 'li'
initialize: ->
_.bindAll #, 'render'
template = _.template $('#featured_recipes').html()
render: ->
#$el.html #model.toJSON()
#
But then I get Cannot call method 'replace' of undefined , which I know is from underscore calling template on something that isn't in the DOM yet, and that's happening because this gets called before my DOM renders. Any thoughts?
Edit 2
Now I've moved the entire app into the footer of my haml layout, so that the $(#featured-_recipes') div is in the DOM. Then I changed the view to this:
class #App.RecipeView extends Backbone.View
tagName: 'li'
initialize: ->
_.bindAll #, 'render'
#template: _.template $('#featured_recipes').html()
render: ->
#$el.html #template(#model.toJSON())
#
But I'm still getting Cannot call method 'replace' of undefined .
Try the following. I added an initialize method to your View, and I used Backbone's Collection get method to get your recipe.
class App.RecipeView extends Backbone.View
initialize: (options) ->
#model = options.recipe
template: _.template(#model.attributes)
render: ->
#$el.html(#template)
#
#view = new App.RecipeView
recipe: recipeList.get(0)

Backbone: Lost reference to actual view?

I'm new to Backbone (and Marionette), and trying to write a pretty simple app using both. The app has a menu of "groups" on the left nav, and a list of "entries" on the main right div. Every time a Group menu item is clicked, I filter the entries with the group ID and show them, when hide all others.
Here is the Entry Item view (all scripts are in CoffeeScript btw):
class EntryItemView extends Backbone.Marionette.ItemView
tagName: 'tr'
template: _.template $('#entryItemTemplate').html()
render: ->
#$el.html #template(#model.toJSON())
show: ->
#$el.show()
hide: ->
#$el.hide()
Here is the Entry List view, extending Marionette's CollectionView:
class EntryListView extends Backbone.Marionette.CollectionView
itemView: EntryItemView
el: '#main tbody'
This is the AppRouter, pretty much straightforwad:
class AppRouter extends Backbone.Router
routes:
'group/:id' : 'showGroup'
router = new AppRouter()
router.on 'route:showGroup', (id) ->
_.each entryViews, (view) ->
if view.model.get('group_id') is parseInt(id)
view.show()
else
view.hide()
(The entryViews variable is a simple global array to store all EntryItemView instances).
With this approach, navigating the app to /group/:id indeed invokes the show() and hide() method of each EntryItemView object. The problem is, looks like the reference between this object and the actual HTML doesn't exist, so the actual element <tr> doesn't show or hide.
Can you guys point out what I'm doing wrong here? Thanks in advance.
Here are a couple pointers:
since your template is in the HTML, you just specify the jQuery selector with template: "#entryItemTemplate"
you can remove the render declaration, because Marionette does that on its own (i.e. you're implementing the default behavior)
unless you know what you're doing, you typically don't declare an el property in a collection view. Instead you declare a region (possibly within a layout), where you will call the show method to display a view instance
The reason your code probably doesn't work is that it looks like Backbone code with some Marionette stuff thrown in. Take a look at the free sample to my book on Marionette. It should get you started quickly with Marionette and will explain most of what you're trying to accomplish here.

Backbone.js collection fetch 'this._byId' undefined

I'm using coffeescript. My code is pretty simple:
class SomeCollection extends Backbone.Collection
constructor: (#options) ->
url: ->
"#{$SCRIPT_ROOT}/some/data/#{#options.someId}"
model: SomeModel
class SomeView extends Backbone.View
initialize: ->
myCollection = new SomeCollection()
myCollection.fetch
success: (coll, resp) ->
console.log coll
The JSON that's being returned from my collection's url is exactly:
[{"id": 1, "comments": "", "name": "images/exceptions/59.png"}]
However, before anything is printed to the console, I receive a backbone.js error on line 768: Cannot read property 1 of undefined. The undefined object is this._byId within the collection's get function. How can I solve this problem?
You are extending Backbone.Collection and providing your own constructor, so you need to make sure to call the parent constructor.
constructor: (#options) ->
super null, #options
Also, the standard arguments for a Collection are (models, options), so I would stick with that.
constructor: (models, #options) ->
super models, #options
Or better yet, use initialize instead of constructor to avoid that entirely
initialize: (models, #options) ->

Categories

Resources