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);
}
Related
I have a function that when the component is loaded, it returns specific data of a certain parameter (ex: /app/items) and displays them in an information balloon, but if I access a child route (ex: /app/items/ create) or (ex: /app/items/1/view) I need to do a validation to see if there is informational data in this route to display, but if not, I need to display the information from the previous parameter (ex: /app/items) . I managed to create this function below that fulfills this, but with a request inside another, which I believe is not the most appropriate. So I'm trying to refactor so that only one request is made validating these parameters. It should also be considered that if the route has numbers, it needs to be filtered and returned an array with just the strings before doing the join('/'), as you can see there in the constructor. I'm new to the area, so I'm trying to find some background to refactor this function.
public information: InformationList | any
public isLoading:boolean = true
error: any;
showError: boolean = false
public currentPageUrl = null;
public output = null
other: any;
constructor(
public screenInfoService: ScreenInfoService,
public loaderService: LoaderService,
public router: Router
) {
this.currentPageUrl = this.router.url.split("/").filter((item: any) => {
return isNaN(item)
})
}
load() {
this.output = this.currentPageUrl.join("/");
let params = new HttpParams();
params = params.set('orderBy', 'id');
params = params.set('search', `path:${this.output}`);
params = params.set('searchFields', `path:ilike`);
params = params.set('searchJoin', 'and');
this.screenInfoService.getAll(params).subscribe(
success => {
if (success.length === 0) {
let output2 = null
output2 = this.currentPageUrl.slice(0,2).join("/");
params = params.set('orderBy', 'id');
params = params.set('search', `path:${output2}`);
params = params.set('searchFields', `path:ilike`);
params = params.set('searchJoin', 'and');
this.screenInfoService.getAll(params).subscribe(
success => {
this.information = success
}, error => {
this.error = 'MSG.T224';
this.showError = true;
}
)
} else {
this.information = success
}
}, error => {
this.error = 'MSG.T224';
this.showError = true;
});
}
ngOnInit(): void {
this.load()
}
I have dataServiceEvent output from Component B to Component A , How do I get or access the dataServiceEvent data inside ngOnit on Component A ? I can access it outside ngOinit as function but I wanna access it inside ngOnit cause I wanna use the data from dataServiceEvent as params to getListOfDeals .
Thanks for any help or idea. Regards.
#Component A ts code
ngOnInit(): void {
//access the dataServiceEvent here
this.getListOfDeals()
}
// I can access it here but I want to access the data on ngOnInit
dataServiceEvent(item: any) {
this.tableElements = item;
// this.getListOfDeals();
}
private getListOfDeals() {
this.searchInput = '';
this.isLoading = true;
console.log("getting deals")
this.dealService
.getAllDeals(
this.accountId,
this.transaction.id,
this.tableElements.pageIndex + 1,
this.tableElements.pageSize,
this.searchInput,
this.tableElements.sortParams = ['name'],
this.tableElements.sortDirs = ['asc']
)
.pipe(finalize(() => (this.isLoading = false)))
.subscribe({
error: (err) => this.notificationService.showError(err),
next: (res) => {
// this.dealsListData[totalElements] = res.items.length;
this.dealsListData = res.totalItemCount;
this.dealsListData = res.lastItemOnPage;
this.dealsListData = res.items;
console.log('res', this.dealsListData);
},
complete: noop,
});
}
#Component A html code
<app-table-multi-sort (dataServiceEvent)="dataServiceEvent($event)" [tableOptions]="tableOptions" [tableData]="dealsListData" (tableActionsEvent)="tableActions($event)"></app-table-multi-sort>
#Component B code - dataServiceEvent is the Output from this Component To Component A
export class TableMultiSortComponent implements OnInit {
#Output() dataServiceEvent = new EventEmitter<any>() ;
#Input() tableOptions:any;
#Input() tableData:any = [];
#Input() isClientSide:boolean = false;
#Input() isLoading: boolean = false;
#Output() tableActionsEvent = new EventEmitter<any>();
#ViewChild(MatMultiSort, { static: false }) sort: MatMultiSort;
tableConfig: any = TABLE_MULTI_SORT_OPTIONS.DEFAULT;
table:TableData<any>;
displayedColumns: any;
constructor() { }
ngOnInit(): void {
this.initTableMultiSort();
}
initTableMultiSort(){
this.tableConfig = {
...this.tableConfig,
...this.tableOptions
}
this.table = new TableData<any>(this.tableConfig.columns,this.tableConfig.sortParams);
this.table.pageSize = this.tableConfig.pageSize;
this.table.pageIndex = this.tableConfig.pageIndex;
this.table.nextObservable.subscribe(() => { this.getData(); });
this.table.sortObservable.subscribe(() => { this.getData(); });
this.table.previousObservable.subscribe(() => { this.getData(); });
this.table.sizeObservable.subscribe(() => { this.getData(); });
setTimeout(()=>{
this.table.dataSource = new MatMultiSortTableDataSource(this.sort, this.isClientSide);
this.getData();
},0);
}
ngOnChanges(changes: SimpleChanges) {
if (changes.tableData && changes.tableData.currentValue){
this.initTableMultiSort()
}
}
getData(){
//Todo: get totalelement, pageindex, pagesize from api service response
this.table.totalElements = 1;
this.table.pageIndex = 0;
this.table.pageSize = 10;
this.table.data = this.tableData;
if(this.dataServiceEvent) {
this.dataServiceEvent.emit(this.table);
}
}
You can define a property in the typescript of component A decorated with #ViewChild to get a reference of the component B (NOTE: the 'static' property of ViewChild was introduced in Angular 8):
#ViewChild(TableMultiSortComponent, { static: true }) tableMultiSortComponent: TableMultiSortComponent;
Then, inside the ngOnInit method of component A, you can subscribe to the dataServiceEvent EventEmitter of component B (EventEmitter is a particular case of a RxJS Subject):
// This property is used as the notifier of the takeUntil operator to prevent memory leaks in your code:
private _destroyed$ = new Subject();
ngOnInit() {
this.tableMultiSortComponent.dataServiceEvent
.pipe(
// switchMap subscribes to a new call everytime dataServiceEvent emits an event, and cancels previous calls, if any - You can use another RxJS higher order mapping operator depending on your needs (check: https://blog.angular-university.io/rxjs-higher-order-mapping/):
switchMap((event) => this.getListOfDeals(event)),
// The takeUntil operator is useful to destroy the subscription to avoid memory leaks once the component gets destroyed, you can use it for other subscriptions as well
takeUntil(this._destroyed$)
)
// The subscribe was moved here from the 'getListOfDeals' method
.subscribe({
error: (err) => this.notificationService.showError(err),
next: (res) => {
// this.dealsListData[totalElements] = res.items.length;
this.dealsListData = res.totalItemCount;
this.dealsListData = res.lastItemOnPage;
this.dealsListData = res.items;
console.log('res', this.dealsListData);
},
complete: noop,
});
}
ngOnDestroy() {
this._destroyed$.next();
this._destroyed$.complete();
}
// In order to work with the subscription in the ngOnInit, here you must pass a parameter, which is the value emitted by the dataServiceEvent EventEmitter and change the method accordingly (I tried to understand it from your previous code, changing 'this.tableElements' with the 'item' parameter):
private getListOfDeals(item) {
this.searchInput = '';
this.isLoading = true;
console.log("getting deals")
return this.dealService
.getAllDeals(
this.accountId,
this.transaction.id,
item.pageIndex + 1,
item.pageSize,
this.searchInput,
item.sortParams = ['name'],
item.sortDirs = ['asc']
)
.pipe(finalize(() => (this.isLoading = false)));
}
You can handle the event and access to its data like this
Component A HTML code:
<app-table-multi-sort (dataServiceEvent)="dataServiceEvent($event)" [tableOptions]="tableOptions" [tableData]="dealsListData" (tableActionsEvent)="tableActions($event)"></app-table-multi-sort>
Component A TS file:
private _eventSubject$ = new Subject();
get eventData() {
return this.eventSubject.asObservable();
}
ngOnInit(){
this.eventData.subscribe((data) => {
console.log(data)
})
}
dataServiceEvent($event) {
this._eventSubject$.next($event);
}
On a datatable that I am using, I am having mixed results when attempting to sort them by using the toggles in the table headers. It appears that columns that get populated by the response in my initial GET call populate and sort as expected. However, the columns with data that comes from the GET calls inside the outer subscription don't sort as expected. Ideally all columns would be able to sort properly.
When wrapping the this.dtTrigger.next() in a setTimeout() of 5000ms, all columns sorted as expected. So the question here is, how would I wait until the innermost GET call finishes to call the dtTrigger.next()so that all of the columns will be sortable once all of the data is finished loading in.
import { Component, OnInit, Input, OnChanges } from '#angular/core';
import { Subject } from 'rxjs';
import { BuildersService } from '#/services/builders/builders.service';
import { CommunitiesService } from '#/services/communities/communities.service';
import { ContactsService } from '#/services/contacts/contacts.service';
import { DataTableDirective } from 'angular-datatables';
#Component({
selector: 'datatable-builders',
templateUrl: './builder-datatable.component.html',
styleUrls: ['./builder-datatable.component.scss']
})
export class BuilderDatatableComponent implements OnInit {
#Input() title: string = '';
#Input() showFooter: boolean = true;
#Input() rowsPerPage: number = 10;
#Input() viewRoute: string = '';
#Input() viewText: string = 'View';
rows = [];
dtOptions: DataTables.Settings = {};
dtTrigger: Subject<any> = new Subject();
constructor(
private buildersService: BuildersService,
private communitiesService: CommunitiesService,
private contactsService: ContactsService
) { }
dataTablesInit() {
this.dtOptions = {
pagingType: 'simple_numbers',
lengthChange: false,
info: this.showFooter,
paging: this.showFooter,
columnDefs: [
{
targets: [4],
orderable: false,
searchable: false
}
],
initComplete: () => {
let searchLabels = document.querySelectorAll('.dataTables_filter > label');
searchLabels.forEach((label) => {
label.setAttribute('aria-label', 'Search/Filter Table');
});
}
};
}
ngOnInit() {
this.dataTablesInit();
// Get the table data
this.buildersService.getBuilders().subscribe((result: any) => {
// Get all Rows
let rows = result.body.map(row => {
let communities = [],
communitiesColumn = [],
managersColumn = [];
// Get the builders info and set up output
row.CommunityIDs.forEach((id) => {
this.communitiesService.getCommunity(id).subscribe((result: any) => {
communities.push(result.body);
communitiesColumn.push( result.body.Name );
});
});
row.Contacts.Managers.forEach((id) => {
this.contactsService.getContact(id).subscribe((result: any) => {
managersColumn.push(result.body);
});
});
console.log(row);
// Set additional row data
row.Communities = communities;
row.CommunitiesColumn = communitiesColumn;
row.ManagersColumn = managersColumn;
return row;
});
this.rows = result.body;
this.dtTrigger.next();
console.log(this.rows)
});
}
}
This is the synchronicity of JavaScript in general, you can use the keyword Async in the top level GET and Await keyword in the down level GET
e.g :
// Get the table data
async this.buildersService.getBuilders().subscribe((result: any) => {
...
//
// Get the builders info and set up output
row.CommunityIDs.forEach((id) => {
await this.communitiesService.getCommunity(id).subscribe((result: any) => {
...
//
communities.push(result.body);
communitiesColumn.push( result.body.Name );
});
});
row.Contacts.Managers.forEach((id) => {
this.contactsService.getContact(id).subscribe((result: any) => {
managersColumn.push(result.body);
});
});
console.log(row);
// Set additional row data
row.Communities = communities;
row.CommunitiesColumn = communitiesColumn;
row.ManagersColumn = managersColumn;
return row;
});
this.rows = result.body;
this.dtTrigger.next();
console.log(this.rows)
});
}
}
I am calling two API to return objects of data. Than run through for each of them and search if it has a value.
I want to check if one of these obj has a vas value matches.
getdata(slug){
this._apiService.getPages().subscribe(data =>{
this.pageobj = data
console.log('this page obj',this.pageobj)
})
this._apiService.getPosts().subscribe(data =>{
this.postsobj = data;
console.log('this post obj',this.postsobj)
})
}
this.pageobj is an object
this.postsobj
in both responses they had a property 'slug'.
I would like to check if in this.postsobj or this.pageobj has an object that contains 'slug' == 'hello-word', if so to return me object and store in var this.content
UPDATE
export class PageSingleComponent implements OnInit {
page: Page;
pageobj:any;
postsobj:any;
pageobjCheck:any
postsobjCheck:any
pageSlug:any;
content =new Array<any>();
constructor( private _apiService: apiService, private route: ActivatedRoute ) { }
getdata(slug){
this._apiService.getPages().subscribe(data =>{
this.pageobj = data
this.content.push(_.filter(this.pageobj, { 'slug':' hello-world' }));
})
this._apiService.getPosts().subscribe(data =>{
this.postsobj = data;
this.content.push(_.filter(this.postsobj, { 'slug':' hello-world' }));
})
}
ngOnInit() {
this.route.params.forEach((params: Params) => {
// Get slug from the rout
let slug = params['pageslug'];
console.log('slug is catcheds', slug)
this.pageSlug = params['pageslug'];
this.getdata(slug)
// Run functions
//
});
}
}
I think you want to use Filter function.
In your callback function passed to map function you want to check whether your response object form array has slug property which is equal to 'hello world'. Your code will like like this:
var content = response.filter(obj => obj && obj.slug === 'hello-world');
I prefer using lodash as below,
this.content =new Array<any>();
this.content.push(_.filter(this.pageobj, { 'slug':' hello-world' });
this.content.push(_.filter(this.postsobj, { 'slug':' hello-world' });
Alternatively you can handle it in the service using takeWhile operator
getPages(){
return this.http.get(...)
.takeWhile(data=>{
if(data.slug ==== 'hello-world'){
return data;
}
})
}
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.