I am displaying a view (ui-router) on the Ionic platform with a message list. New messages should be shown as unread, but then corresponding variables should be changed to read. The problem is that when I set the controller to change the proper variable, the view automatically updates itself immediately with this change and the messages are immediately displayed as read.
How can I deal with this?
In other words: in a variable-dependent view, I want to display the view first and then modify the variable, so that the change of the variable does not affect the view.
The current code (MarkAsRead() is called from within the template):
$scope.messages = $rootScope.Messages.Inbox;
$scope.MarkAsRead = function(ID_NR)
{
angular.forEach($rootScope.Messages.Inbox, function (value, key) {
if ((value.IsRead == 2) && (value.From == ID_NR))
{
value.IsRead = 3;
}
});
}
It's odd you're battling the main thing angular is known for: two-way binding. That said, if you really want to make changes to the variable without updating the view, you can use angular.copy to make a deep copy of the object and manipulate the copy itself. Or display the copy but manipulate the original
Another option, if you're using Angular > 1.3, you can use one-time binding to only update the view first time:
Simply prefix your expression with :: e.g
<p>{{::message.IsRead}}</p>
https://docs.angularjs.org/guide/expression#one-time-binding
Related
I not sure how to solve this issue. I am sure someone will know this very quickly.
I have an array of objects and modifying a property. I have a firebase listener 'child_changed'. When firebase is updated need to update the array. Here is the code below.
dbRefList.on('child_changed', function(snap) {
var len = this.grocerylist.length;
for(var i=len; i--;) {
if(this.grocerylist[i].key === snap.key) {
this.set(['grocerylist', i, 'selected'], snap.val().selected);
}
}
this.notifyPath('grocerylist', this.grocerylist.slice());
}.bind(this));
When the array is modified I want the template repeat-dom to trigger. I know this.set will not trigger array mutation sub properties but again I am not sure how to solve this. I done research and tried so many solutions.
I can force a render on the template dom-repeat but I would prefer the data binding way.
So this code (just the this.set you have in there now) should cause the value of grocerylist.i.selected to update inside the dom-repeat (assuming it's bound in there so it's actually showing up).
What behavior are you seeing? Are you trying to filter or sort the list based on the selected value? In that case, you might need to add observe="selected" on the dom-repeat.
(Also—have you confirmed that the child-changed callback is being called with the this value you expect—the element—rather than window or something else?)
You should be able to force a refresh by doing this.grocerylist = this.grocerylist.slice() or this.set('grocerylist', this.grocerylist.slice()); ... notifyPath doesn't work here because notifyPath doesn't change the value, it notifies the element about a prior change (the second argument is effectively ignored).
I'm an author of angular-input-modified directive.
This directive is used to track model's value and allows to check whether the value was modified and also provides reset() function to change value back to the initial state.
Right now, model's initial value is stored in the ngModelController.masterValue property and ngModelController.reset() function is provided. Please see the implementation.
I'm using the following statement: eval('$scope.' + modelPath + ' = modelCtrl.masterValue;'); in order to revert value back to it's initial state. modelPath here is actually a value of ng-model attribute. This was developed a way back and I don't like this approach, cause ng-model value can be a complex one and also nested scopes will break this functionality.
What is the best way to refactor this statement? How do I update model's value directly through the ngModel controller's interface?
The best solution I've found so far is to use the $parse service in order to parse the Angular's expression in the ng-model attribute and retrieve the setter function for it. Then we can change the model's value by calling this setter function with a new value.
Example:
function reset () {
var modelValueSetter = $parse(attrs.ngModel).assign;
modelValueSetter($scope, 'Some new value');
}
This works much more reliably than eval().
If you have a better idea please provide another answer or just comment this one. Thank you!
[previous answer]
I had trouble with this issue today, and I solved it by triggering and sort of hijacking the $parsers pipeline using a closure.
const hijack = {trigger: false; model: null};
modelCtrl.$parsers.push( val => {
if (hijack.trigger){
hijack.trigger = false;
return hijack.model;
}
else {
// .. do something else ...
})
Then for resetting the model you need to trigger the pipeline by changing the $viewValue with modelCtrl.$setViewValue('newViewValue').
const $setModelValue = function(model){
// trigger the hijack and pass along your new model
hijack.trigger = true;
hijack.model = model;
// assuming you have some logic in getViewValue to output a viewValue string
modelCtrl.$setViewValue( getViewValue(model) );
}
By using $setViewValue(), you will trigger the $parsers pipeline. The function I wrote in the first code block will then be executed with val = getViewValue(model), at which point it would try to parse it into something to use for your $modelValue according the logic in there. But at this point, the variable in the closure hijacks the parser and uses it to completely overwrite the current $modelValue.
At this point, val is not used in the $parser, but it will still be the actual value that is displayed in the DOM, so pick a nice one.
Let me know if this approach works for you.
[edit]
It seems that ngModel.$commitViewValue should trigger the $parsers pipeline as well, I tried quickly but couldn't get it to work.
I am facing an issue with my controllers refs and the returned object being invalid. And by invalid it returning me an object that says it has not been rendered even though i am in the process of using it.
In my controller I have a ref defined as the following.
refs : [ {
ref : 'selectDeltaView',
selector : '[itemId=selectDeltaView]'
}]
and later in my controller I access this view using the created accessor
this.getSelectDeltaView()
What is strange is that the first time this view is presented, inside of an Ext.Window it works just fine. However, on the second launch the view I get back is not rendered.
this.getSeletDeltaView().rendered === false
Reading the documentation over at Sencha it seems that this simply passes my selector to the Ext.ComponentQuery.query function. However, if I call Ext.ComponentQuery.query('[itemId=selectDeltaView]') I get an array with a single element of which rendered is true.
Am i missing something? Why is my controllers reference returning me invalid data.
Update
Some additional details not mentioned in the original post. My initial assumption was that my view was not being destroyed. However, I have log statements in my ondestory and beforedestory events and can confirm that on closing the window my view is being destroyed.
What is the most confusing is that if refs simply use Ext.ComponentQuery.query, why does Ext.ComponentQuery.query only return one view when the window is re-opened and it returns the correct view. I understand from the question below that refs will catch the view, but that view no longer exist.. or shouldn't.. If the view is destroyed, can I force the controller to clear its catch?
The controllers reference is cached after the first evaluation.
What happens is this:
You open your view a first time.
getSeletDeltaView() finds that view, and stores a reference to it in the reference lookup cache.
You close the view, but you do not destroy it completely.
You open the view a second time, but it is a different instance.
getSeletDeltaView() finds the first view, that is no longer rendered, but still exists, because it was not destroyed. You cannot use this reference, because it point to the wrong view.
Solution:
Make sure to destroy() your view, when you hide it. You can also use autoDestroy as a property. Usually, autoDestroy is true by default.
In a view I have a link to choose a start location like below:
<input type="text" ng-model="placeStart" placeholder="place to start">
<input type="text" ng-model="weight" placeholder="goods weight">
and in the location page, I do choose a place, however, When I use $ionicHistory.goBack(), I could not pass the "place" back to the previous view. I also do not want to use state.go('previous view') to pass the "place", because in that way, I will lost the other input information in the previous view.
Here is :
$ionicHistory.backView().stateId;
don't ignore to include $ionicHistory on controller
There are three options which immediately come to mind. $rootScope, localStorage and using routing not goBack().
If you need the value from one view in another, and they're completely separate controllers etc then you need a way to pass them around.
You could create and then put the value into $rootScope.globals or similar.
You could store the value to localStorage before sending the user back.
You could redirect correctly to a route which allows the values to be included in the url and still show the provious page. For example the same route with and without values set, using - or 0 for not set depending on data type:
/an/example/route/-/-/
/an/example/route/0/0/
/an/example/route/123/456
Update:
There is actually a fourth way where you can send data between controllers using $broadcast and $on. The broadcast happens in the sending controller and the $on listens in the receiving controller(s) so you can send an update to values / an object etc. $on and $broadcast in angular
It depends on your situation but if you don't care to preserve state after a page reload which localStorage would be good for, but rather have the state be remembered just when you're going back (and not necessarily when you later navigate forward back into the view again) then I do this: I make a separate service, just an object that sticks around in which I can inject anywhere and store some variables. In Angular 1 this would be a service object.
angular.module('foo').factory("placeHelper", [
() => {
let _place = null;
class PlaceHelper {
set place(place){
_place = place;
}
get place(){
return _place;
}
reset(){
_place = null;
}
}
let helper = new PlaceHelper();
return helper;
}
]);
Then in your controller that you're going back to, you inject placeHelper and ask it for the place and restore your state via $scope.place = placeHelper.place and when in the UI for that controller someone selects a place, you just store it in the service, placeHelper.place = $scope.place.
I would use localStorage within the service if I wanted to keep the state around after a page refresh.
I don't like polluting $rootScope because it's harder to keep track after you start to have more than a few unrelated methods in there and your properties need to have longer names (or get grouped in objects anyway). It's better for maintainability to encapsulate and separate concerns.
Service variation:
The service could be an object literal instead of a class and you could directly set the properties instead of using methods if you wanted it to be a bit more simple.
angular.module('foo').factory("placeHelper", [
() => {
let helper = {
place: null,
reset(){
this.place = null;
}
};
return helper;
}
]);
I stumbled on the same problem recently. I ended up deciding to use $ionicHistory.currentView() and $ionicHistory.backView() (see documentation here). The former function returns an object associated to the current view, while the latter returns an object associated to the view you will go to after you call $ionicHistory.goBack().
Before calling $ionicHistory.goBack() on your location page you call $ionicHistory.backView() and define a new property to the returned object whose contents is the data you want to propagate to the other view.
On your other view, you change its '$ionicView.enter' event handler so it calls $ionicHistory.currentView(), which retrieves the object with the data you want.
I couldn't find an answer or a solution to a challenge yet: How can I bind a variable (Session variable, no mongo collection) reactively in angular-meteor?
I'm converting from standalone meteor. Here I could use the template.helper method. As I can't use templates (and iron:router) anymore with angular-meteor and angularui-router, I can't bind reactivity to the helper anymore (at least in my understanding).
I tried this in an meteor-angular controller, which belongs to a sentence.tpl file:
$scope.parsestring = function(input_string){
tokenizer(input_string);
};
$scope.sentence_type = Session.getJSON("current_sentence.sentence_type");
Tokenizing works (I can see it in the debugger), but the value is only displayed, when I reload the page. What I want to achieve is tokenizing a string from an input field into a JSON representation (the tokenizer takes care of that) and displaying it similtaniously from the JSON representation in a structured way (separate html input elements, which are created dynamically). sentence_type is the variable that should be used on the html-page to show and change the sentence type, which can change while typing.
Anybody has some hints? Maybe, I could also use some Angular feature that I don't know?
Cheers,
Jan
Code repo:
My current code looks like this:
My code looks similar to this:
angular.module('ngaignt').controller("InteractorCtrl", ['$scope', '$meteor', '$meteorCollection',
function ($scope, $meteor, $meteorCollection) {
// Autorun is necessary to make reactive variables out of the JSON returns
var c = Tracker.autorun(function (comp) {
$scope.verb_type = Session.getJSON("current_verb.type");
$scope.object_type = Session.getJSON("current_object.type");
$scope.verb_attributes = _.toArray(Session.getJSON("current_verb.attributes"));
$scope.object_attributes = _.toArray(Session.getJSON("current_object.attributes"));
if (!comp.firstRun) {
// only do not do aply at first run becaulse then apply is already running.
$scope.$apply();
}
});
$scope.parsestring = function (input_string) {
interactor(input_string);
};
//$scope.on('$destroy', function () {c.stop()});
}]);
To use reactive variables, you need a reactive computation. You may need to use Tracker.autorun:
$scope.parsestring = Tracker.autorun(function(someStringInSession){
tokenizer(Session.get(someStringInSession));
});
Or you can use Tracker.autorun(func) wherever you use a reactive variable to reactively rerun a function when the variable changes.
good question and the best answer depend on your needs.
There are 2 possible solutions:
If you want to bind a Session variable to a scope variable, use the $meteorSession service.
What it does is that every time the scope variable will change, it will change to Session variable (and trigger an autorun if it's placed inside one).
and every time the Session variable will change, the scope variable will change as well (and change the view that it's placed upon).
If you are using the Session variable just to get a variable reactive (meaning trigger an autorun), you should use getReactively . this just returns the already existing scope variable but trigger an autorun every time it changes. a good example of this can be found it our tutorial.
Note: In anyway, when you use Tracker.autorun inside Angular, you need to connect it to a scope. this can be easily done if you replace Tracker.autorun with the $meteorUtils autorun function
Would be great if you could share a repo so that I can look on the broader perspective and could better determine what's the best solution from the two.
Based on another answer about "session" reacitivity, I could solve the problem. Just use the approach described in the link https://stackoverflow.com/a/21046935/4035797. You have to substitute Deps.autorun by Tracker.autorun though as Deps is deprecated and you have to make the scope variables for use in the template reactive (e.g., $scope.sentence_type = Session.getJSON("current_sentence.sentence_type");) and not the tokenizer.