Trigger set() when adding element to array - arrays

I am attempting to manage an array within an Angular service like so:
import { TodoItem } from '../models/todo-item.model';
#Injectable()
export class TodoService {
//local storage key name
private readonly lsKey = 'pi-todo';
private _todos: Array<TodoItem>;
//Gets the todo items from local storage
public fetchTodos(): Array<TodoItem> {
//Either get the items if they exist, or get an empty array
this.todos = (JSON.parse(localStorage.getItem(this.lsKey)) as Array<TodoItem>) || [];
return this.todos;
}
//Adds the todo item to local storage
public addTodo(todo: TodoItem): Array<TodoItem> {
if (todo) {
//Better way to do this?
let tempTodos = this.todos;
tempTodos.push(
Object.assign(
{
completed: false
},
todo
)
);
this.todos = tempTodos;
return this.todos;
}
}
private get todos(): Array<TodoItem> {
return this._todos || [];
}
private set todos(todos: Array<TodoItem>) {
this._todos = todos;
localStorage.setItem(this.lsKey, JSON.stringify(this._todos));
}
}
When adding a todo item to the todos array, I tried doing this.todos.push(...); but then that doesn't trigger the setter. How can I do this without using a temp array?

I'd suggest moving the "save to local storage" code to a separate method called by both the setter and the add.
//Adds the todo item to local storage
public addTodo(todo: TodoItem): Array<TodoItem> {
if (todo) {
this.todos.push(
Object.assign(
{
completed: false
},
todo
)
);
this.save();
return this.todos;
}
}
private set todos(todos: Array<TodoItem>) {
this._todos = todos;
this.save();
}
private save() {
localStorage.setItem(this.lsKey, JSON.stringify(this._todos));
}

Yes, because you aren't setting it a new value. A work around would be this: instead of pushing into the array, grab the current array, assign it to a temp variable, then replace with new array. Like so:
triggerSet(newValue) {
const tempArray = this.todos;
tempArray.push(newValue);
this.todos = tempArray;
}

Related

Angular decrement value in array

I try to decrement a value in my array, but I can't get it to work.
My array data contains attributes and everytime a method gets clicked, I call that value from a service and increment it in the array object. The getter is equal to amountCounter.
My main problem is that whenever I try to remove an array object, my amountCounter won't also decrement the value which it had before, but the array object gets removed.
I also put two pictures to better clarify my problem, thank you so much for every help.
app.component.html
<h2>Add values of my service into array:</h2>
<p>Array:</p>
<p>Total: {{amountCounter}}</p>
<div *ngFor="let item of data, let i = index;">
<span>ID: {{item.id}}</span>
<span>Title: {{item.title}}</span>
<span (click)="removeElement(i, item.amountCounter)" class="material-icons">
close
</span>
</div>
app.component.ts
export class AppComponent {
clickEventsubscription: Subscription
ngOnInit() {
}
id: number;
title: String;
amountCounter: number;
data: any = [];
constructor(private share: ShareDataService) {
this.clickEventsubscription = this.share.getClickEvent().subscribe(() => {
this.initialize();
})
}
removeElement(id: number, counter: number) {
this.data.splice(id, 1);
this.amountCounter -= counter //In that line I can't get it to work that my attribute decrements
console.log("before" + this.amountCounter);
console.log("after:" + counter);
}
initialize() {
this.id = this.share.getId();
this.title = this.share.getTitle();
this.amountCounter = this.share.getAmountCounter();
const newData = {
id: this.id,
title: this.title,
amountCounter: this.amountCounter
};
this.data.push(newData);
console.log(this.data);
}
}
share-data.service.ts
export class ShareDataService {
private subject = new Subject<any>();
title: String;
id: number;
amountCounter: number;
getId() {
return this.id;
}
getTitle() {
return this.title;
}
getAmountCounter(){
return this.amountCounter;
}
sendClickEvent() {
this.subject.next();
}
getClickEvent(): Observable<any> {
return this.subject.asObservable();
}
}
That is how my array looks before ID 1 is clicked
That is how my array looks after I clicked at "X", but it decrements wrong
Thank you so much!
Not sure if this is the behavior you are after but generally this method will calculate the sum of the array values
getTotalAmount(): number {
return this.data.reduce((acc, item) => acc + item.amount, 0);
}
The main issue I found very difficult to figure out is that you have amountCounter in [share-data.service, dialog.component, app.component]
I suppose you want to add new items using dialog.component with different amount values.
Here you add new item to your 'data' array, the values for single item comes from share service which was updated in your dialog.component
initialize() {
console.log("initialize");
const id = this.share.getId();
const title = this.share.getTitle();
const amount = this.share.getAmount();
const newData = {
id,
title,
amount
};
this.data.push(newData);
}
To summarize the flow:
in dialog.component you update field values in share-data.service clickMe() method
that method will trigger a method in app.component called initialize which will add the new item to the this.data array.
if you click on item (to remove it) splice will do it, and Angular will refresh the Total calling the getTotalAmount method
Working Stackblitz.

Typescript fire event every time an array changes

I am trying to observe an array in typescript 2.3.3, and I am using rxjs. my goal is to run a method every time an array is changed. Let me clarify with some code before I can tell you what I've tried.
rows: any[] = []
rows.push('test1') //fired event now
rows.splice(0,1) //fire event now
rows = [] // fire event now
Basically, if this property ever changes then I would like to have an event called.
I have researched Rx.Observable and I came up with a couple different things.
Rx.Observable.from(this.rows) and then subscribe to it, however the subscription is never fired.
Rx.Observable.of(this.rows) and then subscribe to it, this fires the subscription only 1 time.
i think that Rx.Observable is the way to go here, but i'm not sure how to get this to fire event every time.
thank you for your advice
Looks like Proxy might fit the bill. Some preliminary testing suggests that most in-place Array mutators (like splice, sort and push) do trigger Proxy setters, so you might be able to emit values on the side as easily as:
const subj: Subject<TItem> = new Subject(); // whatever TItem is convenient for you
const rows: Array<T> = new Proxy([], {
set: function(target: Array<T>, idx: PropertyKey, v: T): boolean {
target[idx] = v;
this.subj.onNext(/* TItem-typed values here */);
return true;
}
});
Note that very nicely (and to my total total surprise), TypeScript coerces new Proxy(Array<T>, ...) to Array<T>, so all your array operations are still typed! On the downside, you will probably be flooded with events, especially handling operations that set multiple times like sort, for which you can expect O(n lg n) notifications.
I ended up creating an ObservableArray class that will fire events and has a subscribe method in it. I will post this here just so that people having this issue will be able to use it too.
export class ObservableArray {
onArrayChanges: EventEmitter<any> = new EventEmitter();
onRemovedItem: EventEmitter<any> = new EventEmitter();
onAddedItem: EventEmitter<any> = new EventEmitter();
onComponentChanges: EventEmitter<any> = new EventEmitter();
length(): number {
return this.collection.length;
}
private collectionCount: number = 0;
private isStartup:boolean=true;
constructor(public array: any[]) {
this.onComponentChanges.subscribe(data => {
this.array = data;
this.onChanges();
});
}
private collection: any[] = [];
private subscriptions: any[] = [];
onChanges(): void {
let collectionModel = new CollectionModel();
collectionModel.originalValue = this.collection;
collectionModel.newValue = (this.isStartup)?this.collection:this.array;
collectionModel.isRecordAdded = this.isAddedItem();
collectionModel.isRecordRemoved = this.isRemovedItem();
this.collectionCount = this.collection.length;
if (this.isAddedItem()) {
this.onAddedItem.emit(collectionModel);
}
if (this.isRemovedItem()) {
this.onRemovedItem.emit(collectionModel);
}
if (this.isChanged()) {
this.updateCollection();
this.onArrayChanges.emit(collectionModel);
this.publish(collectionModel);
this.isStartup = false;
}
}
private isChanged(): boolean {
let isChanged = false;
if (this.array) {
isChanged = this.array.length !== this.collectionCount;
}
return isChanged || this.isStartup;
}
private isAddedItem(): boolean {
let isAddedItem = false;
if (this.array) {
isAddedItem =this.array.length > this.collectionCount;
}
return isAddedItem;
}
private isRemovedItem(): boolean {
let isRemoved = false;
if (this.array) {
isRemoved = this.array.length < this.collectionCount;
}
return isRemoved;
}
private updateCollection() {
this.collection = this.array;
}
private publish(payload?: any) {
for (let callback of this.subscriptions) {
callback(payload);
}
}
subscribe(callback: (payload?: any) => void) {
this.subscriptions.push(callback);
}
}
export class CollectionModel {
originalValue: any[] = [];
newValue: any[] = [];
isRecordAdded: boolean = false;
isRecordRemoved: boolean = false;
}
If you really wanted you could make this and ObservableArray<T> however in my case i didn't think that i needed to do this.
I hope this helps others. :)
try use "spread" instead of "push"
rows = [...rows,"test"]

How to save Mobx state in sessionStorage

Trying to essentially accomplish this https://github.com/elgerlambert/redux-localstorage which is for Redux but do it for Mobx. And preferably would like to use sessionStorage. Is there an easy way to accomplish this with minimal boilerplate?
The easiest way to approach this would be to have a mobx "autorun" triggered whenever any observable property changes. To do that, you could follow my answer to this question.
I'll put some sample code here that should help you get started:
function autoSave(store, save) {
let firstRun = true;
mobx.autorun(() => {
// This code will run every time any observable property
// on the store is updated.
const json = JSON.stringify(mobx.toJS(store));
if (!firstRun) {
save(json);
}
firstRun = false;
});
}
class MyStore {
#mobx.observable prop1 = 999;
#mobx.observable prop2 = [100, 200];
constructor() {
this.load();
autoSave(this, this.save.bind(this));
}
load() {
if (/* there is data in sessionStorage */) {
const data = /* somehow get the data from sessionStorage or anywhere else */;
mobx.extendObservable(this, data);
}
}
save(json) {
// Now you can do whatever you want with `json`.
// e.g. save it to session storage.
alert(json);
}
}
Turns out you can do this in just a few lines of code:
const store = observable({
players: [
"Player 1",
"Player 2",
],
// ...
})
reaction(() => JSON.stringify(store), json => {
localStorage.setItem('store',json);
}, {
delay: 500,
});
let json = localStorage.getItem('store');
if(json) {
Object.assign(store, JSON.parse(json));
}
Boom. No state lost when I refresh the page. Saves every 500ms if there was a change.
Posting the example from here: https://mobx.js.org/best/store.html
This shows a cleaner method of detecting value changes, though not necessarily local storage.
import {observable, autorun} from 'mobx';
import uuid from 'node-uuid';
export class TodoStore {
authorStore;
transportLayer;
#observable todos = [];
#observable isLoading = true;
constructor(transportLayer, authorStore) {
this.authorStore = authorStore; // Store that can resolve authors for us
this.transportLayer = transportLayer; // Thing that can make server requests for us
this.transportLayer.onReceiveTodoUpdate(updatedTodo => this.updateTodoFromServer(updatedTodo));
this.loadTodos();
}
/**
* Fetches all todo's from the server
*/
loadTodos() {
this.isLoading = true;
this.transportLayer.fetchTodos().then(fetchedTodos => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json));
this.isLoading = false;
});
}
/**
* Update a todo with information from the server. Guarantees a todo
* only exists once. Might either construct a new todo, update an existing one,
* or remove an todo if it has been deleted on the server.
*/
updateTodoFromServer(json) {
var todo = this.todos.find(todo => todo.id === json.id);
if (!todo) {
todo = new Todo(this, json.id);
this.todos.push(todo);
}
if (json.isDeleted) {
this.removeTodo(todo);
} else {
todo.updateFromJson(json);
}
}
/**
* Creates a fresh todo on the client and server
*/
createTodo() {
var todo = new Todo(this);
this.todos.push(todo);
return todo;
}
/**
* A todo was somehow deleted, clean it from the client memory
*/
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1);
todo.dispose();
}
}
export class Todo {
/**
* unique id of this todo, immutable.
*/
id = null;
#observable completed = false;
#observable task = "";
/**
* reference to an Author object (from the authorStore)
*/
#observable author = null;
store = null;
/**
* Indicates whether changes in this object
* should be submitted to the server
*/
autoSave = true;
/**
* Disposer for the side effect that automatically
* stores this Todo, see #dispose.
*/
saveHandler = null;
constructor(store, id=uuid.v4()) {
this.store = store;
this.id = id;
this.saveHandler = reaction(
// observe everything that is used in the JSON:
() => this.asJson,
// if autoSave is on, send json to server
(json) => {
if (this.autoSave) {
this.store.transportLayer.saveTodo(json);
}
}
);
}
/**
* Remove this todo from the client and server
*/
delete() {
this.store.transportLayer.deleteTodo(this.id);
this.store.removeTodo(this);
}
#computed get asJson() {
return {
id: this.id,
completed: this.completed,
task: this.task,
authorId: this.author ? this.author.id : null
};
}
/**
* Update this todo with information from the server
*/
updateFromJson(json) {
// make sure our changes aren't send back to the server
this.autoSave = false;
this.completed = json.completed;
this.task = json.task;
this.author = this.store.authorStore.resolveAuthor(json.authorId);
this.autoSave = true;
}
dispose() {
// clean up the observer
this.saveHandler();
}
}
Here, you can use my code, although it only supports localStorage you should be able to modify it quite easily.
https://github.com/nightwolfz/mobx-storage

angular2 observables filter each array element than return it to an array

I have an array of Threads Objects with ID, title and a isBookmarked:boolean.
I've created a Subject and I want to subscribe to it in order to get an array of Thread Objects with isBookmarked=true.
https://plnkr.co/edit/IFGiM8KoSYW6G0kjGDdY?p=preview
Inside the Service I have
export class Service {
threadlist:Thread[] = [
new Thread(1,'Thread 1',false),
new Thread(2,'Thread 2',true),
new Thread(3,'Thread 3',false),
new Thread(4,'Thread 4',true),
new Thread(5,'Thread 5',true),
new Thread(6,'Thread 6',false),
new Thread(7,'Thread 7',false),
]
threadlist$:Subject<Thread[]> = new Subject<Thread[]>()
update() {
this.threadlist$.next(this.threadlist)
}
}
in the component
export class AppComponent implements OnInit {
localThreadlist:Thread[];
localThreadlistFiltered:Thread[];
constructor(private _service:Service){}
ngOnInit():any{
//This updates the view with the full list
this._service.threadlist$.subscribe( threadlist => {
this.localThreadlist = threadlist;
})
//here only if isBookmarked = true
this._service.threadlist$
.from(threadlist)//????
.filter(thread => thread.isBookmarked == true)
.toArray()
.subscribe( threadlist => {
this.localThreadlistFiltered = threadlist;
})
}
update() {
this._service.update();
}
}
which Instance Method do I use in general to split an array?
Also is there a better way to do it?
Thanks
You would leverage the filter method of JavaScript array within the map operator of observables:
this._service.threadlist$
.map((threads) => {
return threads.filter((thead) => thread.isBookmarked);
})
.subscribe( threadlist => {
this.localThreadlistFiltered = threadlist;
});
See this plunkr: https://plnkr.co/edit/COaal3rLHnLJX4QmvkqC?p=preview.

Angular 2 observable doesn't 'map' to model

As I'm learning Angular 2 I used an observable to fetch some data via an API. Like this:
getPosts() {
return this.http.get(this._postsUrl)
.map(res => <Post[]>res.json())
.catch(this.handleError);
}
My post model looks is this:
export class Post {
constructor(
public title: string,
public content: string,
public img: string = 'test') {
}
The problem I'm facing is that the map operator doesn't do anything with the Post model. For example, I tried setting a default value for the img value but in the view post.img displays nothing. I even changed Post[] with an other model (Message[]) and the behaviour doesn't change. Can anybody explain this behaviour?
I had a similar issue when I wanted to use a computed property in a template.
I found a good solution in this article:
http://chariotsolutions.com/blog/post/angular-2-beta-0-somnambulant-inauguration-lands-small-app-rxjs-typescript/
You create a static method on your model that takes an array of objects and then call that method from the mapping function. In the static method you can then either call the constructor you've already defined or use a copy constructor:
Mapping Method
getPosts() {
return this.http.get(this._postsUrl)
.map(res => Post.fromJSONArray(res.json()))
.catch(this.handleError);
}
Existing Constructor
export class Post {
// Existing constructor.
constructor(public title:string, public content:string, public img:string = 'test') {}
// New static method.
static fromJSONArray(array: Array<Object>): Post[] {
return array.map(obj => new Post(obj['title'], obj['content'], obj['img']));
}
}
Copy Constructor
export class Post {
title:string;
content:string;
img:string;
// Copy constructor.
constructor(obj: Object) {
this.title = obj['title'];
this.content = obj['content'];
this.img = obj['img'] || 'test';
}
// New static method.
static fromJSONArray(array: Array<Object>): Post[] {
return array.map(obj => new Post(obj);
}
}
If you're using an editor that supports code completion, you can change the type of the obj and array parameters to Post:
export class Post {
title:string;
content:string;
img:string;
// Copy constructor.
constructor(obj: Post) {
this.title = obj.title;
this.content = obj.content;
this.img = obj.img || 'test';
}
// New static method.
static fromJSONArray(array: Array<Post>): Post[] {
return array.map(obj => new Post(obj);
}
}
You can use the as keyword to de-serialize the JSON to your object.
The Angular2 docs have a tutorial that walks you through this. However in short...
Model:
export class Hero {
id: number;
name: string;
}
Service:
...
import { Hero } from './hero';
...
get(): Observable<Hero> {
return this.http
.get('/myhero.json')
.map((r: Response) => r.json() as Hero);
}
Component:
get(id: string) {
this.myService.get()
.subscribe(
hero => {
console.log(hero);
},
error => console.log(error)
);
}

Resources