How to retain local UI state when using $resources with AngularJS - javascript

My problem is part of a larger program but I extracted a simulation of the problem in a simpler jsfiddle.
I'm implementing a "detailed view" show/hide controller for a todo item. When I click on the link, a detailed view is shown or hidden. The detailed view state is stored in todo.showDetails. This works fine.
When I mark a todo as completed, I persist its current state on the server using:
// Persist immediately as clicked on
$scope.checkTodo = function (todo) {
todo.$save();
};
This causes my detailed view to go hidden as the todo.showDetails state is lost because it's not part of the persisted item (i.e., it's overridden with the server's view of the todo item, which doesn't contain the UI state todo.showDetails). I'm not surprised, this makes sense.
My question is: how can my todo items contain both local UI state and data that's persisted on the server? When I todo.$save(), I'd like the todo's data to be saved on the server while retaining the value of todo.showDetails in the client code. I.e., whenever I call todo.$save(), I'd like my UI state to remain unchanged.
Is there an idiomatic way to do this in AngularJS? I wouldn't want to persist UI values like showDetails on the server.
One way I've been thinking of implementing this would be to have a separate service that stores UI state for each todo item. When I need to access local state, instead of accessing like todo.showDetails, I could do something like AppState.lookup(todo.id).showDetails or similar. But perhaps there's a simpler way..
-- UPDATE 2013-02-25 --
I did as in the below answer and simply separated UI state from todo items created by $resource.get/query calls. It was a lot simpler than I thought (see commit):
My controller changes:
$scope.todos = Todo.query();
+ $scope.todoShowDetails = [];
+ $scope.toggleShowDetails = function (todo) {
+ $scope.todoShowDetails[todo.id] = !$scope.todoShowDetails[todo.id];
+ }
+
+ $scope.showDetails = function (todo) {
+ return $scope.todoShowDetails[todo.id];
+ }
Template changes:
- <label class="btn btn-link done-{{todo.done}}" ng-click="todo.showDetails = !todo.showDetails">
+ <label class="btn btn-link done-{{todo.done}}" ng-click="toggleShowDetails(todo)">
..
- <div ng-show="todo.showDetails" ng-controller="TodoItemCtrl">
+ <div ng-show="showDetails(todo)" ng-controller="TodoItemCtrl">
It's simple when I put it in code. My jsfiddle example is a little bit contrived, it makes more sense in the context of my todo app. Perhaps I should just delete the question if it's either difficult to understand or too trivial?

Is it possible for you to extract that UI state from your todo object? You'd still place the state object on the scope, just not inside the todo object. It sounds like that type of state doesn't belong in the todo object anyway, since you don't want to persist it.

Related

Vue.js bad practice to reference store directly in v-for in template?

I'm working on my first Vue.js application (Vue 2.x) and I'm attempting to sprint before I can crawl, so it's been a trip. I finally have the start of a working prototype, but am concerned I may trip myself up down the line with this. I have two sibling components under the root on the same page, like so:
<Root>
<TimeCreate> router-view
<TimeIndex> = $vm0 router-view
In TimeCreate I'm creating a record that I want instantly displayed within TimeIndex without a page reload of course. TimeCreate creates the record and then I reload the times store using a dispatched action. In TimeIndex, I was defining a times variable in data like so:
data: function () {
return {
times: this.$store.state.userTimes,
...
and then in my template:
<tr v-for="tm, index in times">
...
</tr>
but the TimeIndex component never "reacted" to the data change in store, because I had no watcher? And then I read some SO posts about how it was bad to "watch" Vuex stores... but then when I changed my TimeIndex template for loop to this:
<tr v-for="tm, index in this.$store.state.userTimes">
...
</tr>
suddenly I have a de-facto store-watcher in my TimeIndex template, and it is working great, so far.
Have I set myself up for problems later with this approach? I've been doing this sort of thing fairly frequently (putting store variables directly into a template) and I wonder if I'm going to pay later. Thanks for your help.
Okay so to address your concerns,
1)
data: function () {
return {
times: this.$store.state.userTimes,
...
Here your assigning the value of this.$store.state.userTimes at the point when this address is read to a new variable, it's just the value and unless it is a reactive type then it'll not receive any updates. It's unlikely you'll want to do this unless your positive that you want you want a copy of the value in an initial state and won't want to receive updates.
2)
<tr v-for="tm, index in this.$store.state.userTimes">
...
</tr>
As your using the value that is stored within Vuex by a reference to it you'll recieve updates whenever it's value changes by a mutation. You don't need to use a computed property as your just looking to use the value and aren't building logic into what to do with your component when the value changes. It's useful to note that your not actually watching the variable here, you simply just have a reference to the value contained within the store, no magic going on here.
3)
Have I set myself up for problems later with this approach? I've been
doing this sort of thing fairly frequently (putting store variables
directly into a template) and I wonder if I'm going to pay later.
Thanks for your help
Your using state in the correct manner by asking the store for the current state and using a reference within your components logic, this is the correct approach to take for simple operations and largely for most content in a basic application.

Angular - recalculate a variable on every change

I have a variable that stores the available cars at any moment. Is there a way to automatically re-evaluate this function on every change?
Just using this.carFactory.available in this case is not a solution, because this example I'm showing is simplified - the real calculation in my project is alot more complex.
calculateAvailableCars(){
this.carFactory.available.forEach(function(item){
this.availableCars.push(car.id);
}.bind(this));
}
How could I do this in Angular 2? In Angular JS there was the possibility to $watch a function.
I could of course manually call this function everytime something changes, but it would be nice not to have to call this function in every part of the application that can change the data.
Using template function reference with auto change detection
You can use this function output on template:
carOutput(): cars[] {
this.calculateAvailableCars()
return this.availableCars;
}
and use output on template:
<p>My car ratio is {{ carOutput() }} </p>
However this will trigger very aggressive change detection strategy on this variable. This solution is the simpliest one, but from engineering perspective rather worst: consumes tons of unnecessary function calls. One note, that hosting element must not be set to detect changes onPush.
Separate data model to parent component and pass as property to child
You can store car list display in separate component, and pass new car array as input property to this component:
<car-display [cars]="availableCars"></car-display>
Then you can set changeDetetcion policy in this component to onPush, and each time input property bind to availableCars will change, <car-display> will re-render.
If update relays on some host binding
If some external host action is triggering new cars calculation, then hostBinding may help:
#hostListener(`hover`) recalculateCars() {
this.calculateAvailableCars()
}
And finally, (because you describe your use case quite cryptically, without many details, thus I'm scratching all possible scenarios) if some external component action shall trigger re-calculation, you can hook to ngLifecycle ngOnChanges() if for example external input property change shall re-trigger cars calculation.
In other words and summing all that up, it depends who and from where triggers changes, that shall re-trigger available cars recalculation.
And very important, see an answer from #chiril.sarajiu, because what we are trying to work around here can be handled automatically by single observable. This requires additional setup (service, provide observable to components, e.c.t.) but it's worth.
--- EDIT ---
If each variable change shall retrigger data
As OP clarified, that changes are related with model bound to component. So another option with mentioned by #marvstar is using set, where each model variable change will retrigger fetching function:
modelSchangeSubject: Subject<Model> = new Subject<Model>();
ngOnInitt() {
this.modelSchangeSubject
.subscribe((v: Model) => {
this.calculateAvailableCars()
})
}
/* Rest of controller code */
set modelBounded(v: Model) {
this.modelSchangeSubject.next(v);
}
You need RxJS. What you do is you create a data service, which will store an Observable (in my case a BehaviorSubject, which is mostly the same, but in my case I start with a value).
export class DataService {
private dataStorage$ = new BehaviorSubject(null); //here is the data you start with
get getDataStorage() {
return this.dataStorage$.asObservable(); // so you won't be able to change it outside the service
}
set setDataStorage(data: any) {
this.dataStorage$.next(data);
}
}
Then you subscribe to this data changes everywhere you need to:
constructor(private dataService: DataService){}
ngOnInit() {
this.dataService.getDataStorage.subscribe((data) => this.calculateAvailableCars(data));
}
calculateAvailableCars(){
this.carFactory.available.forEach(function(item){
this.availableCars.push(car.id);
}.bind(this));
}
Read more about best practices of using RxJS in Angular, as there can be quite a bit of pitfalls and problems.
Try using setter and getter.
private _YourVariable:any;
public set YourVariable(value:any){
this._YourVariable = value;
//do your logik stuff here like. calculateAvailableCars
}
public get YourVariable():any{
return this._YourVariable ;
}

Meteor react design - nested component: function call

I'm currently facing a problem with Meteor and React, where i know some partly solutions but they don't work and imo none of them is pointing in the true direction.
The situation:
All is about an fitness app: I have a structure that represents exercises for customers, while each exercise can have a defined number of sets (a set is how often a exercise should be done). Each set has some properties (all the user can manipulate within the font-end).
Now i have the following component structure with some map-functions (state properties are in {}):
Training {customers,exercises,datetime,otherinfos}
- Overview {customers,exercises}
exercises.map():
- Exercise {exercise,customers}
customers.map():
- Customer {exercise,customer}
exercise.sets.map()
Set {exercise, customer, set, valuesofset}
From a UI-perspective (react) this all works without problems.
Now the idea is to have a button "Save" within the Training component. When the button is pressed, I want to save the state of all Set-Components in a "sets" collection (if it has other values than the default placeholder ones) and at the same time save the Training-Component in a "trainings" collection. But the training should also include information about what Sets are integrated (so at least the Set._id should be in the Training-Component state at time of Saving.
Here now my ideas so far:
Create refs from Training all the way down to all Sets and then, when pressing "Save" iterate over all refs and call a "Mongo.insert" from all Sets. Here i have the problem that i cannot return the inserted _id. Of course i could call a different function in each Component from Set all the way back to Training, but imo this is an overflow.
Try to manage the state of all sets within the Training state by calling a nested function. As i have onChangeHandler on the Inputs, this would always call a method in Training and check which one of the Sets was changed and then changes it. I have tried it this way, but it led to a very bad performance.
Create a temp-ID for Training, forward it to to the Sets (using the componentWillReceiveProps method) and when in Set, insert the Set in the database with the temp-ID. Then receive all Sets with temp-ID and use it to add the Training in the database. --> imo very complicated and I don't really want to do a database call if it is not necessary.
So currently i don't know how to solve this problem. The reason i try to separate "sets" and "trainings" is given through the fact, that later on i would like to give information about the last Set right next to the new empty Set whenever one is on the database. Any tips are welcome!
EDIT:
As suggested, there is also the possibility to solve the problem with Session. So therefor i have added the following code to Set:
componentDidMount() {
Tracker.autorun(() => {
Session.set(`set_${this.state.id}`, {
...this.state
});
});
}
My idea was then to iterate over all Session-Keys from Training which start with "set_" - unfortunately there is no function to that holds all Keys.
Second idea was to to use an array as value for a Session-pair. However, it's quite a procedure to handle the update of the reactive Set component (copy array from session, check whether an element is available or not, create a new one or update the existing one).
EDIT2:
I think i got a solution with Session:
Object.getOwnPropertyNames(Session.keys)
did the trick to get all SessionKeys! Thank you for your help!
If you do not want to use Redux or pass parent bound callbacks in the child component, you can try Session to store data at app level which can be accessed(set/get) in any component
https://docs.meteor.com/api/session.html
In your case, you may set values of "Set" in Session and access it in Training. You may also need https://guide.meteor.com/react.html#using-withTracker. Using withTracker will help in doing reactive update of the database on change of any Session variable.

Reactively call a js function/event with meteor.js

I'm new to meteor.js. Still getting used to it.
I get how templates update reactively according to the cursor updates on the server, like this:
{{#if waitingforsomething.length}} Something Happened! {{/if}}
This is good to display elements on the page, updating lists and content. Now, my question is: what if I want to call some javascript or fire some event when something gets updated reactively? What would be the right way to do it with meteor.js?
Anything inside Tracker.autorun or template instance this.autorun runs with changes in reactive data sources inside these autoruns.
Reactive data sources are ReactiveVar instances, db queries, Session variables, etc.
Template.myTemplate.onCreated(function() {
// Let's define some reactive data source
this.reactive = new ReactiveVar(0);
// And put it inside this.autorun
this.autorun(() => console.log(this.reactive.get()));
});
Template.myTemplate.events({
// Now whenever you click we assign new value
// to our reactive var and this fires
// our console.log
'click'(event, template) {
let inc = template.reactive.get() + 1;
template.reactive.set(inc);
}
});
It is a little bit outdated, but Sacha Greif's Reactivity Basics is a very quick and concise introduction to meteor's reactivity model.
Basically, you have what's called reactive computations, code that observes special data objects (sessions, subscriptions, cursors, etc.) and gets executed whenever any of these reactive sources changes.
This is exposed via the Tracker API
Computation works pretty well for me:
Template.myTemplate.onRendered(function() {
this.computation = Deps.autorun(function () {
if (something) {
$(".reactive").html("Something Happened!");
}
});
});
Template.myTemplate.destroyed = function(){
if (this.computation){
this.computation.stop()
}
};
I Hope this helps.

AngularJS ng-repeat directive loses data after update data parent $scope

I have a notifications page generated by a ng-repeat looking like this:
The list is generated like this:
<div id="notifications" class="page_content">
<div class="notification center" notification-view notification="notification" providers="notifications.providers" ng-repeat="notification in notificationsObjects">
</div>
After the user completed the notification (and the data is updated in the database) I update the notificationsObjects array by removing the completed notification. I do this with the following function:
$scope.removeNotificationFromNotificationList = function(notification){
for(var i = 0; i<$scope.notificationsObjects.length; i++){
if($scope.notificationsObjects[i].id == notification.id){
var indexToRemove = $scope.notificationsObjects.indexOf($scope.notificationsObjects[i]);
var updatedArray = $scope.notificationsObjects;
updatedArray.splice(indexToRemove, 1);
$scope.notificationsObjects = updatedArray;
}
}
}
But after updating the array, this happens although the data is still available:
The 2 arrays (before and after updating) look exactly what I expect but for some reason the directive for a single notification doesn't have the notification object anymore. These are the 2 notificationsObjects arrays (before and after removing the completed notification):
I thought by just recalculating the notifications and reset the new $scope.notificationsObjects would solve my problem, and it does, but it reloads my whole DOM and I don't want that. I just want the completed notification to disappear from the array (and so, remove it from the DOM). Does anyone have a clue what causes this problem, or does anyone have a better solution to solve this?
Thanks in advance!
I solved it now but didn't really find the cause. I noticed some variables in the single notification directive not being set on the $scope. Even though they were not depending on changes, I coupled them to the $scope and the problem was solved.

Categories