I am trying to listen for when my view's "attachmentClicked" function is called in another object. Here is the class which originally calls the event:
class AttachmentView extends AttachmentViewerView
template: _.template($('#AttachmentViewTemplate').html())
className: "attachmentView"
#
# initialize ->
#
initialize: ->
console.log "AttachmentView initialized"
#render()
events: {
'click' : 'attachmentClicked'
'dblclick' : 'openAttachment'
}
#
# render ->
#
render: ->
#$el.html(#template(#model.toJSON()))
$('div.attachmentViewerView').append(#el)
# #bind 'event', method
#
# attachmentClicked ->
#
attachmentClicked: ->
#$el.addClass('selectedAttachmentView')
this object calls attachmentClicked upon click, now in another class which created this object, I am trying to listen for that event. Here is that class
class AttachmentViewerView extends AttachmentAppController
template: _.template($('#AttachmentViewerTemplate').html())
className: "attachmentViewerView"
#
# initialize ->
#
initialize: (options) ->
console.log "AttachmentViewer initialized"
#office = options.office
#ticket = options.ticket
#attachmentViews = []
#render()
#
# render ->
#
render: ->
#$el.html(#template())
# Append to fileViewer Div
$('#attachmentViewerWindow').append(#el)
#renderFiles()
#
# bindEvents ->
#
bindEvents: (view) ->
#listenTo view, 'attachmentClicked', #attachmentClicked
#
# renderFiles ->
#
renderFiles: ->
#attachments = new AttachmentCollection({#office, #ticket})
#attachments.fetch({
success: (collection) =>
_.each collection.models, (model) =>
# Create the attachment views and bind events right away
#bindEvents new AttachmentView({model: model})
})
#
# attachmentClicked ->
#
attachmentClicked: (attachment) ->
console.log( # )
#$el.find('.selectedAttachmentView').removeClass('selectedAttachmentView') unless #selected == attachment
#selected = attachment
so what is happening is that when this class is created, it eventually calls renderFiles which fetches the files from the server, then it creates a view for each returned model and calls bindEvent with that as a param.
Then bindEvent tries to listen to that newly created item's attachmentClicked method and bind it to this classes attachmentClicked function. However, it doesn't work. I've tried several ways and am not sure where my issue is. Guidance would be greatly appreciated.
You're listening for 'attachmentClicked' events on your AttachmentView:
bindEvents: (view) ->
#listenTo view, 'attachmentClicked', #attachmentClicked
but I don't see anything that will trigger such an event. Setting up some DOM event handlers in a view like this:
events:
'click' : 'attachmentClicked'
'dblclick' : 'openAttachment'
merely means that a click will trigger an attachmentClicked call, it won't trigger an 'attachmentClicked' Backbone event; if you want that event then you'll have to trigger it yourself:
attachmentClicked: ->
#$el.addClass('selectedAttachmentView')
#trigger 'attachmentClicked'
Related
I am implementing a title bar and menu drawer using MaterialUI in a Hyperstack project. I have two components, a Header component, and a Menu component. The Menu component is the expandable Drawer. I am storing the state in the Header component and passing it and a handler to the Menu component to toggle the drawer state when the drawers close button is clicked. For some reason, the drawer is just toggling open and closed very rapidly and repeatedly.
The drawer was opening fine before I implemented the close button. I have tried moving the state up to the main app component and passing it all the way down, but it produces the same result. I tried setting a class function on the Header component and calling it from within the Menu component instead of passing in an event handler.
The Header component
class Header < HyperComponent
before_mount do
#drawer_open = false
end
def toggle_drawer
mutate #drawer_open = !#drawer_open
end
render(DIV) do
AppBar(position: 'static', class: 'appBar') do
Toolbar do
IconButton(class: 'menuButton', color: 'inherit', aria_label: 'Menu') do
MenuIcon(class: 'icon')
end
.on(:click) do
toggle_drawer
end
Typography(variant: 'h6', color: 'inherit', class: 'grow') { 'Admin Main' }
Button(color: 'inherit', class: 'float-right') { 'Login' } # unless App.history != '/admin'
end
end
Menu(open_drawer: #drawer_open, toggle_drawer: toggle_drawer)
end
end
The Menu component
class Menu < HyperComponent
param :open_drawer
param :toggle_drawer
def menu_items
%w(Main Inventory Customers)
end
def is_open?
#OpenDrawer
end
render(DIV) do
Drawer(className: 'drawer, drawerPaper', variant: 'persistent', anchor: 'left', open: is_open?) do
IconButton(className: 'drawerHeader') { ChevronLeftIcon() }
.on(:click) { #ToggleDrawer }
List do
menu_items.each do |mi|
ListItem(button: true, key: mi) { ListItemText(primary: mi) }
end
end
end
end
end
I expected for the drawer to open on the open button click and close when the close button is clicked, but it is just opening and closing very rapidly.
The reason its opening and closing rapidly is that you are passing the value of toggle_drawer from the Header component to the Menu component. Each time you call toggle_drawer it changes the state variable #drawer_open, and rerenders the component, and then lather-rinse-repeat.
What you need to do is pass a proc to Menu, and then let Menu call the proc in the on_click handler.
So it would look like this:
class Header < HyperComponent
...
render(DIV) do
...
Menu(open_drawer: #drawer_open, toggle_drawer: method(:toggle_drawer))
end
end
and
class Menu < HyperComponent
...
param :toggle_drawer
...
IconButton(className: 'drawerHeader') { ChevronLeftIcon() }
.on(:click) { #ToggleDrawer.call } # note you have to say .call
...
end
By the way nice article here on how method(:toggle_drawer) works
and compares it the same behavior in Javascript.
But wait! Hyperstack has some nice syntactic sugar to make this more readable.
Instead of declaring toggle_drawer as a normal param, you should declare it with the fires method, indicating you are going to fire an event (or callback) to the calling component. This not only will make you life a little easier, but will also announce to the reader your intentions.
class Menu < HyperComponent
...
fires :toggle_drawer # toggle_drawer is a callback/event that we will fire!
...
IconButton(className: 'drawerHeader') { ChevronLeftIcon() }
.on(:click) { toggle_drawer! } # fire the toggle_drawer event (note the !)
...
end
now Header can use the normal event handler syntax:
class Header < HyperComponent
...
render(DIV) do
...
Menu(open_drawer: #drawer_open)
.on(:toggle_drawer) { toggle_drawer }
end
end
BTW if I could give a little style advice: Since the Menu can only close the drawer that is what I would call the event, and in the event handler I would just directly mutate the drawer state (and just lose the toggle_drawer method).
This way reading the code is very clear what state you are transitioning to.
The resulting code would look like this:
class Header < HyperComponent
before_mount do
#drawer_open = false # fyi you don't need this, but its also not bad practice
end
render(DIV) do
AppBar(position: 'static', class: 'appBar') do
Toolbar do
IconButton(class: 'menuButton', color: 'inherit', aria_label: 'Menu') do
MenuIcon(class: 'icon')
end.on(:click) { mutate #drawer_open = true }
Typography(variant: 'h6', color: 'inherit', class: 'grow') { 'Admin Main' }
Button(color: 'inherit', class: 'float-right') { 'Login' } # unless App.history != '/admin'
end
end
Menu(open_drawer: #drawer_open)
.on(:close_drawer) { mutate #drawer_open = false }
end
end
The problem is that after the second show view. this.ui.uielemnet return not an element but only a string selector. Events and other logic works well but i cant get element after secondshow vie. In the firstshow view` everything works as planed.
code is below. i add console log for explain. CoffeeScript)
Router:
class App.Routers.PanelRouter extends Marionette.AppRouter
initialize: (options = {}) ->
#mainView = options.cpView
routes:
'sbis-docs(/)': 'sbisDocShow'
'sbis-send(/)': 'sbisSendShow'
sbisDocShow: ->
view = new App.Views.SbisDoc
#mainView.getRegion('childRegion').show view
view.showTable()
sbisSendShow: ->
view = new App.Views.SendSbis
#mainView.getRegion('childRegion').show view
part of view
class App.Views.SendSbis extends Marionette.View
template: _.template(App.Templates.SbisSend);
initialize:() ->
vent.on('event:change-search-method', #changeSearchMethod)
vent.on('event:change-send-method', #changeSendMethod)
changeSearchMethod: (data) =>
if data.checked
#ui.cust.attr('placeholder', 'Customer ID')
#ui.labelCust.text('Номер договора')
else
#ui.cust.attr('placeholder', 'Логин')
#ui.labelCust.text('Логин пользователя')
changeSendMethod: (data) =>
console.log #ui.month
if data.checked
#ui.month.prop('disabled', false)
else
#ui.month.prop('disabled', true)
ui:
sendDocs: '#send-docs'
form: '#form-docs'
cust: '#cust'
year: '#year'
month: '#month'
labelCust:'#label-cust'
Other view:
class App.Views.SetupSend extends Marionette.View
template: _.template(App.Templates.SetupSend)
onAttach: ->
#ui.checkboxes.bootstrapToggle()
ui:
search: '#search-method'
send: '#send-method'
checkboxes: 'input[type=checkbox][data-toggle^=toggle]'
events:
'change #ui.search': 'changeSearchMethod'
'change #ui.send': 'changeSendMethod'
changeSearchMethod: (e) ->
vent.trigger('event:change-search-method', e.target)
changeSendMethod: (e) ->
vent.trigger('event:change-send-method', e.target)
In first time in changeSendMethod: (data) console log is
[input#month.form-control, prevObject: r.fn.init[1]]
But when i change route and comeback again console.log is
#month
If i change #ui.month.prop('disabled', false) on $(#ui.month).prop('disabled', false) it will be work. But i don understand why it happens and how i can fix it.
Well, the problem was that after new show view append new vent.on('event:change-search-method', #changeSearchMethod)
solve in my case
onDestroy: () ->
vent.off('event:change-search-method', #changeSearchMethod)
vent.off('event:change-send-method', #changeSendMethod)
I would like to use the bubling behaviour of collection views but it doesn't seem to work.
A bit of context: I display a modal AddPartFromPurchase that show a table populated with a collectionView. This works well.
When the user clicks a row, the itemview triggers the purchase:chosen event so according to the documentation, I expect the collection view to receive the itemview:purchase:chosen event but no such event is ever triggered: neither in AddPartFromPurchase or Purchases. :(
Here is the example code.
AddPartFromPurchase = Backbone.Marionette.ItemView.extend
template: 'pages/vehicles/modifications/add_part_from_purchase'
initialize: (attributes)->
#purchases = attributes.purchases
onRender: ->
view = new Purchases(el: #$('tbody'), collection: #purchases)
#bindTo(view, 'all', #foo)
view.render()
foo: (event, foo, bar, baz)->
console.log(event, foo, bar, baz)
Purchase = Backbone.Marionette.ItemView.extend
template: 'pages/vehicles/modifications/purchase'
tagName: 'tr'
events:
'click' : 'selectPurchase'
selectPurchase: ->
#trigger('purchase:chosen', #model)
false
serializeData: ->
purchase: #model
part: #model.get('part')
Purchases = Backbone.Marionette.CollectionView.extend
itemView: Purchase
initialize: ->
#bindTo(#, 'all', #foo)
foo: (event, foo, bar, baz)->
console.log(event, foo, bar, baz)
Maybe I'm doing it wrong, I feel bad about defining the listener in the onRender, but as I use a el I can't do that in initialize.
How can I deal with that?
Answer based on comment stream: be sure you're on v0.7.6 or higher, when this feature was introduced.
Using chrome development tools. when I instantiate a view in the app I get this...
new job.SearchView
SearchView
$el: jQuery.fn.jQuery.init[1]
cid: "view8"
el: HTMLDivElement
mbti: function (){ return fn.apply(me, arguments); }
options: Object
__proto__: ctor
From with jasmine I get the following (and spec failure)
new job.SearchView
SearchView
$el: jQuery.fn.jQuery.init[0]
cid: "view17"
el: undefined
mbti: function (){ return fn.apply(me, arguments); }
options: Object
__proto__: ctor
Why would el: be undefined when instantiating in jasmine?
SearchView defined like this...
jQuery ->
class SearchView extends Backbone.View
el: '#search'
template: JST['resume']
<snip>
#job = if window.job then window.job else {}
#job.SearchView = SearchView
and the spec like this...
describe 'Search View', ->
it 'should be defined', ->
expect(job.SearchView).toBeDefined()
beforeEach ->
#view = new job.SearchView()
describe 'render', ->
it 'should render the task', ->
$el = #view.render().$el
expect($el).toBeDefined()
When you specify the el attribute in your view, this acts as a container. In your app #search exists in the DOM. In the jasmine test it doesn't.
You can create sandboxed containers using jasmine-jquery and use them when constructing your views in tests.
`
I'm creating a very simple backbone app to familiarize myself with the framework. All is working except for binding of model change events to a function in my view. I've checked previous questions on SO and none have helped.
My Model has one variable 'counter'. View has 2 buttons which increment or decrement model's 'counter'. Simple stuff. That's all working fine, but when I try and listen for model change events in the view, I only receive notification once -on model creation (when defaults are created, I assume).
I know that counter is being updated because if I manually call render after updating the model I can see the effect, but in the interest of better mvc-ish structure, I want to view to be notified of change events and to update itself.
Below is the coffeescript code.
$ ->
class Count extends Backbone.Model
defaults: counter : 0
change: -> console.log('changed')
class Spinner extends Backbone.View
el: $('#counterView')
initialize: =>
#model = new Count()
#model.bind 'change' , #update()
events:
'click button#incBtn' : 'inc'
'click button#decBtn' : 'dec'
inc: ->
#model.set counter : #model.get('counter') + 1
dec: ->
#model.set counter : #model.get('counter') - 1
update: ->
console.log('update')
$('#num').html(#model.get 'counter')
view = new Spinner()
HTML:
<body>
<div id="counterView">
<button id="incBtn">Increment</button>
<button id="decBtn">Decrement</button>
<div id="num">Number</div>
</div>
</body>
Thanks in advance.
b
Your error is here:
#model.bind 'change' , #update()
What you're telling it is to bind the change event to what #update() returns, when you want to bind it to #update itself. So it should be:
#model.bind 'change' , #update
(Without the brackets). As it were, Spinner.update would execute immediately on Spinner.initialize, exactly as you found out. A few more notes:
It's unnecessary to wait for document.ready to create your classes. You could do that first (outside of document.ready), and only instantiate the models, views etc on document.ready.
It seems kind of weird to create a new model inside of your view. You probably want to do something like this instead:
view = new Spinner(model: new Count)
Edit: As Trevor Burnham notes below, you want the fat arrow => on inc, dec and update. Fixed below.
Taken together:
class Count extends Backbone.Model
defaults: counter : 0
change: -> console.log('changed')
class Spinner extends Backbone.View
el: '#counterView'
initialize: =>
#model.bind 'change' , #update
events:
'click button#incBtn' : 'inc'
'click button#decBtn' : 'dec'
inc: =>
#model.set counter : #model.get('counter') + 1
dec: =>
#model.set counter : #model.get('counter') - 1
update: =>
console.log('update')
#$el.find('#num').html(#model.get 'counter')
$ ->
view = new Spinner(model: new Count)
Ok, so after some fiddling, the following is working. For some reason, when the model is updated, it fails to dispatch the event back to the view so I'm manually dispatching a change event from the model with:
#trigger('change')
Pretty clunky, but it's the only thing that works for me.
I also noticed quirky behaviour when using defaults, so I'm now setting counter with initialize (rather than through defaults). To see the unintended behaviour with defaults, uncomment defaults and comment out initialize. It's like change event is not dispatched whenever counter = original value of 0.
class Count extends Backbone.Model
#defaults: counter : 0
initialize: ->
#set counter : 0
change: ->
#trigger('change') #this lil fella made it work
$ ->
class Spinner extends Backbone.View
el: $('#counterView')
initialize: =>
#model = new Count
#model.bind 'change' , #update
#update()
events:
'click button#incBtn' : 'inc'
'click button#decBtn' : 'dec'
inc: =>
#model.set counter : #model.get('counter') + 1
dec: =>
#model.set counter : #model.get('counter') - 1
update: =>
$('#num').html(#model.get 'counter')
view = new Spinner()