Computed property based on array values is not recomputed - javascript

Problem Space
I'm rendering some nested Ember views so I can make a splitter-pane style UI. I want to resize my views when they first render so that they'll have equal widths. I don't want my child views looking at each other, so I'm using a subclass of Ember.ContainerView to hold my content and draggable splitter handles.
I can't use Ember.View#didInsertElement on my container view, because I need to wait for my child views to be fully rendered.
My (attempted) Solution
I'm using the code presented in this answer: How to wait for a template to be fully rendered. This adds a property isRendered to all Ember.View instances that is set automatically when a template fires didInsertElement by re-opening Ember.View:
Ember.View.reopen
didInsertElement: ->
res = #_super();
#_setIsRendered();
res
_setIsRendered: ->
if (!! #$())
#set('isRendered', true)
else
Ember.run.next this, ->
#_setIsRendered()
I tried re-opening Ember.ContainerView to add a childViewsRendered property to all container views, but Ember objected and threw some very strange IndexOutOfBounds errors for container views with only one item in childViews.
I ended up putting my collection code in the following mixin:
App.ChildrenRendered = Ember.Mixin.create
childViewsRendered: (->
res = #get('childViews').everyProperty('isRendered')
console.log('childViewsRendered', res, this)
# Pointer to this most offensive object for debugging
window.wtf = this
res
).property('childViews.#each.isRendered')
_runChildViewsDidRender: (->
if #get('childViewsRendered')
console.log('trying to invoke childViewsDidRender')
Ember.tryInvoke(this, 'childViewsDidRender')
).observes('childViewsRendered')
And then I have a class like this:
App.SplitterView = Ember.ContainerView.extend App.ChildrenRendered,
# ...(some properties)...
init: ->
child_views = #get('childViews')
child_views.pushObjects([App.WindowView.create(), App.WindowView.create()])
What works:
App.SplitterView#childViewsRendered is computed once, before any views have rendered, and thus becomes false
Views are processed (inserted and rendered) by Ember, and set their own isRendered property fine and dandy.
Later running window.wtf.get('childViews').everyProperty('isRendered') returns true.
What doesn't work:
the computed property childViewsRendered never updates itself again.
Computed properties on dummy values on childView array element members also don't seem to work.

Without a jsFiddle to see I can only go by what you've said and the snippets you've provided but it seems like all you're trying to do is make sure all the childViews of a container are in the DOM before doing something, yes?
If that's the case... when views are added to childViews they are, as you know, automatically rendered and inserted into the DOM. Well this all happens within the same RunLoop. So to delay the execution of some function until all children are "inDOM" it's as easy as observing childViews.#each and using Ember.run.next to delay the execution until the next RunLoop. Here is an example
App.SomeContainerView = Em.ContainerView.extend
init: ->
#_super() # run default init
# add 2 of our 4 views
#get('childViews').pushObjects [#aView.create(), #bView.create()]
# a non-cached (volatile) computed property to check if children are inDOM
# just used for demo purposes
childrenAreInDom: (->
#get('childViews').every (v) ->
v.state is "inDOM"
).property().volatile()
# our observer
observeChildren: (->
# if the container isn't inDOM yet the afterRender function we add below
# will handle it, if views are added/removed after that then we handle it here
return unless #state is "inDOM"
# output to console if every childview are inDOM
console.log "observeChildren", #get('childrenAreInDom')
# the above will always be false since we're still in the same RunLoop
# next RunLoop our function will run
Ember.run.next #, 'doWhatever'
).observes('childViews.#each') # observe changes in the childViews array
afterRender: ->
console.log "afterRender", #get('childrenAreInDom')
Ember.run.next #, 'doWhatever'
# the function we want to run eventually after children are inDOM
doWhatever: ->
console.log "doWhatever"
# print out childrenAreInDom one more time.. it will be true
console.log "childrenAreInDom:", #get('childrenAreInDom')
# some views to insert
aView: Em.View.extend
template: Em.Handlebars.compile("A")
bView: Em.View.extend
template: Em.Handlebars.compile("B")
cView: Em.View.extend
template: Em.Handlebars.compile("C")
dView: Em.View.extend
template: Em.Handlebars.compile("D")
If you were to have {{view App.SomeContainerView}} in a template, in the console you'd see:
afterRender false
doWhatever
childrenAreInDom true
If you were to then programmatically add cView & dView via pushObjects after the container is already in the DOM, you'd see
observeChildren false
doWhatever
childrenAreInDom true
Even if this isn't exactly what you want hopefully it helps you get where you need to be without all that Mixin nonsense :D

Related

Service property not updating consistently

So this one may be a little involved. The full app, if you get really stumped and want to run it for yourself, is on github.
(If you do, you'll need to login, username and password is in API/tasks/populate.rake... just don't tell anyone else, k?)
I'm using Ember State Services to keep track of the isClean state of an editor component (which is using hallo.js).
Here's some sample code, with the scenario, and the problem I'm having with it:
When the content in the editor is changed, hallo.js fires a hallomodified event. In the component's didInsertElement hook, I'm attaching a jquery event listener, which sets the isClean property to false, and logs it to the console so we can check it's actually working:
JfEdit = Ember.Component.extend
editorService: Ember.inject.service('jf-edit')
editorState: Ember.computed('page', ->
#editorService.stateFor(#page) # page is passed in to the component
# in the route template
).readOnly()
# ...
didInsertElement: ->
self = #
#$().on("hallomodified", ->
console.log "modified"
self.get('editorState.isClean') = false
console.dir self.get('editorState') # -> Logs: Class -> __ember123456789:null,
# isClean: false
# in the console (but I have to click on
# isClean to show the value)
).hallo(
# ...
)
`export default JfEdit`
# Full code is at https://github.com/clov3rly/JoyFarm/blob/master/app/app/components/jf-edit.em
# (written in emberscript, which is an adaptation of coffeescript)
This seems to work, editorState.isClean = false in the console.
So then, when we attempt to transition away from the page, we check the editor state, in order to prompt the user to save.
NewPostRoute = Ember.Route.extend
model: ->
#*.store.createRecord 'post',
title: "New Post"
content: "Content here."
editorService: Ember.inject.service('jf-edit')
editorState: Ember.computed('model', ->
#editorService.stateFor(#model)
).readOnly().volatile()
actions:
willTransition: (transition) ->
model = #modelFor('posts/new')
console.log "editorState:", #get('editorState.isClean')
# -> logs true, after the log in
# the file above logged false.
console.dir #get('editorState') # -> Logs: Class ->
# __ember123456789:null
# (the same number as above)
# but the isClean property is
# not in the log statement
# ...
unless #get('editorState.isClean')
confirm("Do you want to discard your changes?") || transition.abort()
# Full code at https://github.com/clov3rly/JoyFarm/blob/master/app/app/routes/posts/new.em
So now, editorState.isClean is returning false (and not showing up when logging the object itself). However, the first property of the object has the same key, which I'm assuming is an ember ID of some sort?
The template looks like this:
{{{jf-edit page=model save="save" class="blog editor"}}}
So, page in the component/jf-edit file should be the same object as model in the routes/new file.
The service/state files are pretty simple, if you want to see them, you can find them in this gist (or in the repo itself).
Turns out this problem goes away if I store the editorState on the controller, and look up this.controller.editorState in the route willTransition action handler.
Thanks Grapho and locks on IRC ;)

Click event handler only working once in Backbone.js

I'm new to Backbone and am using it with jQuery in Rails. In my view's render method, I use delegateEvents to bind an event handler to the "click" event of a button with id btn-go. This button is itself rendered by the view in question.
Clicking the button stores the form's values in an array and causes the view to re-render. This button is itself rendered by the view in question. This works the first time I click the button, but nothing happens the second time, even though the view does correctly re-render its template.
class MyApp.Views.ChainsNew extends Backbone.View
# #template is defined by a conditional inside render()
render: (step_number) ->
window.model = #model
#step_number = step_number
#template = if #step_number is 1 then JST['chains/new'] else JST['chains/step']
$(#el).html(#template())
#delegateEvents({
'click #btn-go': 'add_step'
})
#
add_step: ->
#divide array into arrays of steps before and after step being edited
steps = #model.get('steps')
array1 = steps.slice(0, #step_number - 1)
array2 = steps.slice(#step_number)
array1.push(#$el.find('textarea').val())
newArray = array2.concat(array1)
#model.set({
steps: newArray
})
The view's render method is called by the router. As you can see in the code below, the router is listening to the change event on the model. This causes it to update the URL, which in turn triggers the router's step method to be called, and it's within that method that the view's render method is finally called.
class MyApp.Routers.Chains extends Backbone.Router
routes:
'chains/new(/)': 'new'
'chains/new/step/:step_number(/)': 'step'
initialize: ->
# Model
#model = new MyApp.Models.Chain()
#listenTo(#model, "change", ->
#goto_step_number #model.get('steps').length + 1
)
# Views
#view_new = new MyApp.Views.ChainsNew({
model: #model
})
step: (url_step_number) ->
# Before rendering the view, make sure URL & number of steps in model are correctly related
url_step_number = parseInt url_step_number
steps_entered = #model.get('steps').length
if url_step_number > steps_entered + 1
#goto_step_number steps_entered + 1
else
$('#main-container').html(#view_new.render(url_step_number).el)
new: ->
#goto_step_number 1
goto_step_number: (step_number) ->
#.navigate('chains/new/step/' + step_number, trigger: true)
Why doesn't anything happen the second time I click the button? I'm guessing that the event handler hasn't been correctly bound to the button, but I have no idea why.
Your problem is right here:
$('#main-container').html(#view_new.render(url_step_number).el)
From the fine manual:
.html( htmlString )
[...]
When .html() is used to set an element's content, any content that was in that element is completely replaced by the new content. Additionally, jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.
Note the removes other constructs such as data and event handlers part. The sequence of events goes like this:
You call render.
render calls delegateEvents to attached a jQuery event delegator to the view's el.
You call $x.html(view.el) but view.el is already there so jQuery detaches all the event bindings (including the one you just added in 2), clears out $x, and then puts view.el back into $x.
But when view.el is put back on the page, the events are already gone. This is roughly equivalent to what you're doing:
# In the view...
add_step: ->
re_render(#step_number + 1)
and
v = new YourView
$('#main-container').append(v.render(1).el)
re_render = (step_number) ->
$('#main-container').html(v.render(step_number).el)
Demo: http://jsfiddle.net/ambiguous/4rJyB/
You need to stop calling .html all the time. Once the view is on the page, you can simply tell it to re-render itself and that's all you need to do. So, if the view has been rendered once to get its el into #main-container, you just need to:
#view_new.render(url_step_number)
and that's it. Then you can remove the #delegateEvents call from render and use the usual events map on the view:
class MyApp.Views.ChainsNew extends Backbone.View
events:
'click #btn-go': 'add_step'
render: (step_number) ->
window.model = #model
#step_number = step_number
#template = if #step_number is 1 then JST['chains/new'] else JST['chains/step']
#$el.html(#template())
#
#...

Backbone constructor calls itself

I've encountered a problem I don't understand. I'm playing with Backbone and one of my initializer is called twice, one on purpose (when I instantiate my object) and it seems like it's called a second time from the constructor itself.
Here is my code :
class Views extends Backbone.Collection
model: View
initialize: ->
_.bindAll #
class View extends Backbone.View
initialize: ->
_.bindAll #
console.error 'Inner'
views = new Views
console.log 'Outer'
views.add new View
When I run this code, Outer is displayed once while Inner is displayed 2 times. Here is the stack trace :
Any idea about this ?
When you initialize a collection, the first argument is the list of models to pre-populate it with.
class Models extends Backbone.Collection
model: Model
initialize: (#rawModels) ->
# CoffeeScript has the fat arrow that renders this unnecessary.
# But it's something you should use as sparingly as possible.
# Whatever. Not the time to get into that argument.
_.bindAll #
# At this point in time, all the models have been added to the
# collection. Here, you add them again. IF the models have a
# primary key attribute, this will detect that they already
# exist, and not actually add them twice, but this is still
# unnecessary.
_.each #rawModels, #addItem
# assuming this was a typo
addItem: ( place ) -> #add new Model model
models = new Models json
Not directly related to your question, but hopefully helpful.
More directly related: don't create a collection of views. Collections are for storing Models. Backbone.View is not a type of Backbone.Model; they're separate. It doesn't really make sense -- you can just create an array of views -- and a lot of operations won't work right on that view collection.
Which is what's happening here.
When you call Backbone.Collection::add, it tries to see if what you're adding is a Backbone.Model. Since it's not, it assumes you're trying to add a JSON blob that it wants to turn into a Model. So it tries to do that...using its this.model class as a guide. But since that's View, it creates another one and adds that instead (not checking after the fact that it actually produced a Backbone.Model).
You can follow the call stack from add to set to _prepareModel, where the second View is instantiated.

Backbone context issue with coffeescript

I have a simple calendar that re-renders whenever the date changes. After onClick the date is set triggering the change:date event and the render method is called.
However, the context is off as the html is not being replaced.
I don't know why this is so because I am using => to preserve the context and when I console.log #$el it always shows me the same class ie. WidgetView
It works when I do $(elementName).html but not with #$el.html. Ideas?
class WidgetView extends sandbox.mvc.View
className: 'sidebar-group'
events:
"click a" : "onClick"
template: sandbox.template.compile tmpl
initialize: (options) ->
#date = new DateModel {rid:1000}
#listenTo #date, "change:date", #render
onClick: (e) ->
e.preventDefault()
# Get value
value = $(e.currentTarget).text()
# Set date model
#date.set {date:value}
# Emit model change event
sandbox.emit "model.date.change", #date
return false
render: (model) =>
data = CalendarResource()
#$el.html #template {calendar: data}
#
I think the problem is pretty simple. Look at listenTo closely if you didn't read it.
object.listenTo(other, event, callback)
Unlike bind
object.on(event, callback, [context])
If you see listenTo doesn't set the context of your callback in any shape or form.
Try just doing
#listenTo #date, "change:date", _.bind(#render, this);
Bleh I'm not a coffee scripter but you get the point. Force the context in the constructor to always be the instance.
Problem
The issue for this particular problem was that I was using the JQuery Page Slide plugin which was copying over the element to another node.
As a result, I was updating the reference node and the plugin was not keeping the new node in sync.
Solution
Since there was no refresh method for the plugin, I will have to copy over the elements and rebind all events.

Why is backbone.js returning an empty array when accessing models?

I have a router accessing its collection. My for loop wasn't iterating through the models so I tried logging the collection to see what it returned. Turns out when I log the collection directly I see all of the models as expected. But if I try to log the models attribute of the collection I get an empty array! It doesn't make sense. These lines are directly following each other. I tried changing the order and got the same outcome.
console.log(this.collection);
=> Shots
_byCid: Object
_byId: Object
length: 15
models: Array[15]
__proto__: Shots
...
console.log(this.collection.models);
=> []
console.log(this.collection.length);
=> 0
Why would this happen?
Here is the code as it is in the router to give a better context of where this code is firing:
# Routers
class Draft.Routers.Shots extends Backbone.Router
routes:
'' : 'index'
'shots/:id' : 'show'
initialize: ->
#collection = new Draft.Collections.Shots()
#collection.fetch()
index: ->
console.log #collection
console.log #collection.models
Jim,
This doesn't fix your problem - you've worked that out. But it explains why you're seeing the console output you see.
When you run console.log(this), you output the object itself and the console links references (pointers if you like) to the inner variables.
When you're looking at it in the console, at the time the console.log(this) runs the models area is empty, but at the time you look at the logs, the collection has finished loading the models and the inner array variable is updated, AND the reference to that variable in the object log shows the current content.
Basically in console.log(this),inner models variable continues its normal life and the console shows the current status at the time you're looking at it, not at the time you called it.
With console.log(this.models), the array is dumped as is, no reference is kept and all the inner values are dumped one by one..
That behaviour is quite simple to reproduce with a short timeout, see this fiddle.. http://jsfiddle.net/bendog/XVkHW/
I found that I needed to listen for the collection to reset. So instead of passing the model into the view I created another view expecting the collection and listened for the 'reset' event to fire 'render' for the view.
# Routers
class Draft.Routers.Shots extends Backbone.Router
routes:
'' : 'index'
'shots/:id' : 'show'
initialize: ->
#collection = new Draft.Collections.Shots()
#collection.fetch()
index: ->
view = new Draft.Views.Desktop(collection: #collection)
# Views
class Draft.Views.Desktop extends Backbone.View
el: $("body")
initialize: ->
#collection.on("reset",#render,this)
render: ->
console.log #collection
console.log #collection.length
You can use a promise. (.done will do fine)
#collection.fetch().done =>
for model in #collection.models
console.log model
this will give you #collection's models fetched and ready to go.
or if you don't need to force the app to wait,
#collection.on 'sync', =>
for model in #collection.models
console.log model
Both of these will let you do what you want.

Categories

Resources