On a project I'm working on, a number of checkboxes in a view are used to apply and remove a set of filters from a search result set. The values of these checkboxes are written to the query string to facilitate the ability to pre-load a filter state via navigation. To simplify, I'll pretend there is only one checkbox and ignore the fact that there's supposed to be some filtering.
To ensure the state in the query string is correct, when a check-box is checked, a function is called which writes the checkbox's value to the query string. The checkbox's value is bound to a scope property called "checkBoxValue".
<input ng-model="checkBoxValue"
ng-change="WriteValueToQueryString()"
type="checkbox"/>
In addition to this, when there is an update to the query string, I call a function which gets the current value from the query string and writes it to $scope.checkBoxValue. It ensures that the value in my controller and the value in the query string are in sync with one another, regardless of where a change in value originates. The altogether simplified version of this all looks like this:
app = angular.module 'myApp', []
app.controller 'MyCtrl', ['$scope', '$location', ($scope, $location) ->
$scope.GetValueFromQueryString = () ->
queryStringValue = $location.search()['checkbox'] or false
#making sure I write a true/false and not "true"/"false"
$scope.checkBoxValue = queryStringValue is true or queryStringValue is 'true'
$scope.WriteValueToQueryString = () ->
$location.search 'checkbox', $scope.checkBoxValue.toString()
$scope.$on '$locationChangeSuccess', () ->
$scope.GetValueFromQueryString()
]
When I check the box, it changes checkBoxValue, updates the value in the query string, and everything is just lovely. If I press the "Back" button in my browser, it notices that the location has changed, updates its value to match the query string (efectively undoing my last), and everything is just lovely.
...Unless I'm using Internet Explorer 10 or 11...
In IE10 and IE11, the browser "Back" action doesn't appear to do anything. In reality, it is performing a browser "Back" action (when I look at it, the query string reads as it should after a browser "Back" action). The problem seems to be that $locationChangeSuccess never fires. The value is thus never retrieved from the query string, which means that my checkbox's actual value is never updated from the query string. Eventually, if you press "Back" enough, it just boots you off to whatever actual page you were on before you got to my own page, checkbox still in the state it was before you started hitting "Back".
Funny thing is? IE8 and IE9 have no problems with this.
Link to CodePen (try it in IE10 or IE11): http://codepen.io/anon/pen/zaGwt/
Thanks for reading my ramblings for this long. Any help you could give on this issue would be tremendously appreciated!
Things I've tried:
$scope.$watch ( () -> $location.search() ), () ->
$scope.GetValueFromQueryString()
Which appears to behave identically to what's listed above. No dice.
Related
I have a component listing-table which takes a number of properties, like this:
{{listing-table model=model.devices type='user' exclude='customerName'}}
This works as intended, and the integration tests also work just fine. However, my acceptance tests fail, because apparently my exclude property is not being taken into account while running an acceptance test.
I have tested this by printing to console the value of this.get('exclude') in the component's javascript file and getting undefined. However, printing e.g. this.get('type') yields the expected results.
I have then, for testing purposes, removed exclude and replaced type's value with it, i.e. type='endpointName,typeName', however, I would get the previous value in the console, e.g. user.
This is all way beyond puzzling, and I'd really like to know what's the matter with acceptance test. Any sort of hints are more than welcome, and thanks for your time!
EDIT:
I have now edited my acceptance test to exclude clicking through various elements to get to the route that contains my listing-table component:
From:
visit('/users/1')
click('a:contains("Devices")')
To:
visit('/users/1/devices')
And the test passes. I still don't understand why clicking through makes my component's properties disappear, whereas visiting the page directly works just fine.
EDIT 2:
So, here is some sample code. This is what my test looks like:
test('/customers/1/devices should display 5 devices', function (assert) {
let type = server.create('endpoint-type')
let user = server.create('user')
let endpoint = server.create('endpoint', { type })
server.createList('device', 5, { user })
visit('/customers');
click('a:contains("Customer 0")')
click('a:contains("Devices")')
andThen(function () {
assert.equal(find('.device-listing').length, 5, 'should see 5 listings')
assert.equal(find('th').text().trim(), 'IDModelManufacturerMACExtensionLocation', 'should only contain ID, Model, Manufacturer, MAC, Extension, and Location columns')
})
Now, my Devices table should, in this case, omit the 'Customer' column, however, the column does appear in there, even though my component in devices.show.customers has been invoked with:
{{listing-table model=model.devices type='user' exclude='customerName'}}
My listing-table.js file basically uses this.get('exclude') inside the init () function to process the excludes, but as I said, if I add a console.log(this.get('exclude') in that file, I get undefined.
EDIT 3:
After more testing, I have made some progress, and the resulting question needs its own page, here.
Just a few thoughts:
I assume this one has been done since you got green on your second attempt... are you using andThen to handle your assertions to make sure all of your async events are settled?
Is the model hook being triggered? Depending on how you enter the route, the model hook will sometimes not get triggred: Why isn't my ember.js route model being called?
Might be helpful to have some code to look at.
I am using flux in my application where I use Backbone.View as the view layer.
Generally there is a store instance for the whole page, the store save the data(or the state) of the application, and the view will listener to the change event of the store, when the store trigger a change event, the view will re-render itself accordingly.
So far so good, however I meet some problems when I use the form, when use try to submit the form or a blur event triggered for an element, I want to validate the input in the server and display the errors as soon as possible, this is what I have done:
when user hit the submit button or value changed for an element,I will dispatch an action like:
dispatch({type:"validate",value:"value"});
The store will respond to this action and send request to server
When the response get back,I will update the store and trigger the change event:
store.validate_response=response;
store.trigger("change");
The View(form in the example) will re-render itself.
I can display the errors but I can not keep the value of the element since the elements in the form are re-rendered which means they will display the origin value rather than the value the user typed.
I have thought that save the typed values too when dispatch the validate action like this:
dispatch({type:"validate",value:"value",userTypedValueForEveryElement:"....."});
It works when use hit the submit button, since generally when they hit the button they will not type anything in the form, but how about this situation:
<input type="text" id="A" />
<input type="text" id="B" />
User type avalue in input A, then type bv in input B, at the same time I will do the validation, and send both the value when dispatch the action:
{a:"avalue",b:"bv"}
The store will keep these values.
And during the request, user keep typing for element B, now the value is bvalue, and at the same time the validation response returned, then the form will re-render, and it will set avalue for A and bv for B, this is the point, the value of the B is lost, user will be surprised, they do not know what happened.
Any idea to fix that?
It seems that the flux manner:
view trigger action -->
store respond to actions -->
store trigger changed -->
view respond to store(re-render in most case) -->
view trigger action"
make this kind of requirement complex than that before. You will have to do more extra job to keep the state of the view once there are to much interactive for your view.
Is this true or beacuse I miss anything?
It sounds like you have a few different issues in play here, but they're all solvable. This is a little long, but hopefully it addresses all the issues you're running into.
Store design: First, what information is your Store actually meant to hold? Try not to think of a Flux store like you would a Backbone Model, because their purposes aren't quite the same. A Flux store should store part of an application's state (not necessarily part of a UI component's state), and shouldn't know or care about any views using it. Keeping this in mind can help you put behavior and data in the right places. So let's say your store is keeping track of the user's input into a specific form. Since your application cares about whether input is valid or not, you need to represent that in the store somehow. You could represent each input as an object in the store, like {val: 'someInput', isValid: false}. However you store it, it has to be there; any part of your app should be able to pull data from the store and know what input is valid/invalid.
I agree with #korven that putting lots of application logic in Stores is a poor choice. I put my AJAX calls into the action creation logic, with AJAX response callbacks creating the actual actions on the Dispatcher; I've seen this recommended more than once.
Preserving user input: For one, you only want to render the form inputs when the user has finished typing - otherwise, the render will change the text as they're typing it. That's easy enough -- throttle or debounce (debounce is probably preferable here) the input validation handler for user input events. (If you're using focus or blur events, timing is less likely to be an issue, but you should still consider it.) Have the store update only after validation is done. And, of course, only render when the store updates. So we only modify an input's value in the DOM when a user has stopped typing and we have validated their input.
Even with throttling/debouncing, since the validation requests are async and the user could (potentially) trigger many validation requests in a short period of time, you can't rely on the responses coming back in order. In other words, you can't process each response as they come back; if they come back out of order you'll overwrite recent input with old input. (I've run into this in real life. It may be an edge case for you but when it happens the bug will be confusing enough that it's worth addressing up front.) Fortunately, we only care about the most recent thing the user typed. So we can ignore all responses to our validation requests except the response for the most recent request. You can easily integrate this logic with whatever makes the requests by keeping track of a 'key' for each request. Here's an example of how I've solved this:
// something in your view
this.on(keyup, function() {
var input = this.getUserInput();
validationService.validate(input);
}
// within validationService
validate: function(input) {
// generate random int between 1 and 100
var randKey = Math.floor(Math.random() * (100 - 1)) + 1;
this.lastRequestKey = randKey;
this.doAjaxRequest({
data: {input: input},
callback: function() {
if (randKey !== this.lastRequestKey) {
// a newer request has modified this.lastRequestKey
return;
}
// do something to update the Store
});
}
In this example, the object responsible for the validation service 'remembers' only the most recently set 'key' for a request. Each callback has its original key in scope thanks to its closure, and it can check if its original key equals the one set on the service object. If not, that means another request has happened, and we no longer care about this response. You'll want the 'keys' to be set per-field, so that a new request for field B doesn't override an older request for field A. You can solve this in other ways, but the point is, discard all but the last request's response for any given input. This has the added bonus of saving some update/render cycles on those discarded responses.
Multiple fields rendering: When you're doing Flux right, you should never 'lose' data because all changes come from the Dispatcher/Store, and because the Dispatcher won't send a new update to stores until the previous update is completely finished. So as long as you update the Store with each new input, you won't lose anything. You don't have to worry about a change to input B causing you to lose a change to input A that was in progress, because the change to input A will flow from the Dispatcher to the Store to the View and finish rendering before the Dispatcher will allow the change to input B to begin processing. (That means renders should be fast, as they'll block the next operation. One of the reasons React goes well w/Flux.)
As long as you put everything into the store -- and don't put the wrong thing into the store, which the input and async handling stuff above addresses -- your UI will be accurate. The Flux pattern treats each change as an atomic transaction that's guaranteed to complete before the next change occurs.
When writing my react application I faced the exactly same issue. As a result I end up writing a small library to achieve the same.
https://www.npmjs.com/package/jsov
all you need to do is this, as soon as store triggers the change with the data they typed. There will be an onChange function in your component that will be listening to this change from store (and probably setting the state) now what you would do here is before setting the state use
onChange:function(){
var validated_response=JsOV.schemaValidator(Schema,Store.getResponse());
this.setState({data:validated_response});
}
P.S: To save the pain I have also provided a schema generator function in the library. It takes a dummy response and generates the schema boilerplate, to which you can add your own validations.
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
I am going through the Mithril tutorial and am having trouble understanding m.withAttr. The guide has the following line in the view layer:
m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()})
I have two questions.
1) I understand that the first half, onclick: m.withAttr("checked", task.done) means, essentially:
'set task.done, using m.prop, to the value of the "checked" attribute'. But what is the purpose of the second half, checked: task.done()? It seems like it is simply repeating the first half.
2) As I went through the tutorial, I wanted to add the functionality of persisting my Todos to a persistence layer. I created a save function, so that I could refactor the line that I referenced above into something like:
m("input[type=checkbox]", { onclick: todo.vm.markAsDone.bind(todo.vm, task)})
And in my view-model, I had the function:
vm.markAsDone = function(todo) {
m.withAttr("checked", todo.done), checked: todo.done();
todo.save();
};
But this did not work; I get an Uncaught SyntaxError: Unexpected token : error. I think the problem is that the event is not being properly bound to the markAsDone function, so it doesn't understand the "checked" attribute; but I'm not sure how to fix this problem (if that even is the problem).
Thanks for any help.
Question 1
The second parameter of the m() function defines attributes on the HTML element, in this case an <input type=checkbox> will be decorated. (The exception is the special config field)
checked determines if the input checkbox is checked, so it is required to display the state of the task.
onclick is the event handler that will modify the state.
So the attributes do different things, therefore both of them are needed.
Question 2
Since markAsDone is passed a todo model, you don't have to do any m.withAttr call there. Simply modify the model, and let Mithril redraw the view. The redraw happens automatically if you call markAsDone through an event like onclick.
If you want more information about the redraw procedure, I summarized it in a previous SO question.
Edit: markAsDone will probably look like this:
vm.markAsDone = function(todo) {
todo.done(true);
todo.save();
};
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.