Updating an array in SharePoint spfx using React & PnPJS - arrays

I'm creating a web app that allows the user to update their status and location.
I have a data list table on SharePoint with the user's name, email address, status (for example: online, offline, or busy), location (which is a select field), along with other fields.
The web app is just 2 different select fields. Which allows the user to update his status and location.
When the user accesses the page on componentDidMount() I'm getting the user's email addresses (since he's logged into SharePoint) and then filtering the data list array to view the element for his information (so looking for his email address in the MyList. The part I'm stuck at now is updating the MyList list with the selected response that the user selected.
Using PnP-JS i found this should be possible here are two links showing the update() function.
https://github.com/SharePoint/PnP-JS-Core/wiki/Basic--Operations
https://github.com/SharePoint/PnP-JS-Core/wiki/Working-With:-Items
My code found here:
export default class SigninLocationWebpart extends React.Component<ISigninLocationWebpartProps, {Status: string, Location: string, userName: string, getEmail: string, selectedUser: any}> {
constructor(props) {
super(props);
this.state = {
Status: 'Online',
Location: 'New York',
userName: '',
getEmail: '',
selectedUser: {},
};
this.handleChangeStatus = this.handleChangeStatus.bind(this);
this.handleChangeLocation = this.handleChangeLocation.bind(this);
}
handleChangeStatus(event) {
const { value } = event.target;
this.setState({ Status: value });
}
handleChangeLocation(event) {
const { value } = event.target;
this.setState({ Location: value });
}
private _onUpdate(event) {
event.preventDefault();
//This is where I need help on updating list
let list = pnp.sp.web.lists.getByTitle("MyList")
//Instead of getting by ID i need to get by that selectUser array I believe
list.items.getById(1).update({
Status: this.state.Status, //User changing from Online to Offline
Location: this.state.Location //User changing from New York to Los Angeles
}).then(i => {
console.log(i);
});
}
public componentDidMount() {
if (Environment.type === EnvironmentType.Local) {
}
else if (Environment.type === EnvironmentType.SharePoint || Environment.type === EnvironmentType.ClassicSharePoint) {
//This gets the current users info and sets it to username and email
sp.web.currentUser.get().then((response : CurrentUser) => {
//console.log(response);
this.setState({
userName: response["Title"],
getEmail: response["Email"],
})
})
//This gets the list of all all items in the list
pnp.sp.web.lists.getByTitle("MyList").items.get().then((items: any[]) => {
console.log(items);
//Comparing email from sign in user and filtering items array to get that element
var compareEmail = this.state.getEmail;
console.log(compareEmail);
let selectedUser = _.filter(items, function(item) {
return item.Email_x0020_Address.toLowerCase() === compareEmail.toLowerCase();
});
console.log(selectedUser);
});
}
}
public render(): React.ReactElement<ISigninLocationWebpartProps> {
return (
<div className={ styles.signinLocationWebpart }>
<h3>Hello {this.state.userName}</h3>
<form onSubmit={this._onUpdate}>
<label>
Check In our Out
</label>
<select name="Status" value={this.state.Status} onChange={this.handleChangeStatus}>
<option value="Online">Online</option>
<option value="Offline">Offline</option>
<option value="Busy">Busy</option>
</select>
<label>
Location
</label>
<select name="Location" value={this.state.Location} onChange={this.handleChangeLocation}>
<option value="New York">New York</option>
<option value="Michigan">Michigan</option>
<option value="Los Angeles">Los Angeles</option>
</select>
<input type="submit" value="Submit" />
</form>
</div>
);
}
}

First of all, instead of getting all items in the List, and then filtering for the current user, you should get only the item(s) for the current user to begin with. Once you list gets large, you would be performing a lot of overhead by retrieving all items.
Secondly, and what you allude to in your comments, is that you need to specify the ID of the item to update. So, in your componentDidMount, after you get the List Item for the current user, save that Item in your state.
public componentDidMount() {
if (Environment.type === EnvironmentType.Local) {
}
else if (Environment.type === EnvironmentType.SharePoint || Environment.type === EnvironmentType.ClassicSharePoint) {
//This gets the current users info and sets it to username and email
sp.web.currentUser.get().then((response : CurrentUser) => {
//console.log(response);
this.setState({
userName: response["Title"],
getEmail: response["Email"],
});
pnp.sp.web.lists.getByTitle("MyList").items.filter("Email_x0020_Address eq '" + this.state.getEmail + "'").top(1).get().then((items: any[]) => {
if (items && items.length) {
this.setState( { selectedUser: items[0] } );
}
});
})
}
}
Then at update time, you can use the ID of that item to save it.
private _onUpdate(event) {
event.preventDefault();
//This is where I need help on updating list
let list = pnp.sp.web.lists.getByTitle("MyList")
//Instead of getting by ID i need to get by that selectUser array I believe
list.items.getById(this.state.selectedUser.ID).update({
Status: this.state.Status, //User changing from Online to Offline
Location: this.state.Location //User changing from New York to Los Angeles
}).then(i => {
console.log(i);
});
}
Additionally, you'll want to make sure you are binding your submission handler just like you are doing for your onchange handlers in your constructor:
this._onUpdate = this._onUpdate.bind(this);
I will also add, that unless you've make sure to pre-populate the List with all possible users, and will always keep it updated with new users, it would be best in to put a check in your _onUpdate that if this.state.selectedUser == null || this.state.selectedUser.ID == null then you should create a new item (and add the new item to your this.state.selectedUser), instead of updating.

Related

Avoid pushing duplicate objects to an angular array

I have multiple checkboxes in my angular application. When user checked and unchecked checkboxes I want to pass those true/false values into an array. It's happening from below code.
But my problem is as you can see the below console.log, it has duplicate checkbox values(index 0 and 3 have same thing) and push it to the array.
I want to know how to check duplicate objects and avoid pushing object to the array.
.ts file
layerChange(e:any){
var isChecked = e.target.checked;
var id = e.target.attributes.id.nodeValue;
const layer = {
isChecked: isChecked,
id: id,
}
this.layers.push(layer);
console.log(this.layers);
}
.html file
<input id="population" (change)="layerChange($event)" type="checkbox">
<input id="gender" (change)="layerChange($event)" type="checkbox">
<input id="householdIncome" (change)="layerChange($event)" type="checkbox">
console.log(this.layers)
**0: {isChecked: true, id: 'population'}**
1: {isChecked: true, id: 'age'}
2: {isChecked: false, id: 'population'}
**3: {isChecked: true, id: 'population'}**
You can check if an entry exists first using either :
var id = e.target.attributes.id.nodeValue;
var attr = this.layer.find(x => x.id === id);
if(!attr)
this.layers.push(layer);
or
this.layer.filter(x => x.id === id)
Edit
in your scenario is better to construct the array only one time in page load.
ngOnInit(): void {
this.layer = [{id: 'age', isChecked: false}, {id:'population',isChecked: false}]
}
and then alter the check state when user check/uncheck :-
layerChange(e:any){
var isChecked = e.target.checked;
var id = e.target.attributes.id.nodeValue;
this.layer.find(x => x.id === id).isChecked = isChecked;
console.log(this.layers);
}
You can create a string array which contains only ids and you can insert or remove elements from the array as per the selection
layerChange(e:any) {
const id = e.target.attributes.id.nodeValue;
const index = this.layers.findIndex(el => el === id)
if(index === -1) {
this.layers.push(id);
} else {
this.layers.splice(index, 1)
}
console.log(this.layers);
}
my GOD!
Kalana (and others) we need re-thinking the problem using "variables". Yes, Angular philosophy is binding variables. Variables in .ts makes the values are showed in the .html
So, some simple like declare an array of object in the .ts
layers:any[]=[{isChecked: true, id: 'population'},
{isChecked: true, id: 'age'},
{isChecked: false, id: 'population'}
]
Allow us write in .html
<ng-container *ngFor="let layer in layers">
<input type="checkbox" [(ngModel)]="layer.isChecked"
(change)="onChange()">
</ng-container>
In .ts we has a function:
onChange(){
console.log(this.layers)
}
Finally , I was able to find a solution. Thank you.
layerChange(e:any){
var isChecked = e.target.checked;
var id = e.target.attributes.id.nodeValue;
const index = this.layers.findIndex(el => el.id === id);
const layer = {
isChecked: isChecked,
id: id,
}
if(index > -1){
this.layers[index].isChecked = isChecked;
}else{
this.layers.push(layer);
}
console.log(this.layers);
}

How do I validate a checkout form in React?

I am trying to implement a checkout form in React. The form has 4 fields in all: Name, CC Number, CC expiration and CVV. I am using a library that validates each field on unfocus. The validation is triggered by the validationCallback method which takes 3 arguments: field, status, and message. I'd like to key off of the status for each input and only allow submit once each status === true. Here is my code.
constructor(props) {
super(props);
this.state = {
nameOnCard: '',
errorMessage: '',
showLoaderForPayment: '',
collectJs: null,
token: null,
isPaymentRequestCalled: false,
showErrorModal: false,
paymentErrorText: '',
disabled: true,
};
}
I have a disabled property in my state which I'm initially setting to true.
validationCallback: (field, status, message) => {
if (status) {
this.setState({ errorMessage: '' });
} else {
let fieldName = '';
switch (field) {
case 'ccnumber':
fieldName = 'Credit Card';
break;
case 'ccexp':
fieldName = 'Expire Date';
break;
case 'cvv':
fieldName = 'Security Code';
break;
default:
fieldName = 'A';
}
if (message === 'Field is empty') {
this.setState({ errorMessage: `${fieldName} ${message}` });
} else {
this.setState({ errorMessage: `${message}` });
}
}
},
In the above method, I'd like to set disabled to false if each of the field's status===true... Below is the button which I'm setting to be the value of this.state.disabled.
<button
className="continueBtn disabled"
disabled={this.state.disabled}
onClick={this.handleCardSubmit}
>
<span className="fa fa-lock" />
Pay $
{selectedPayment.amount}
</button>
I hope this is enough of the code to help with the issue. I can provide more of the file if need be.
From what i understand, you want to set the button to NOT DISABLED if all the fields are filled properly, i.e. all status are true.
What you can do is maintain a boolean array for each field and update the status in that array, i.e. initialize an array of length = no. of fields (in your case 3) and set all values as false. False depicts that the field hasn't been validated.
this.state = {
statusArray = [false, false, false] // For as many fields
}
Then in validationCallback, set the index as true or false for that field i.e. if the 2nd field status is returned true by your validation library, set statusArray as [false, true, false].
The form will only be validated if all 3 of the values become true. So you can iterate over the array and check if array has all 3 values as true. or you can use the logical AND operator which returns true only if all values are true(the approach which i use below).
For the button,
<button disabled={this.checkDisable()}>
checkDisable = () => {
let temp = this.state.statusArray;
let answer = true;
for(int i=0;i<temp.length;i++)
answer = answer && temp[i];
return answer; // Only returns true if all 3 values are true
}
I hope you get it now.
You need to check 2 things, has the form been touched and are there any errors. I don't know what library you are using but most likely it has a property touched in it, if not add an onFocus to each input field and a touched property in your state. You don't really need a disabled property in your state since its a computed value. Just check on every render if the form has been touched and if there are any errors.
state = {
...,
touched: false,
...
}
handleFocus = () => this.setState({touched: true})
render(){
const disabled = !!(this.state.touched && this.state.errorCode)
return(
...
<input onFocus={this.handleFocus} ... />
...
<button disabled={disabled}
)
}
EDIT:
state = {
...
validInputs: []
}
validationCallback: (field, status, message) => {
if (status) {
this.setState((state) => ({ errorMessage: '', validInputs: [... new Set([...state.validInputs, field])] }));
} else {
...
render(){
const disabled = this.state.length < inputs.length // the number of the input fields
return(
...
<button disabled={disabled} >
...
)

How can I return true if a specific object was found in an array?

I have a simple form with checkboxes made in Angular and I would like to make the checkbox check itself if the user has this role.
Here is what I tried:
<form [formGroup]="rolesForm">
<label
formArrayName="roles"
*ngFor="let role of rolesForm.controls['roles'].controls; let i = index"
>
<input
type="checkbox"
[checked]="checkIfTrue(role[i])"
[formControlName]="i"
/> {{role[i].name}}
</label>
</form>
The component itself:
roles: Role[] = [
{
uid: '456DNC',
name: 'Admin'
},
{
uid: '546DKZ',
name: 'Member'
},
{
uid: '741JXY',
name: 'Guest'
}
]
user: User = {
uid: '123ABC',
name: 'Steve',
roles: [
{
uid: '456DNC',
name: 'Admin'
}
]
}
rolesForm: FormGroup;
// So I can get every roles and the user can check multiple checkboxes to get as many roles as it wants
ngOnChanges() {
const formControls = this.roles.map(role => new FormControl(false));
this.rolesForm = this.formBuilder.group({
roles: new FormArray(formControls)
});
}
checkIfTrue(role: Role) {
// I assume that it should return true if it finds the role in the user.roles array but it doesn't. It doesn't check the box and I get an eror.
return this.user.roles.find(role);
}
I'm getting something like this: [object Object] is not a function at Array.find
I have tried the following functions .indexOf() and .includes()
You should set the value when building the form instead of using the [checked] property, because of the reactive form you use.
https://stackblitz.com/edit/angular-hsvug5?file=src/app/app.component.ts
ngOnChanges() {
const formControls = this.roles.map(role => new FormControl(this.checkIfTrue(role)));
this.rolesForm = this.formBuilder.group({
roles: new FormArray(formControls)
});
}
checkIfTrue(role) {
return this.user.roles.find(r => r.uid === role.uid);
}
<form [formGroup]="rolesForm">
<label
formArrayName="roles"
*ngFor="let role of rolesForm.controls['roles'].controls; let i = index"
>
<input
type="checkbox"
[formControlName]="i"
/> {{roles[i].name}}
</label>
</form>
indexOf() return -1 if it does not find elements.
So you can implement something like this:
return this.user.roles.indexOf(role) != -1
In this way you'll have true if indexOf found an element false otherwise.
in your checkIfTrue(role: Role) method
Everything is fine only change its like that :
checkIfTrue(role: Role) {
this.user.roles.find(e=>{
if(e.name === role) {
return true;
}
});
}

Vue.js: Manipulate Array and post form with new data

In my Vue.js application I want to post form data to my Node.js/MongoDB Backend.
This is my source code: https://github.com/markusdanek/t2w-vue/blob/master/src/components/backend/JobEdit.vue
JSON for my job entry: http://t2w-api.herokuapp.com/jobs/591c09a55ba85d0400e5eb61
Relevant code for my question:
HTML:
<div class="row">
<input type='text'
:name="'qual'+index"
v-model="qualifications[index]">
<button #click.prevent="removeQualifiaction(index)">X</button>
</div>
Methods:
onChange(value, $event){
if (!this.job.xmlOnline)
this.job.xmlOnline = []
const index = this.job.xmlOnline.findIndex(v => v == value)
const checked = $event.target.checked
if (checked && index < 0)
this.job.xmlOnline.push(value)
if (!checked && index >= 0)
this.job.xmlOnline.splice(index, 1)
}
removeQualifiaction() {
this.qualifications.splice(this.qualifications.index, 1);
}
Sending the form data with submit button on form end:
editJob() {
let job = Object.assign({}, this.job);
job.qualifications = this.qualifications;
job.responsibility = this.responsibility;
this.$http.post('https://t2w-api.herokuapp.com/jobs/' + this.$route.params.id, job).then(response => {
console.log(response);
}, response => {
console.log(response);
});
}
My problems now:
When I edit a "Job", I have a list of "qualification items", that are input fields in my form.
Clicking the "delete" button results that the first input gets deleted, not the one I am clicking. Done with #thanksd answer.
How do I add a button and method to add a new input field and to append it to my job.qualifications?
In my JobAdd.vue implemented, to add a new entry to job.qualifications, like this:
<a #click.prevent="addQualification">+</a>
addQualification() {
this.qualification.push({ text: '' });
}
addJob() {
let job = Object.assign({}, this.job);
job.qualifications = this.qualification.map(q => q.text);
this.$http.post('https://t2w-api.herokuapp.com/jobs/', job).then(response => {....
Full source for my JobAdd.vue: https://github.com/markusdanek/t2w-vue/blob/master/src/components/backend/JobAdd.vue
this.qualification.push({ text: '' }); doesnt work obviously not in my JobEdit.vue when there are already strings in my job.qualifications.
Change your removeQualifiaction method to use the index being passed in:
removeQualifiaction(index) {
this.qualifications.splice(index, 1);
}

Why is my state being updated here?

I'm pretty new to React, but liking it so far. I'm building a large application, which is going well, except I've run into an issue. I'm building a list of responses to questions, and they can be deleted, but I also want to have a "Cancel" button so all unsaved changes can be reverted. What is confusing me is the cancel button reverts to the initial state for the name value, but not the responses. If I add in some console logging to the response deletion script, I would expect to see log lines 1 & 2 match, with 3 being different. However, I'm seeing that 1 is the original, but 2 & 3 match. Why is state being updated before I call setState, and why does updating state seem to update the my initial props?
EDIT: I added a jsFiddle
getInitialState: function() {
return {
name: this.props.question.name,
responses: this.props.question.responses,
};
},
handleCancelButtonClick: function(e) {
this.replaceState(this.getInitialState());
},
handleNameChange: function(e) {
this.setState({name: e.target.value});
},
handleResponseDeletion: function(e) {
var resp = this.state.responses;
var from = Number(e.target.value);
console.log(JSON.stringify(this.state.responses));
resp.splice(from, 1);
console.log(JSON.stringify(this.state.responses));
this.setState({responses: resp});
console.log(JSON.stringify(this.state.responses));
},
render: function() {
var key = "mp" + this.props.question.name;
var resp = [];
if (this.state.responses) {
this.state.responses.forEach(function(response, i) {
var rkey = "r_" + this.props.question.name + "_" + i;
resp.push(<ModalResponse response={response} key={rkey} value={i} deleteResponse={this.handleResponseDeletion} />);
}.bind(this));
}
return (
<layer id={this.props.question.name} style={questionModal} key={key}>
<h2>Edit {this.state.name}</h2>
<button onClick={this.handleCancelButtonClick}>Cancel</button>
<div class='form-group'>
<label for='client_name' style={formLabel}>Question Name:</label><br />
<input type='text' style={formControl} id='question_name' name='question_name' value={this.state.name} onChange={this.handleNameChange} required />
</div>
<div class='form-group'>
<label style={formLabel}>Responses:</label><br />
<ul style={responseList} type="response_list" value={this.props.qname}>
{resp}
</ul>
</div>
</layer>
);
}
});
The problem is that splice modifies original array. It means the one that belongs to the original question. So when you call getInitialState from within handleCancelButtonClick you get modified array.
To avoid this you need to somehow clone original data inside getInitialState. For example
getInitialState: function() {
//copy array and responses
const copy = resp => ({...resp})
return {
name: this.props.question.name,
responses: this.props.question.responses.map(copy)
};
}
Here's what I did to fix the issue:
handleResponseDeletion: function(e) {
var resp = []
var from = Number(e.target.value);
this.state.responses.forEach(function(res, i) {
if (i != from) {
resp.push(res);
}
});
this.setState({responses: resp});
},

Resources