I would like to update a Firestore model containing a profile name and a list of hashtags with Angular 6. The "name" is stored as the value of a document field and the "hashtags" are stored as the keys of an object. When I try to update the database entry, my program adds a new document field called "data" every time I call the update function instead of updating the existing fields.
How can I fix this?
This is how my firestore looks like before the update.
My update function adds a new "data" field instead of updating everytime I call it.
My Firestore Service:
export class MembersService {
membersCollection: AngularFirestoreCollection<Member>;
members$: Observable<Member[]>;
memberDoc: AngularFirestoreDocument<Member>;
constructor(public afs: AngularFirestore) {
this.membersCollection = afs.collection<Member>('Members');
this.members$ = this.membersCollection.snapshotChanges().pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data() as Member;
const id = a.payload.doc.id;
return { data, id };
}))
);
}
getMembers(): Observable<Member[]> {
return this.members$;
}
updateMember(member: Member) {
this.memberDoc = this.afs.doc(`Members/${member.id}`);
this.memberDoc.update(member);
}
}
My input component.ts:
export class MembersComponent implements OnInit {
members: Member[];
editState: boolean;
membertoEdit: Member;
constructor(private membersService: MembersService) {
this.editState = false;
}
ngOnInit() {
this.membersService.getMembers().subscribe(members => {
this.members = members;
});
}
editMember(member: Member) {
this.editState = true;
this.membertoEdit = member;
}
clearState() {
this.editState = false;
this.membertoEdit = null;
}
submit(member: Member, editName: string, editHashtag: string) {
if ( editName !== '' && editHashtag !== '') {
this.membertoEdit.name = editName;
const key = editHashtag;
const object = {};
object[key] = true;
this.membertoEdit.hashtag = object;
this.membersService.updateMember(this.membertoEdit);
}
this.clearState();
}
}
My component.html for the user Input:
<button *ngIf="editState == false" (click)="editMember(member)">edit</button>
<div *ngIf="editState && membertoEdit.id == member.id">
<form>
<input type="text" #editName>
<input type="text" #editHashtag>
<button (click)="submit(member, editName.value, editHashtag.value);
editName.value=''">Submit</button>
</form>>
</div>
Found a solution: Even I don´t think it is elegant. It´s possible to pass every input on its own
updateMember(member: Member, editName: string, editHashtag: object) {
this.memberDoc = this.afs.doc(`Members/${member.id}`);
console.log(this.memberDoc);
this.memberDoc.update({
name: editName,
hashtag: editHashtag
});
}
submit(member: Member, editName: string, editHashtag: string) {
if ( editName !== '' && editHashtag !== '') {
const key = editHashtag;
const object = {};
object[key] = true;
this.membersService.updateMember(member, editName, object);
}
this.clearState();
}
Related
I'm trying to make a game using React to display the UI elements and using Typescript classes to represent the state of the game.
Here are a few examples of the classes I'm using to represent my data:
export class Place extends Entity {
items: Item[];
npcs: NPC[];
location: LatLng | null;
onEnter: (...args: any[]) => any = () => {};
constructor(
name: string,
description: string,
location?: LatLng,
onEnter: (...args: any[]) => any = () => {},
items: Item[] = [],
npcs: NPC[] = []
) {
super(name, description);
this.items = items;
this.npcs = npcs;
this.location = location ? location : null;
this.onEnter = onEnter;
}
export class Item extends Entity {
url: string;
constructor(
name: string,
description: string,
actions = {},
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Question_mark_%28black%29.svg/1920px-Question_mark_%28black%29.svg.png"
) {
super(name, description);
this.url = url;
this.actions = actions;
}
}
export class NPC {
name: string;
description: string;
messages: Message[];
url: string;
timesTalkedTo = 0;
constructor(
name: string,
description: string,
url = "https://cdn.icon-icons.com/icons2/1378/PNG/512/avatardefault_92824.png",
messages: Message[] = []
) {
this.name = name;
this.description = description;
this.messages = messages;
this.url = url;
}
getMsg() {
console.log(this.messages);
if (this.messages.length > 1) {
for (var i = 1; i < this.messages.length; i++) {
const msg = this.messages[i];
if (msg["cond"] && msg["cond"]()) {
this.timesTalkedTo += 1;
return msg;
}
}
}
this.timesTalkedTo += 1;
return this.messages[0];
}
}
Later on, I store instances of these classes in hooks so I can display them using other components I've defined:
function UI() {
const [places, setPlaces] = useState({});
const [inventory, setInventory] = useState([]);
const [playerPlace, setPlayerPlace] = useState(outside);
const [playerLocation, setPlayerLocation] = useState(L.latLng([0, 0]));
...
My problem is that I wanted to define a class and functions like this inside my UI component, so I would be able to access the setState hooks and use the "drop" and "pick up" actions on any item I've defined as Droppable:
class Droppable extends Item {
dropped;
constructor(
name,
description,
actions = {},
dropped = true,
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Question_mark_%28black%29.svg/1920px-Question_mark_%28black%29.svg.png"
) {
super(name, description, actions, url);
this.dropped = dropped;
const drop = () => {
addToPlace(removeFromInventory(this));
this.dropped = true;
this.actions["pick up"] = pickUp;
delete this.actions["drop"];
};
const pickUp = () => {
addToInventory(removeFromPlace(this));
this.dropped = false;
this.actions["drop"] = drop;
delete this.actions["pick up"];
};
if (dropped) {
this.actions["pick up"] = pickUp;
} else {
this.actions["drop"] = drop;
}
}
}
const addToInventory = useCallback(
(item) => {
setInventory((inv) => [...inv, item]);
return item;
},
[setInventory]
);
const removeFromInventory = useCallback(
(item) => {
setInventory((inv) => inv.filter((i) => i !== item));
return item;
},
[setInventory]
);
const addToPlace = useCallback(
(item) => {
setPlaces((places) => {
return {
...places,
[playerPlace.name]: {
...playerPlace,
items: [...playerPlace.items, item],
},
};
});
return item;
},
[setPlaces, playerPlace]
);
const removeFromPlace = useCallback(
(item) => {
setPlaces((places) => {
const newPlace = { ...places[playerPlace.name] };
newPlace.items = newPlace.items.filter((i) => i !== item);
const newPlaces = [...places];
newPlaces[playerPlace.name] = newPlace;
return newPlaces;
});
return item;
},
[setPlaces, playerPlace]
);
However, when I try removing an item from the place it's in and adding it to the player's inventory (the "pick up" action), I find that, while it is successfully added to the inventory, it cannot be removed from the place, because the playerPlace state variable is stale. Even though setPlayerPlace had been called successfully and set the playerPlace to a place containing items, the value is still set to its initial empty Place, so there is an error when trying to access the items of that Place.
My guess is that these callbacks are not being updated properly according to the state because they are used inside the class that I defined, but I can't think of any other way to give methods inside the class easy access to the state variables.
Is it a bad idea to be using ordinary classes alongside React in this way? If so, what would be a better way to structure my app. If not, how can I give my classes access to the state inside my React components?
I would recommend moving the class outside of the component and then passing the setters and data to the class as parameters if you really want to use classes. You can also use a third-party state management library for this and then hook it together, but I don't think it's really worth it. Generally speaking, using classes for your state in react is an antipattern IMHO. Usually what I would do is just write types and then utility functions for those types if I need to encapsulate functionality. This has many benefits aside from working with react such as easily being able to serialize the data to JSON (they are now POJOs).
I am having an issue when I try to get a specified user from Firebase, Firestore.
export class TaskService {
tasksCollection: AngularFirestoreCollection<Task>;
taskDoc: AngularFirestoreDocument<Task>;
tasks: Observable<Task[]>;
task: Observable<Task>;
constructor(private afs: AngularFirestore) {
this.tasksCollection = this.afs.collection('tasks', ref => ref.orderBy('title', 'asc'));
}
getTask(id: string): Observable<Task> {
this.taskDoc = this.afs.doc<Task>(`clients/${id}`);
this.task = this.taskDoc.snapshotChanges().pipe(map(action => {
if (action.payload.exists === false) {
return null;
} else {
const data = action.payload.data() as Task;
data.id = action.payload.id;
return data;
}
}));
return this.task;
}
}
And this is my Component.ts file
export class TaskDetailsComponent implements OnInit {
id: string;
task: Task;
hasHours = false;
showHoursOnUpdate: false;
constructor(
private taskService: TaskService,
private router: Router,
private route: ActivatedRoute
) { }
ngOnInit() {
// Get id from url
this.id = this.route.snapshot.params.id;
// Get client
this.taskService.getTask(this.id).subscribe(task => {
if (task != null) {
if (task.hours > 0) {
this.hasHours = true;
}
}
this.task = task;
});
console.log(this.id);
console.log(this.task);
}
}
The result for id is good.
But the result for object (task) is undefined.
P.S
I also have functions for getting all the users and adding a new user, so if that's relevant please let me know in the comments
Your line of code
this.id = this.route.snapshot.params.id;
In this case id is not a table column but it's your document id by Firestore
Here an example of firestore
So your Id in this case is the red one and not the blue one.
I'm trying to send the selected data in my table row that I am selecting via a checkbox to the server but having questions about how it should be sent via a service. I have the basic skeleton but need help with getting the items to a delete REST API call. Using C# .Net Core JSON call as the server endpoint for this service call.
view.component.ts
#Component({
templateUrl: 'view.component.html'
})
export class ViewComponent implements OnInit, OnDestroy {
// User Fields
currentUser: User;
users: User[] = [];
currentUserSubscription: Subscription;
loading : boolean;
// Action Fields
viewData: any;
viewName: string;
refNumber: number;
currentActionSubscription: Subscription;
displayedColumns: string[] = [];
dataSource: any = new MatTableDataSource([]);
pageSizeOptions: number[] = [10, 20, 50];
#ViewChild(MatSort) sort: MatSort;
#ViewChild(MatPaginator) paginator: MatPaginator;
selection = new SelectionModel<TableRow>(true, []);
defaultSort: MatSortable = {
id: 'defColumnName',
start: 'asc',
disableClear: true
};
defaultPaginator: MatPaginator;
constructor(
private iconRegistry: MatIconRegistry,
private sanitizer: DomSanitizer,
private actionService: ActionService
) {
this.loading = false;
this.iconRegistry.addSvgIcon(
'thumbs-up',
this.sanitizer.bypassSecurityTrustResourceUrl(
'assets/img/examples/thumbup-icon.svg'
)
);
}
loadAction(action: any) {
this.loading = true;
// If there is already data loaded into the View, cache it in the service.
if (this.viewData) {
this.cacheAction();
}
if (this.sort) {
// If there is sorting cached, load it into the View.
if (action.sortable) {
// If the action was cached, we should hit this block.
this.sort.sort(action.sortable);
} else {
// Else apply the defaultSort.
this.sort.sort(this.defaultSort);
}
}
if (this.paginator) {
// If we've stored a pageIndex and/or pageSize, retrieve accordingly.
if (action.pageIndex) {
this.paginator.pageIndex = action.pageIndex;
} else { // Apply default pageIndex.
this.paginator.pageIndex = 0;
}
if (action.pageSize) {
this.paginator.pageSize = action.pageSize;
} else { // Apply default pageSize.
this.paginator.pageSize = 10;
}
}
// Apply the sort & paginator to the View data.
setTimeout(() => this.dataSource.sort = this.sort, 4000);
setTimeout(() => this.dataSource.paginator = this.paginator, 4000);
// Load the new action's data into the View:
this.viewData = action.action;
this.viewName = action.action.ActionName;
this.refNumber = action.refNumber;
// TODO: add uniquifiers/ids and use these as the sort for table
const displayedColumns = this.viewData.Columns.map((c: { Name: any; }) => c.Name);
displayedColumns[2] = 'Folder1';
this.displayedColumns = ['select'].concat(displayedColumns);
// tslint:disable-next-line: max-line-length
const fetchedData = this.viewData.DataRows.map((r: { slice: (arg0: number, arg1: number) => { forEach: (arg0: (d: any, i: string | number) => any) => void; }; }) => {
const row = {};
r.slice(0, 9).forEach((d: any, i: string | number) => (row[this.displayedColumns[i]] = d));
return row;
});
this.dataSource = new MatTableDataSource(fetchedData);
this.loading = false;
}
// Stores the current Action, sort, and paginator in an ActionState object to be held in the action service's stateMap.
cacheAction() {
let actionState = new ActionState(this.viewData);
// Determine the sort direction to store.
let cachedStart: SortDirection;
if (this.sort.direction == "desc") {
cachedStart = 'desc';
} else {
cachedStart = 'asc';
}
// Create a Sortable so that we can re-apply this sort.
actionState.sortable = {
id: this.sort.active,
start: cachedStart,
disableClear: this.sort.disableClear
};
// Store the current pageIndex and pageSize.
actionState.pageIndex = this.paginator.pageIndex;
actionState.pageSize = this.paginator.pageSize;
// Store the refNumber in the actionState for later retrieval.
actionState.refNumber = this.refNumber;
this.actionService.cacheAction(actionState);
}
ngOnInit() {
// Subscribes to the action service's currentAction, populating this component with View data.
this.actionService.currentAction.subscribe(action => this.loadAction(action));
}
/** Whether the number of selected elements matches the total number of rows. */
isAllSelected() {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
/** Selects all rows if they are not all selected; otherwise clear selection. */
masterToggle() {
this.isAllSelected()
? this.selection.clear()
: this.dataSource.data.forEach((row: TableRow) => this.selection.select(row));
}
// Delete row functionality
deleteRow() {
console.log(this.selection);
this.selection.selected.forEach(item => {
const index: number = this.dataSource.data.findIndex((d: TableRow) => d === item);
console.log(this.dataSource.data.findIndex((d: TableRow) => d === item));
this.dataSource.data.splice(index, 1);
this.dataSource = new MatTableDataSource<Element>(this.dataSource.data);
});
this.selection = new SelectionModel<TableRow>(true, []);
this.actionService.deleteRow(this.selection).subscribe((response) => {
console.log('Success!');
});
}
ngOnDestroy() {
}
}
view.service.ts
deleteRow(selection: any): Observable<{}> {
console.log('testing service');
return this.http.delete<any>(`http://localhost:15217/actions/deleteRow`);
}
There are 2 things that your code as it currently stands needs to do:
Pass the ids of the selected rows back to the server in some way (generally via the url in a DELETE request)
Subscribe to the observable to materialise it. Currently the http request won't run, because it's an observable without any subscribers. At the very least the call to the service in the component should look a little like this:
this.actionService.deleteRow(this.selection).subscribe((response) => {
console.log('Success!');
});
Edit:
With number 1, it depends on what your server method looks like. If it accepts an array of numeric ids, then view.service.ts would look something like:
deleteRow(selection: SelectionModel<TableRow>): Observable<{}> {
console.log('testing service');
// create an array of query params using the property that you use to identify a table row
const queryParams = selection.selected.map(row => `id=${row.id}`);
// add the query params to the url
const url = `http://localhost:15217/actions/deleteRow?${queryParams.join('&')}`;
return this.http.delete<any>(url);
}
I'm guessing here at how you pass information about table rows to your server. If you're still struggling with this, you will need to provide a bit of information about the DELETE endpoint.
Edit 2:
Now we know a bit more about what the objects look like...
deleteRow(selection: SelectionModel<TableRow>): Observable<{}> {
console.log('testing service');
// create an array of query params using the property that you use to identify a table row
const queryParams = [...selection._selection].map(row => `id=${row.id}`);
// add the query params to the url
const url = `http://localhost:15217/actions/deleteRow?${queryParams.join('&')}`;
return this.http.delete<any>(url);
}
I want to track changes to the properties of my classes in typescript so that I only update the fields in my database which have actually changed. Currently, I am using an array where I add properties when they change and then I iterate through the array to determine what fields changed and need to be updated in the database. However, I would prefer to do this with some sort of isDirty check. My thought is that I would be able to call something like if (property.dirty) then {} to determine if a property has changed.
I remember being able to do something along these lines in vb.net, but it's been a while and I can't remember exactly what we did in that codebase.
Is the desired code below possible?
Current Code
class test{
private _ID: Guid;
private _dirty: Array<{}>;
get ID(): Guid {
return this._ID;
}
set ID(id: Guid) {
if (this._ID != id) {
this._ID = id;
this._dirty.filter(function (f) { return f.Field == "id" }).length > 0 ? this._dirty.filter(function (f) { return f.Field == "id" })[0].Value = id.toString() : this._dirty.push({Field: "id", Value: id});
}
}
get Name(): string {
return this._Name;
}
set Name(name: string) {
if (this._Name != name) {
this._Name = name;
this._DirtyFields.filter(function (f) { return f.Field == "ccseq_name" }).length > 0 ? this._DirtyFields.filter(function (f) { return f.Field == "ccseq_name" })[0].Value = name : this._DirtyFields.push(new EntityField("ccseq_name", name, FieldType.String));
}
}
}
Desired Code
class test{
private _ID: Guid;
get ID(): Guid {
return this._ID;
}
set ID(id: Guid) {
if (this._ID != id) {
this._ID = id;
this._ID.isDirty = true;
}
}
get Name(): string {
return this._Name;
}
set Name(name: string) {
if (this._Name != name) {
this._Name = name;
this._Name.isDirty = true;
}
}
}
In javascript you can add a property to an object so it's not a problem to do this:
this._ID.dirty = true;
Even when Guid doesn't have this dirty member.
The problem of course is typescript which will complain because of that.
To avoid that you can simply do:
private _ID: Guid & { dirty?: boolean };
Edit
Again, javascript already supports it, you can do this:
obj.dirty = true;
For any js type: booleans, strings, arrays and even functions.
But for having support for that in typescript you can do this:
interface Object {
dirty?: boolean;
}
But be aware that you are adding this to **all* of the objects that you have in your code. As you're not actually changing the prototype it won't have any effect in runtime, but typescript-wise it will effect all instances.
The way I solved this was to create a Field class that I then used as properties in my Objects.
Field Class
export class EntityField {
private _Field: string;
private _Value: any;
private _FType: FieldType;
private _isDirty: boolean;
constructor(field: string, value: any, fType: FieldType) {
this._Field = field;
this._Value = value;
this._FType = fType;
this._isDirty = false;
}
markClean(): void {
this._isDirty = false;
}
markDirty(): void {
this._isDirty = true;
}
get isDirty(): boolean {
return this._isDirty;
}
get Field(): string {
return this._Field;
}
set Field(field) {
if (this._Field !== field) {
this._Field = field;
}
}
get Value(): any {
return this._Value;
}
set Value(value: any) {
if (this._Value !== value) {
this._Value = value;
this._isDirty = true;
}
}
get FType(): FieldType {
return this._FType;
}
set FType(fType: FieldType) {
if (this._FType != fType) {
this._FType = fType;
}
}
}
Usage
export class Entity{
public Name: Field
}
Entity test = new Entity()
Entity.Name.isDirty() // Returns False
Entity.Name.Value = "Test";
Entity.Name.isDirty() // Returns True
I'm trying to make my react app as dry as possible, for common things like consuming a rest api, I've created classes that act as stores with predefined actions to make it easy to modify it.
Behold, big code:
import {autorun, action, observable} from 'mobx'
export function getResourceMethods(name) {
let lname = name.toLowerCase()
let obj = {
methods: {
plural: (lname + 's'),
add: ('add' + name),
addPlural: ('add' + name + 's'),
rename: ('rename' + name),
},
refMethods: {
add: ('add' + name + 'ByRef'),
addPlural: ('add' + name + 'sByRef'),
rename: ('rename' + name + 'ByRef'),
setRef: ('set' + name + 'Ref'),
},
fetchMethods: {
pending: (lname + 'Pending'),
fulfilled: (lname + 'Fulfilled'),
rejected: (lname + 'Rejected'),
}
}
return obj
}
class ResourceItem {
#observable data;
#observable fetched = false;
#observable stats = 'pending';
#observable error = null;
constructor(data) {
this.data = data;
}
}
class ResourceList {
#observable items = [];
#observable fetched = false;
#observable status = 'pending';
constructor(name) {
this['add' + name + 's'] = action((items) => {
items.forEach((item, iterator) => {
this.items.push(item.id)
})
})
}
}
class ResourceStore {
constructor(name, resourceItem, middleware) {
let {methods} = getResourceMethods(name)
this.middleware = middleware || []
let items = methods.plural.toLowerCase()
this[items] = observable({}) // <--------------- THIS DOES NOT WORK!
// Add resource item
this[methods.add] = action((id, resource) => {
let item = this[items][id], data;
if (item && item.fetched) {
data = item.data
} else {
data = resource || {}
}
this[items][id] = new resourceItem(data)
this.runMiddleware(this[items][id])
})
// Add several resource items
this[methods.addPlural] = action((resources) => {
resources.forEach((resource, iterator) => {
this[methods.add](resource.id, resource)
})
})
// Rename resource item
this[methods.rename] = action((oldId, newId) => {
let item = this[items][oldId]
this[items][newId] = item
if (oldId !== newId) {
delete this[items][oldId]
}
})
// Constructor ends here
}
runMiddleware(item) {
let result = item;
this.middleware.map(fn => {
result = fn(item)
})
return result
}
}
class ReferencedResourceStore extends ResourceStore {
#observable references = {}
constructor(name, resource, middleware) {
super(name, resource, middleware)
let {methods, refMethods, fetchMethods} = getResourceMethods(name)
let getReference = (reference) => {
return this.references[reference] || reference
}
this[refMethods.setRef] = action((ref, id) => {
this.references[ref] = id
})
this[refMethods.add] = action((ref, data) => {
this[methods.add](getReference(ref), data)
this[refMethods.setRef](ref, getReference(ref))
})
this[refMethods.rename] = action((ref, id) => {
this[methods.rename](getReference(ref), id)
this[refMethods.setRef](ref, id)
})
// *** Fetch *** //
// Resource pending
this[fetchMethods.pending] = action((ref) => {
this[refMethods.add](ref)
})
// Resource fulfilled
this[fetchMethods.fulfilled] = action((ref, data) => {
this[refMethods.add](ref, data)
this[refMethods.rename](ref, data.id)
let item = this[methods.plural][data.id];
item.fetched = true
item.status = 'fulfilled'
})
}
}
export {ResourceItem, ResourceList, ResourceStore, ReferencedResourceStore}
Now I'm just creating a simple user store:
class UserResource extends ResourceItem {
constructor(data) {
super(data)
}
#observable posts = new ResourceList('Posts')
#observable comments = new ResourceList('Comment')
}
// Create store
class UserStore extends ReferencedResourceStore {}
let store = new UserStore('User', UserResource)
And mobx-react connects just fine to the store, can read it as well. BUT, whenever I do any changes to the items (users in this case, the name of the property is dynamic) property, there are no reactions. I also noticed that in chrome, the object property does not have a "invoke property getter" in the tree view:
Didn't read the entire gist, but if you want to declare a new observable property on an existing object, use extendObservable, observable creates just a boxed observable, so you have an observable value now, but not yet an observable property. In other words:
this[items] = observable({}) // <--------------- THIS DOES NOT WORK!
should be:
extendObservable(this, {
[items] : {}
})
N.b. if you can't use the above ES6 syntax, it desugars to:
const newProps = {}
newProps[items] = {}
extendObservable(this, newProps)
to grok this: https://mobxjs.github.io/mobx/best/react.html
Edit: oops misread, you already did that, it is not hacky but the correct solution, just make sure the extend is done before the property is ever used :)
I found a hacky solution:
First off, use extendObservable instead (this is the correct solution) and then use a fresh version of the object and set it as the property.
let items = methods.plural.toLowerCase()
extendObservable(this, {
[items]: {}
})
// Add resource item
this[methods.add] = action((id, resource) => {
let item = this[items][id], data;
if (item && item.fetched) {
data = item.data
} else {
data = resource || {}
}
this[items][id] = new resourceItem(data)
this.runMiddleware(this[items][id])
this[items] = {...this[items]}
})
This works, not sure if there's a better solution.
Your options are using extendObservable or using an observable map.
For reference see the documentation of observable and specifically:
To create dynamically keyed objects use the asMap modifier! Only initially existing properties on an object will be made observable, although new ones can be added using extendObservable.