NGXS selector or Selector annotation return is passing by value or reference? - ngxs

what is ngxs' way of passing object?
I have this following state
#State<CounterStateModel>({
name: 'counter',
defaults: { count: 0, isLoading: true, tests: [{value: 0}] }
})
and the selector
#Selector()
static count(state: CounterStateModel) {
return state.count;
}
#Selector()
static test(state: CounterStateModel) {
return state.tests;
}
I used those two selectors in component.ts
#Select(CounterState.test) test$: Observable<TestModel[]>;
#Select(CounterState.count) count$: Observable<number>;
then in template I show the value by
<div class="count" *ngFor="let test of (test$ | async)">TEST: {{ test?.value }}</div>
<div class="count">COUNT: {{ (count$ | async) }}</div>
then I increase the count or value by subscribing and directly plus 1
incrementT() {
this.test$.subscribe(test => test.map(t => t.value++))
}
incrementC() {
this.count$.subscribe(count => count++)
}
after clicking button
the value in test array element is increased, but count remains the same.
so what is NGSX passing?
see the stackblitz url:
https://stackblitz.com/edit/ng-ngxs-playground-pmekh1?file=src/app/counter/counter.component.ts

To mutate values contained in the state you need dispatch an action to the state, rather than modify the values that you receive in your subscriptions.
NGXS enforces this rule to ensure state is immutable, so if in your stack blitz you set developmentMode: true you will see NGXS errors in the console when you try to increment the 'test' values.
To modify the count or the test items you need to dispatch an action to the state and have the state make the update (thus emitting the new value to your subscribed component). This is outlined here in the NGXS documentation. This is a bit of a different way of thinking if you are new to NGXS (or CQRS patterns).

Related

Use methods and computed properties in child component

In my List component I have a method which count the length of the array within certain categories.
methods: {
getLengthofaCategory(cat) {
const LowerCaseSearch = this.search.toLowerCase();
let categoryCount = this.products.filter(
product =>
(product.name.toLowerCase().includes(LowerCaseSearch) ||
product.category.toLowerCase().includes(LowerCaseSearch)) &&
(!this.checked.length || this.checked.includes(product.category)) &&
product.category === cat
);
return categoryCount.length;
}
}
See here my setup in this sandbox.
But I want the values next to the checkboxes (which are coming from my CheckBox component).
How do I get the logic from the method getLengthofaCategory into my CheckBox component?
So I am able to use {{ getLengthofaCategory('tennis') }} in the v-for loop, inside the CheckBox component. And then maybe I can also use category.value instead of hardcoding e.g 'tennis' as the paramater?
In your list.vue, you can use the already created computed function filteredData instead of doing the filter again. This saves some performance because in Vue, computed properties are "cached" once run.
So you can create a new computed function that creates an object with keys per category and value can either be just the amount or an array of products in this category.
I would then pass this computed value to the CheckBox component via a prop, then inside the CheckBox component, you can display the .length or value regarding how many items each category has:
List.vue:
computed: {
//...
amountPerCategory() {
return this.filteredData.reduce((categories, product) => {
if (!(product.category in categories)) {
categories[product.category] = [];
}
categories[product.category].push(product);
return categories;
}, {});
}
}
CheckBox.vue:
<span class="ml-2 text-gray-700 capitalize">{{ category.value }}</span> -
<span
v-if="count[category.value]"
class="ml-2 text-gray-700 capitalize"
>{{ count[category.value].length }}</span>
count: {
type: Object,
default: () => ({})
}
https://codesandbox.io/s/admiring-ellis-4hojl?file=/src/components/CheckBox.vue

Calling a huge object inside v-for causing massive lag?

recently I'm working on a project showing sport events data
While v-for through the events array (which is around 500~2000 length), I'll call a method in each to show their participants name
<div v-for="event in eventArr" :key="event.id">
<template v-if="mappingObj[event.id]">
<div class="participantName">
{{ getCorrespondParticipants (
mappingInfo[event.id][mappingId],
mappingInfo[event.id][sportId])
}}
</div>
</template>
</div>
what it looks like in template, mappingObj is a object collecting all event data with event id as key, its length is around 500~2000(same as the origin events array)
getCorrespondParticipants (pidArr, sportId) {
this.participantListObj[sportId];
});
// this part was supposed to get corresponded participants data from the object, returning this.participantListObj[sportId][pid in pidArr]
return pArr;
},
The method was trying to get the participants info, tho even if I remove all the lines just left 'this.participantListObj[sportId]' there(so it basically did nth but calling the obj), it still causing massive lag result to slow performance for my page, not to mention there's another method to get the league info
participantListObj () {
let cloneObj = JSON.parse(
JSON.stringify(this.totalParticipantList)
);
for (let sportId in cloneObj) {
cloneObj[sportId] = cloneObj[sportId].reduce(
(pObj, item) =>
Object.assign(pObj, { [item.participant_id]: item }),
{}
);
}
return cloneObj;
},
which would looks like...
participantListObj :{
1: {
123:{
participant_id: 123
name_EN: 'Player',
name_CN: '玩家'
},
...(18000+)
}
}
The obj is participants list object under each sport Id, made from a participants array list which length could be around 18000, in this case the page has only one sport that is soccer. Did I did anything wrong?

Dynamic data storage during input with laravel and vuejs

I have a multistep form on vue js. I need to dynamically during input send requests, validate on the server and receive a response to display validation errors.
My data:
data() {
return {
form: {...this.candidate}, // we get an object with fields already filled *
errors: new Errors(), // object with validation errors
}
},
Now for each input I have computed property:
characteristicFuturePlans: {
get() {
return this.form.characteristic_future_plans;
},
set(value) {
this.saveField('characteristic_future_plans', value);
}
},
saveField method sends data:
saveField(field, value) {
this.form[field] = value; // keep the data in the object relevant
axios.put(`/api/candidate/${this.candidate.token}`, {[field]: value})
.then(() => { this.errors.clear(field) })
.catch((error) => {
this.errors.record(field, error.response.data.errors[field]);
});
},
Now, with each change of input, a request will be sent. But with this approach, when we quickly type text in a field, sometimes the penultimate request sent comes after the last. It turns out that if you quickly write "Johnny", sometimes a query with the text "Johnn" will come after a query with the text "Johnny" and the wrong value will be saved in the database.
Then I made sure that the data was sent 1 second after the termination of text input. Added timerId: {} to data(){} and then:
saveField(field, value) {
if(this.timerId[field]) {
clearTimeout(this.timerId[field]);
}
this.timerId[field] = setTimeout(this.send, 1000, field, value);
},
send(field, value) {
this.form[field] = value;
axios.put(`/api/candidate/${this.candidate.token}`, {[field]: value})
.then(() => { this.errors.clear(field) })
.catch((error) => {
this.errors.record(field, error.response.data.errors[field]);
});
},
But now, if after filling in the input in less than a second, press the button to go to the next step of the form, the page will simply refresh. (the button to go to the next step will send a request to the server to check if the required fields are filled)
How correctly save data to the database during text input? Can I do this without setTimeout()? If so, how can I make sure that the data of the last request, and not the penultimate, is stored in the database?
I will be glad to any tip.
Updated. Attach some template code.
Part of Step[i].vue component:
<div class="row">
<div class="form-group col-md-6">
<element-form title="Title text" :error="errors.get('characteristic_future_plans')" required isBold>
<input-input v-model="characteristicFuturePlans" :is-error="errors.has('characteristic_future_plans')"
:placeholder="'placeholder text'"/>
</element-form>
</div>
</div>
template of input-input component:
<input :value="value" :type="type" class="form-control" :class="isErrorClass"
:placeholder="placeholder" #input="$emit('input',$event.target.value)">
Components of form steps are called from the Page component. Nearby is the component of the button for moving to the next step.
<component :is="currentStep"
:candidate="candidate"
// many props
:global-errors="globalErrors"/>
<next-step :current-step="step" :token="candidate.token"
#switch-step-event="switchStep" #throw-errors="passErrors"></next-step>
NextStep component sends a request to the server, it is checking whether the required fields are filled in the database. If not, throw out a validation error. If so, go to the next form step.
you could try with watching the input values, and then use _.debounce() from underscore.js (src: https://underscorejs.org/#debounce) to delay the method call that makes a server request:
watch: {
fieldName: _.debounce(function(value) {
if(value === ''){
return;
}
this.saveField(this.fieldName, value);
},
...

Unclear syntax in redux-form

Having trouble understanding the syntax in redux-form...
In redux-docs
const renderField = (field) => (
<div className="input-row">
<input {...field.input} type="text"/>
{field.meta.touched && field.meta.error &&
<span className="error">{field.meta.error}</span>}
</div> )
How is the spread operator used inside the input and why does the following returnes an error:
<input {...field.input, id} type="text"/>
The second syntax I dont understand is:
class MyCustomInput extends Component {
render() {
const { input: { value, onChange } } = this.props <---??
return (
<div>
<span>The current value is {value}.</span>
<button type="button" onClick={() => onChange(value + 1)}>Inc</button>
<button type="button" onClick={() => onChange(value - 1)}>Dec</button>
</div>
)
}
}
What is:
input: { value, onChange }
?
Queston 1:
The error from using {...field.input, id} should be something like the following:
Syntax error: ...: Unexpected token, expected }
If you already have props as an object, and you want to pass it in
JSX, you can use ... as a “spread” operator to pass the whole props
object (Spread Attributes)
So it appears the JSX attribute spread is not exactly the same as a es2015 spread. I believe it is limited to just the examples in the react docs.
Question 2:
The Field component passes these props to your wrapped component MyCustomInput
input.onChange(eventOrValue) : Function
A function to call when the form field is changed. It expects to either receive the React SyntheticEvent or the new value of the field.
input.value: any
The value of this form field. It will be a boolean for checkboxes, and a string for all other input types. If there is no value in the Redux state for this field, it will default to ''. This is to ensure that the input is controlled. If you require that the value be of another type (e.g. Date or Number), you must provide initialValues to your form with the desired type of this field.
In other words when MyCustomInput fires onChange the Field component will dispatch an action to update the form store.
The value prop is used to maintain the state MyCustomInput.

Angular2 - Custom validator using old data

I'm having an issue with a custom validator that I've written. The validator is used to validate all the data in a component.
Here's the setup:
The component displays a table with X rows of data. Some of the rows are display-only mode and others will be in edit mode (certain cells in the table will have inputs, selects, etc). Backing the table is an array of data objects (tableData).
If a row is in edit mode, each input is two-way bound to the appropriate element in the tableData array.
My custom validator is applied to the form tag and takes tableData as input. Everything mostly works. The validation data looks at each row in the table and does everything I need it to do.
The issue is that the data based to my custom validator is old data. So if a row is in edit mode, when I change a value in a select, tableData is updated, but the version of it that's passed to the validator is before the update. So I'm always validating an old version of tableData.
I'm not sure how to get the validation to use the up to date version of tableData. I think the issue may be related to the fact that the select binding changes a value of an object in the tableData array, but the tableData array itself doesn't actually change.
I tried adding callback to the (change) event on the select in the row being edited. The method called on (change) manually triggers change detection using a ChangeDetectorRef, but that didn't work.
I don't want to spam everyone with all the entire files, so I've tried to just add the important snippets.
Here's the template:
<form #f="ngForm" novalidate custom-validator="{{tableData | json}}">
<p-dataTable [value]="tableData">
...
<p-column [header]="'Program Name'">
<template let-row="rowData" let-idx="rowIndex" pTemplate type="body">
<span *ngIf="!row['edit']">
{{row['data'].programName}}
</span>
<div *ngIf="row['edit']">
<select #progName="ngModel" [(ngModel)]="row['data'].programCode"
title="Select Program" required (change)="onProgramChange($event, idx)"
name="programSelect-{{idx}}">
<option [value]=""></option>
<option *ngFor="let prog of programList" [value]="prog.code">
{{prog.name}}
</option>
</select>
</div>
</template>
</p-column>
...
</p-dataTable>
</form>
Here's the backing component:
//imports...
...
private tableData: PersonAssignmentRowData[] = [];
private programList: Program[] = [];
...
onProgramChange(event: any, index: number) {
for(let prog of this.programList) {
if(prog.code == event.target.value) {
this.tableData[index].data.programAcronym = prog.acronym;
this.tableData[index].data.programLocation = prog.location;
this.tableData[index].data.programName = prog.name;
break;
}
}
}
...
Here's the validator:
#Directive({
selector: '[custom-validator]',
providers:[{provide: NG_VALIDATORS, useExisting: CustomValidator, multi: true}]
})
export class CustomValidator implements Validator{
#Input('custom-validator') tableDataString: string;
validate(control: AbstractControl) {
if(this.tableDataString == null || this.tableDataString.length == 0) {
return null;
}
let tableData: PersonAssignmentRowData[] = [];
tableData = JSON.parse(this.tableDataString);
let message: string = '';
//logic that tests the validity of the data and sets any error messages in the message variable
if(message.length > 0) {
return {'validationMessage': message};
} else {
return null;
}
}
}
Now it's clear. Of course it will not work. The only data source that the validator should check is a control that's passed to validate() method. No #Inputs() or anything of the kind. The only difference is that control.value below will contain all values of all controls in the form, not just your table, so you should pick the correct nested value where your table is.
#Directive({
selector: '[custom-validator]',
providers:[{provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidator), multi: true}]
})
export class CustomValidator implements Validator {
validate(control: AbstractControl) {
tableData = control.table.value; // need to figure out exact path based on your form structure
let message: string = '';
//logic that tests the validity of the data and sets any error messages in the message variable
if(message.length > 0) {
return {'validationMessage': message};
}
return null;
}
}

Resources