Service property not updating consistently - javascript

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 ;)

Related

How to display a "not found" page with parameterised route in Durandal

I have a Durandal application, and I use router.mapUnknownRoutes to display a user-friendly error page if the URL does not correspond to a known route. This works fine -- if I go to, say /foo, and that doesn't match a route, then the module specified by mapUnknownRoutes is correctly displayed.
However I cannot find any way to display that same error page when I have a parameterised route, and the parameter does not match anything on the backend.
For example, say I have a route like people/:slug where the corresponding module's activate method looks like this:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
});
};
If I go to, say /people/foo, then the result depends on whether dataService.load('foo') returns data or an error:
If foo exists on the backend then no problem - the observables are set and the composition continues.
If foo doesn't exist, then the error is thrown (because there is no catch). This results in an unhandled error which causes the navigation to be cancelled and the router to stop working.
I know that I can return false from canActivate and the navigation will be cancelled in a cleaner way without borking the router. However this isn't what I want; I want an invalid URL to tell the user that something went wrong.
I know that I can return { redirect: 'not-found' } or something similar from canActivate. However this is terrible because it breaks the back button -- after the redirect happens, if the user presses back they go back to /people/foo which causes another error and therefore another redirect back to not-found.
I've tried a few different approaches, mostly involving adding a catch call to the promise definition:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
}).catch(function (err) {
// ... do something to indicate the error ...
});
};
Can the activate (or canActivate) notify the router that the route is in fact invalid, just as though it never matched in the first place?
Can the activate (or canActivate) issue a rewrite (as opposed to a redirect) so that the router will display a different module without changing the URL?
Can I directly compose some other module in place of the current module (and cancel the current module's composition)?
I've also tried an empty catch block, which swallows the error (and I can add a toast here to notify the user, which is better than nothing). However this causes a lot of binding errors because the observables expected by the view are never set. Potentially I can wrap the whole view in an if binding to prevent the errors, but this results in a blank page rather than an error message; or I have to put the error message into every single view that might fail to retrieve its data. Either way this is view pollution and not DRY because I should write the "not found" error message only once.
I just want an invalid URL (specifically a URL that matches a route but contains an invalid parameter value) to display a page that says "page not found". Surely this is something that other people want as well? Is there any way to achieve this?
I think you should be able to use the following from the activate or canActivate method.
router.navigate('not-found', {replace: true});
It turns out that Nathan's answer, while not quite right, has put me on the right track. What I have done seems a bit hacky but it does work.
There are two options that can be passed to router.navigate() - replace and trigger. Passing replace (which defaults to false) toggles between the history plugin using pushState and replaceState (or simulating the same using hash change events). Passing trigger (which defaults to true) toggles between actually loading the view (and changing the URL) vs only changing the URL in the address bar. This looks like what I want, only the wrong way around - I want to load a different view without changing the URL.
(There is some information about this in the docs, but it is not very thorough: http://durandaljs.com/documentation/Using-The-Router.html)
My solution is to navigate to the not-found module and activate it, then navigate back to the original URL without triggering activation.
So in my module that does the database lookup, in its activate, if the record is not found I call:
router.navigate('not-found?realUrl=' + document.location.pathname + document.location.hash, { replace: true, trigger: true });
(I realise the trigger: true is redundant but it makes it explicit).
Then in the not-found module, it has an activate that looks like:
if (params.realUrl) {
router.navigate(params.realUrl, { replace: true, trigger: false });
}
What the user sees is, it redirects to not-found?realUrl=people/joe and then immediately the URL changes back to people/joe while the not-found module is still displayed. Because these are both replace style navigations, if the user navigates back, they go to the previous entry, which is the page they came from before clicking the broken link (i.e. what the back button is supposed to do).
Like I said, this seems hacky and I don't like the URL flicker, but it seems like the best I can do, and most people won't notice the address bar.
Working repo that demonstrates this solution

AngularJS: Changing object does not update field but changing string directly does

I feel like this is something trivial, but I've been stuck for awhile.
I have an object user, set in the directive UserSettings. The directive's element contains a button with html {{user.name}} to open a model for user settings. When the page loads user.name is set.
The user settings form in the modal is contained by a controller called UserSettingsForm. I've been trying to debug the controller and I'm confused by the behavior I'm seeing.
console.log #$scope.user # debug to show user object is there
#$scope.test = angular.copy(#$scope.user) # set test equal to a copy of user
#$scope.test.name = 'wowee' # change test object's 'name' property
#$scope.user = angular.copy(#$scope.test) # set user back to test
console.log #$scope.test # test is changed
console.log #$scope.user # user is equivalent to test
The above debugging works as expected, but the unexpected part (for me, at least) is the fact that {{user.name}} in the nav bar is not being updated. But when I do #$scope.user.name = #$scope.test.name the {{user.name}} field in the HTML is updated.
I am admittedly an angular noob (even though this is probably a JavaScript concept), but the logic I'm having trouble with doesn't make sense to me and I would be very appreciative if someone could clear it up for me, and maybe even give me a push in the right direction as far as properly updating the user object to equal the test object. test will eventually be an instance of the user settings form and only when the data is saved successfully will that instance be saved as user.
Angular is still watching the previous reference, even after you do the change.
If you use:
angular.copy(source, destination)
It will deleted all of the previous properties and replace them with the source properties.
Here's the updated example for your case:
angular.copy($scope.test, $scope.user)
That statement should solve the issue.

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.

Computed property based on array values is not recomputed

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

Bind to error event of a model created by collection.create()?

I have a collection of Comments and a view which is used to create new comments. Each comment has some client side validation going on:
class Designer.Models.Comment extends Backbone.Model
validate: (attrs) ->
errors = []
# require presence of the body attribte
if _.isEmpty attrs.body
errors.push {"body":["can't be blank"]}
unless _.isEmpty errors
errors
The Comments collection is super simple:
class Designer.Collections.Comments extends Backbone.Collection
model: Designer.Models.Comment
I create comments in the NewComment view. This view has access to the comments collection and uses it to create new comments. However, validations fails in the Comment model don't seem to bubble up through the collection. Is there a batter way to do this?
class Designer.Views.NewComment extends Backbone.View
events:
'submit .new_comment' : 'handleSubmit'
initialize: ->
# this is where the problem is. I'm trying to bind to error events
# in the model created by the collection
#collection.bind 'error', #handleError
handleSubmit: (e) ->
e.preventDefault()
$newComment = this.$('#comment_body')
# this does fail (doesn't hit the server) if I try to create a comment with a blank 'body'
if #collection.create { body: $newComment.val() }
$newComment.val ''
this
# this never gets called
handleError: (model, errors) =>
console.log "Error registered", args
The problem is that the collection event that aggregates all of the model events hasn't been hooked up yet. That hookup happens in the _add() function. Since the validation fails before the model gets added, you don't get the event.
The only indication of failure happens when create returns false but it looks like you've figured that out already.
If you need the validation errors, you will need to come up with a way to get the errors to you.
One way would be to fire off an EventAggregator message inside the validator. The other would be to circumvent or re-define the Collection.create function to hook the error event on the model.
Something like this?
model = new Designer.Models.Comment()
model.bind "error", #handleError
if model.set body: $newComment.val()
model.save success: -> #collection.add(model)

Categories

Resources