Mobx Losing State when child component calls method(function) in parent component (jsFiddle demo) - mobx

Any help much appreciated:
Mobx seems to lose state between child and parent components.
I've made a Fiddle demo below. Includes comments and output for clarity.
( Current versions of React, Mobx, and Mobx-React )
Thanks.
JSFIDDLE DEMO HERE
https://jsfiddle.net/ymp1g6qk/3/
SCREENSHOT OF DEMO HERE
CONSOLE OUTPUT HERE
THE EXAMPLE CODE HERE:
const {observable, computed, extendObservable} = mobx;
const {observer} = mobxReact;
const {Component} = React;
const {render} = ReactDOM
const {autorun} = mobx
class MyItem {
id = 0;
#observable name = "";
#observable selected = false;
constructor(id, name, selected) {
this.id = id;
this.name = name;
this.selected = selected;}
}
/* ESTIMATE STORE */
// #observable <---- Observable not allowed. Generates "Not a Constructor Error"
class MyStore extends Component {
#observable items = [];
constructor() {
super();
this.items.push(new MyItem(1, 'copper' ));
this.items.push(new MyItem(10, 'Silver' ));
this.items.push(new MyItem(20, 'Gold' ));
this.items.push(new MyItem(30, 'Platinum'));
this.items.push(new MyItem(40, 'Sapphire'));
this.items.push(new MyItem(50, 'Diamond' ));
console.log("finished with MyStore:", this.items, " with count: ", this.items.length, " items.");
}
}
let MyItems = new MyStore();
#observer
class MyCard extends Component {
constructor(props){
super(props)
}
_toggle(e){
console.log("... At (Child) Component");
console.log("click event generates id:", e, "...and calls parent component function that was passed as a prop. (it succeeds)");
this.props.onToggleSelection(e);
}
render() {
return (
<li onClick={this._toggle.bind(this, this.props.id )} className={(this.props.selected ? " selected" : "")}>
<p>{this.props.name}</p>
</li>
)
}
}
#observer
class Test extends Component {
/* selections */
item = 0;
#observable selectedMyItems = [];
#computed get itemsSelected() {
return this.selectedMyItems.length;
}
constructor( props ){
super( props );
}
componentWillReceiveProps(nextProps, nextContext) {
console.log(" ...At panel will recieve props and selectedMyItems:", this.selectedMyItems.length);
}
componentDidMount() {
console.log(" ...At DidMount, and selectedMyItems:", this.selectedMyItems.length);
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
console.log(" ...At ShouldUpdate, and selectedMyItems:", this.selectedMyItems.length);
return null;
}
componentWillUpdate(nextProps, nextState, nextContext) {
console.log(" ...At WillUpdate and selectedMyItems:", this.selectedMyItems.length);
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.log(" ...At DidUpdate and selectedMyItems:", this.selectedMyItems.length);
}
componentWillUnmount () {
console.log(" ...At DidUnmount and selectedMyItems:", this.selectedMyItems.length);
}
toggleSelection(id){
var itemindex = -1;
console.log("...At (parent) _toggle selection:", id );
/* find the clicked item */
itemindex = MyItems.items.findIndex((MyItem => MyItem.id == id));
console.log("...the id of the item is :", id);
console.log("...the item's index is:", itemindex);
console.log("...the record for that id is:", MyItems.items[itemindex]);
console.log("...selected items array consists of:", this.selectedMyItems); // <<< Warning: "Undefined"
console.log("...See??? The array is undefined:", this.selectedMyItems);
/* toggle selected */
MyItems.items[itemindex].selected = !MyItems.items[itemindex].selected; // <<< Succeeds.
/* add or remove the selected item */
if ( MyItems.items[itemindex].selected ) {
/* add the item to the list of selected items */
console.log("...See??? The next line's push to the array fails: undefined:", this.selectedMyItems);
this.selectedMyItems.push(id); // <<< ERROR: "Cannot read property 'push' of undefined"
} else {
/* remove the item from the list of selected items */
this.selectedMyItems.splice(this.selectedMyItems.indexOf(id), 1); // <<< ERROR: "Cannot read property 'splice' of undefined"
}
// this.props.uistore.doCommand({cmd: "TOGGLE_PANEL_SELECTION", pld:{ panelname:"estimate", id: choice }});
}
render() {
var i = 1;
return (
<div id="my-panel">
Open the Console.<br/>
Click on an item in the list to select or unselect it.<br/>
Note that selectedMyItems is out of scope (unassigned). Why?
<ul className="">
{MyItems.items.map(MyItem => {
return (
<MyCard key={i++} id={MyItem.id} name={MyItem.name} selected={MyItem.selected} tags={MyItem.tags} onToggleSelection={this.toggleSelection}/>
)
})}
</ul>
<ul className=""></ul>
</div>
);
}
};
ReactDOM.render(<Test/>, app);

Method toggleSelection(id) in class Test was not bound.
You should bind it in class Test constructor (prefered):
class Test extends Component {
constructor() {
this.toggleSelection = this.toggleSelection.bind(this);
}
...
}
or bind it "in place":
<MyCard ... onToggleSelection={this.toggleSelection.bind(this)}/>
Check updated jsFiddle version: https://jsfiddle.net/ymp1g6qk/6/

Related

Vue.js 3 Pinia store is only partially reactive. Why?

I'm using Pinia as Store for my Vue 3 application. The problem is that the store reacts on some changes, but ignores others.
The store looks like that:
state: () => {
return {
roles: [],
currentRole: 'Administrator',
elements: []
}
},
getters: {
getElementsForCurrentRole: (state) => {
let role = state.roles.find((role) => role.label == state.currentRole);
if (role) {
return role.permissions.elements;
}
}
},
In the template file, I communicate with the store like this:
<template>
<div>
<draggable
v-model="getElementsForCurrentRole"
group="elements"
#end="onDragEnd"
item-key="name">
<template #item="{element}">
<n-card :title="formatElementName(element.name)" size="small" header-style="{titleFontSizeSmall: 8px}" hoverable>
<n-switch v-model:value="element.active" size="small" />
</n-card>
</template>
</draggable>
</div>
</template>
<script setup>
import { NCard, NSwitch } from 'naive-ui';
import draggable from 'vuedraggable'
import { usePermissionsStore } from '#/stores/permissions';
import { storeToRefs } from 'pinia';
const props = defineProps({
selectedRole: {
type: String
}
})
const permissionsStore = usePermissionsStore();
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const onDragEnd = () => {
permissionsStore.save();
}
const formatElementName = (element) => {
let title = element.charAt(0).toUpperCase() + element.slice(1);
title = title.replace('-', ' ');
title = title.split(' ');
if (title[1]) {
title = title[0] + ' ' + title[1].charAt(0).toUpperCase() + title[1].slice(1);
}
if (typeof title == 'object') {
return title[0];
}
return title;
}
</script>
My problem is the v-model="getElementsForCurrentRole". When making changes, for example changing the value for the switch, the store is reactive and the changes are made successfully. But: If I try to change the Array order by dragging, the store does not update the order. I'm confused, because the store reacts on other value changes, but not on the order change.
What can be the issue here? Do I something wrong?
-Edit- I see the following warning on drag: Write operation failed: computed value is readonly
Workaround
As workaround I work with the drag event and write the new index directly to the store variable. But...its just a workaround. I would really appreciate a cleaner solution.
Here is the workaround code:
onDrag = (event) => {
if (event && event.type == 'end') {
// Is Drag Event. Save the new order manually directly in the store
let current = permissionsStore.roles.find((role) => role.value == permissionsStore.currentRole);
var element = current.permissions.elements[event.oldIndex];
current.permissions.elements.splice(event.oldIndex, 1);
current.permissions.elements.splice(event.newIndex, 0, element);
}
}
You should put reactive value on v-model.
getElementsForCurrentRole is from getters, so it is treated as computed value.
Similar to toRefs() but specifically designed for Pinia stores so
methods and non reactive properties are completely ignored.
https://pinia.vuejs.org/api/modules/pinia.html#storetorefs
I think this should work for you.
// template
v-model="elementsForCurrentRole"
// script
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const elementsForCurrentRole = ref(getElementsForCurrentRole.value);

Why does my context consumer not automatically update?

I am working on a react native app and I have an array of components that I need to be updated from different sections of my app. I am using context to be able to provide different consumers with this array, as well as the ability to the update the array.
As you can see in the code below I am passing my array, as well as a function to update the list, as the value for my provider. I then am using "header-right" in the navigation to call the function that updates the array in the provider state. I have confirmed that the array is updating and will only re-render the consumers when a change state in another component causes a re-render.
What I need is the change state of the provider to re-render all consumer components when it is changed, not when another components state change has occurred.
const DbContext = React.createContext("DEFAULT");
export default class DbProvider extends Component {
constructor(props){
super(props);
this.state = {
choreList: [],
refresh:null,
}
}
componentDidMount(){
sqlQueries.getChores().then(row => this.setChoresList(row));
}
updateChoreList = (choresAdded, offset) => {
console.log("K");
if(offset == 1){ // if adding chores
var tempList = this.state.choreList;
tempList.push(choresAdded);
this.setState({
choreList: tempList,
})
}else if(offset == -1){ // if removing a chore
for(var i = 0; i < this.state.choreList.length; i++){
for(var j = 0; j < choresAdded.length; j++){
if(this.state.choreList[i].state.name == choresAdded[i].state.name){ // if chore being removed is i in state choreList
var tempList = this.state.choreList; // remove and update state choreList
tempList.splice(i, 1);
this.setState({
choreList: tempList,
});
break;
}
}
}
}
}
setChoresList(row){
var tempList = [];
for(var i = 0; i < row._array.length; i++){
tempList.push(<Chore choreName={row._array[i].name} dayId={row._array[i].id} nav={this.props} inDB={true} key={row._array[i].name}/>);
}
this.setState({
choreList: tempList,
})
}
render () {
return (
<DbContext.Provider value={{choreList: this.state.choreList, update: this.updateChoreList}}>
{this.props.children}
</DbContext.Provider>
)
}
}
export { DbContext, DbProvider }
I am then in another component using this list to display something
render(){
return (
<DbProvider>
<DbContext.Consumer>
{globalChores => globalChores.choreList == 0
? <NoChores nav={this.props.nav} dayState={this} />
: (
<View style={styles.ChoreDayContainer}>
<View style={styles.ChoreListContainer}>
{globalChores.choreList}
</View>
<AddChoresBtn nav={this.props.nav} dayState={this}/>
</View>)}
</DbContext.Consumer>
</DbProvider>
);
}
and finally I am updating the provided array like so
componentDidMount(){
this.props.navigation.setOptions({
headerRight: () => (
<DbProvider>
<DbContext.Consumer>{(db) =>(
<Button
onPress={ () => this.dataToDB(db)}
title="Save"
color="#fff"
/> )}
</DbContext.Consumer></DbProvider>
),
})
}
dataToDB(db){
db.update(this.state.choreShowingList, 1);
}
}

Mat-select is not showing the options: Angular 10

I wonder what is wrong with my use of mat-select. Because it is not showing the list of options:
<form>
<mat-form-field appearance="standard">
<mat-label>New Error Type</mat-label>
<mat-select [(ngModel)]="selectedNewErrorCode" name="faultType">
<mat-option *ngFor="let faultType of faultTypes" [value]="faultType.code">
{{faultType.label}}
</mat-option>
</mat-select>
</mat-form-field>
<p>Selected error: {{selectedNewErrorCode}}</p>
</form>
The component which is displayed in a modal is the following.
/** Imports animations */
import {slideInAnimation} from '../../animations';
/** Imports models */
import {StackTraceView} from '../../objects/stack-trace-view.model';
import {FaultType} from '../../objects/fault-type.model';
#Component({
selector: 'app-consult-details',
templateUrl: './consult-details.component.html',
styleUrls: ['./consult-details.component.sass'],
animations: [slideInAnimation]
})
export class ConsultDetailsComponent implements OnInit {
constructor() {
}
public flux: StackTraceView;
public modifState = false;
/** Used in resetting the form */
originalFlux: string;
faultTypes: FaultType[];
/** determines if the original error flux should be visible or not */
public originalErrorVisible = false;
/** Sets the new fault type for reanalysing the stack trace */
selectedNewErrorCode: string;
ngOnInit(): void {
}
modifyFlux() {
this.modifState = !this.modifState;
}
sendFlux() {
console.log(`The flux changed to ${this.flux.originalFlux}`);
}
/** Reste the form to its original state */
resetForm() {
document.getElementById('toBeReset').innerHTML = this.originalFlux;
this.flux.originalFlux = this.originalFlux;
this.modifState = false;
}
/** Sets the visibility of the original error flux to flse if it is true and vice versa */
isOriginalErrorVisible() {
this.originalErrorVisible = !this.originalErrorVisible;
}
}
The entire component is displayed in a modal. The variable faultTypes is fed in when the modal is called in the parent component. The corresponding code in the parent component is the following:
const detailsContent = this.specificReportList.filter(
entry => entry.messageId === originalMessageId
)[0];
const modalRef = this.modalService.open(ConsultDetailsComponent, {
size: 'xl',
ariaDescribedBy: 'Details'
});
/** The input sata for the pop-up component */
modalRef.componentInstance.flux = detailsContent;
modalRef.componentInstance.originalFlux = detailsContent.originalFlux;
modalRef.componentInstance.faultTypes = this.faultTypeList;
modalRef.result.then((result) => {
this.closeResult = `Close with ${result}`;
}, (reason) => {
this.closeResult = `Dismissed ${this.getDismissReason(reason)}`;
});
}```
As a final comment the variable faulttypes is well fed in via the
parent component and when I use nromal select and option I do not have
any problem; The code works very well, the only problem is when I use
mat-select which is important for me beacuse It gives a unifrom look
and feel to my application.

Angular 5 - Event emitter (Property 'update' does not exist on type ....)

I've got a component that I want to update when a person's name changes by emitting an event. My problem is the code doesn't compile because of an error. This is my code
ApplicationFormComponent
#Output() nameChange = new EventEmitter();
closeAccordion(isComplete: string, accordionToClose: string, accordion: NgbAccordion) {
if (accordionToClose === 'personal-details-panel') {
this.applicationStatusFlags.personalDetailsStatus = (isComplete === 'true');
this.nameChange.emit({ personId: this.personId });
}
}
ApplicationFormComponent.html
<name-display
[personId]="personId"
[placeHolderText]="'Hello'"
(nameChange)="update($event)">
</name-display>
NameDisplayComponent
import { Component, Input, OnChanges, SimpleChanges } from '#angular/core';
import { PersonService } from "../../../service/person.service";
#Component({
selector: 'name-display',
templateUrl: './NameDisplay.component.html',
providers: [PersonService]
})
export class NameDisplayComponent implements OnChanges {
constructor(private readonly personService: PersonService) { }
#Input() personId;
#Input() placeHolderText: string = "";
forename: string = "";
ngOnChanges(changes: SimpleChanges): void {
if (changes["personId"]) {
this.personService.getPersonDetails(this.personId).subscribe((res: IPersonDetails) => {
this.forename = res.forenames;
});
}
};
update(personId: number) {
alert("update name");
this.personService.getPersonDetails(personId).subscribe((res: IPersonDetails) => {
this.forename = res.forenames;
});
}
}
My problem is basically when I use angular cli with the command ng server --aot, it doesn't compile because of this error:
ERROR in src\app\component\ApplicationForm\ApplicationForm.component.html(42,9): : Property 'update' does not exist on type 'ApplicationFormComponent'.
I've written a similar component that uses an event emitter which doesn't have this problem, so I'm stuck with how to fix the error.
Any ideas?
It is because you are passing $event to method.
(nameChange)="update($event)"
But it accepts number.
update(personId: number) {
alert("update name");
}
Please change the method as below.
update(event:any) {
const personId = event as number
alert("update name");
}

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: ''

I am writing a component which takes one #Input parameter and display it in the html bit I am getting below error.
SuggestionsComponent.html:54 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: '<p>so string value</p>
'.
at viewDebugError (core.js:9801)
at expressionChangedAfterItHasBeenCheckedError (core.js:9779)
at checkBindingNoChanges (core.js:9948)
at checkNoChangesNodeInline (core.js:14002)
at checkNoChangesNode (core.js:13976)
at debugCheckNoChangesNode (core.js:14805)
at debugCheckDirectivesFn (core.js:14707)
at Object.eval [as updateDirectives] (SuggestionsComponent.html:54)
at Object.debugUpdateDirectives [as updateDirectives] (core.js:14689)
at checkNoChangesView (core.js:13814)
Here is the component.
export class SuggestionsComponent implements OnInit, AfterViewInit {
#Input() suggestions: Array<Suggestions>;
#Output() approveSuggestion = new EventEmitter<Object>();
constructor(
private elementRef: ElementRef,
) {
}
ngOnInit() {
}
ngAfterViewInit() {
if (this.suggestions && this.suggestions.length > 0) {
this.suggestions
.map((value, index) => {
this.suggestions[index].newSuggestion = value.descriptionSuggestion;
});
}
}
The problem is changing component #Input variable value in ngAfterViewInit(). At that time angular changes view and making some change in bind value in upsets angular.
So moving the #input() value to ngOnInit solves the problem as its the method which executes during component bootstrap.
ngOnInit() {
// This is placed here in the in after view init is because it will throw exception as view will be change at that time
this.suggestions
.map((value, index) => {
this.suggestions[index].newSuggestion = value.descriptionSuggestion;
});
}
ngAfterViewInit() {
if (this.suggestions && this.suggestions.length > 0) {
this.suggestions
.map((value, index) => {
this.elementRef.nativeElement.querySelector('.class' + index).style.display = 'none';
});
}
}