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.
Related
I have an AngularJS application that manages badges. In the application is a form to set the badge # and the name of the person it is assigned to, etc. This gets stored in $scope.badge.
When the user submits the form, I want to add the new badge to a list of badges, which is displayed below the form.
Partial code looks like this:
var badge = angular.copy($scope.badge); // make a copy so we don't keep adding the same object
$scope.badgeList.push(badge);
The first time I run this code, it adds the badge as expected.
Any subsequent time I run this code, the next badge REPLACES the previous badge in the badgeList. In other words, if I add 5 badges, the badgeList still only has 1 object in it because it just keeps getting replaced.
I'm thinking that this may be happening because the same object keeps getting added? Maybe I'm wrong? I am using angular.copy to try and avoid that happening, but it doesn't seem to be working.
Any thoughts on this?
$scope.badgeList.push(($scope.badge);
console.log($scope.badgeList)
no need to use angular.copy since you are ultimately storing all the badges in an array
angular.copy is used when you want to make a clone of object and not update the existing object and the clone's change are not reflected in main object.
If you just want to maintain a list of badges you can execute this block of code
like this
function addBadges(){
$scope.badgeList.push(($scope.badge);
console.log($scope.badgeList)
}
If you are refreshing the controller then obviously the variable will be reset and for such a case you need to make use of angular services.
Create a service and inside the service you need to define getter and setter method that will help in data persistence
and your bages array if saved in service will persist till the application is in foreground.
You could do something like this.
function addBadges(){
//initialize if undefined or null
if(!$scope.badgeList){
$scope.badgeList = [];
}
//Check if badge does not exists in the list
if ($scope.badgeList.indexOf($scope.badge) === -1) {
//Add to badge list
$scope.badgeList.push($scope.badge);
}
}
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.
Abstract
Hi, I'm using angular + ui-router in my project, I have huge amount of nested states and different views that in turn contain huge amount of different inputs, a user fills these inputs incrementally step by step.
The problem
Sometimes users require additional info that is located on the previous step, and browsers "back" button helps the user to sneak peek into that data, but as soon as the user presses it, the info he already entered is lost due to state transition, which is obviously a bad thing.
Strategy
In order to overcome described problem I have the following plan:
Associate each user's "navigation" (guess this is a proper term) with a random id
To prevent scope-inheritance-and-serialization issues, instead of putting viewmodel into $scope use ordinary javascript object that will be storing immediate values that are bound to UI.
Add watcher to look for changes on that "storage object"
As soon as the change spotted, serialize the object and persist it
Explanations
Why do we need a random parameter in URL?
We don't want to store all data in URL, since there might be quite some amount of data that wont fit into URL. So in order to provide the guarantees the URL won't break, we put only small random GUID/UUID into it that later allows obtaining the data associated with current "navigation" by this random GUID/UUID.
The storage
There are multitude of storage scenarios available out there: LocalStorage, IndexedDB, WebSQL, Session Storage, you name it, but due to their cross-tab, cross-browser, browser-specific nature it would be hard to manipulate and manage all of the data that gets into the storage. The implementation will be buggy / might require server-side support.
So the most elegant storage strategy for this scenario would be storing data in special window.name variable which is capable of storing data in-between requests. So the data is safe until you close your tab.
The Question
On behalf of everything written above, I have the root view called "view" that has a state parameter id (this is the random GUID/UUID)
$stateProvider.state('view', {
url: '/view/{id}',
controller: 'view',
templateUrl: 'views/view.html'
});
All of the other views derive from this view, is there way to make ui-sref directive to automatically inject a random GUID/UUID into id state parameter of my root view, instead of writing each time ui-sref's like:
<a ui-sref="view({id:guid()}).someNestedView({someNestedParam: getParam()})"
I would like to have something like:
<a ui-sref="view.someNestedView({someNestedParam: getParam()})"
The AOP and Decorator pattern are the answer. The comprehensive description could be found here:
Experiment: Decorating Directives by Jesus Rodriguez
Similar solution as described below, could be observed:
Changing the default behavior of $state.go() in ui.router to reload by default
How that would work? There is a link to working example
In this case, we do not solve from which source the random GUID comes from. Let's just have it in runtime:
var guidFromSomeSource = '70F81249-2487-47B8-9ADF-603F796FF999';
Now, we can inject an Decorator like this:
angular
.module('MyApp')
.config(function ($provide) {
$provide.decorator('$state', function ($delegate) {
// let's locally use 'state' name
var state = $delegate;
// let's extend this object with new function
// 'baseGo', which in fact, will keep the reference
// to the original 'go' function
state.baseGo = state.go;
// here comes our new 'go' decoration
var go = function (to, params, options) {
params = params || {};
// only in case of missing 'id'
// append our random/constant 'GUID'
if (angular.isUndefined(params.id)) {
params.id = guidFromSomeSource;
}
// return processing to the 'baseGo' - original
this.baseGo(to, params, options);
};
// assign new 'go', right now decorating the old 'go'
state.go = go;
return $delegate;
});
})
Code should be self explanatory, check it in action here
I noticed a behavior in Ember that does not make any sense to me. I am not sure if this is a bug or feature. In the latter case I am really interested why this is a desired behavior. So here we go:
Make sure you can see your browsers console output.
Open the example project on JS Bin. Notice the two init messages comming from IndexController and FoobarController.
Click on the button saying 'Add one'. Do this so that there is some state on the FoobarController.
Click on the 'Go to hello' link to transition to the hello route
Go back to index via the link
The count variable still has the value. All good!
Now there is a tiny change in the next JS Bin. I pass the model to the render helper.
Follow the steps above again. After step 5 you see that count is 0 now and that the 'init FoobarController' appears again.
Somehow a controller belonging to a render helper gets reset when a model is passed. I can't find any information on why this happens or think of any reason why this makes sense.
From the Docs
If a model property path is specified, then a new instance of the controller will be created and {{render}} can be used multiple times with the same name.
Passing that second param re-instantiates the FoobarController, which basically resets the count to 0, whereas not passing the model param creates a singleton instance of the FoobarController.
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.