How to trigger validation input after debounce time in Angular2? - validation

I have a form with a "name" control.
<div class="field">
<label>Name</label>
<input ngControl="name">
<p *ngIf="name.pending">
Fetching data from the server...
</p>
<div *ngIf="!name.valid && !name.pending"
class="ui error message">Name is not valid</div>
</div>
The control is built with FormBuilder like this :
this.name = fb.control('', null, this.characterNameValidator.bind(this));
and I created a validator :
characterNameValidator(control: Control) {
let q = new Promise((resolve, reject) => {
setTimeout(() => {
if (this._characterService.isCharacterNameAlreadyExists(control.value)) {
resolve({nameCharacterAlreadyExistsError: true});
} else {
resolve(null);
}
}, 1000)
});
return q;
}
On each keystroke, my validator is called. I'm looking for a way to call the validator only after a debounce time.
I try with valueChanges(), but I understand only if I call a specific service but not in the case of validation.
Edit
Is it a good idea to manage validation manually to achieve my problem ? I don't put a validator in my control but I set errors manually on valueChanges.
this.name = fb.control('');
this.name.valueChanges.debounceTime(400).subscribe(() => {
this.characterNameValidator(this.name).then((validationResult => {
this.name.setErrors(validationResult)
}))
});

See https://github.com/angular/angular/issues/1068 for a related open issue.
If you pass a reference to the control to the validator you could use something like
this.formGp.controls['numberFld'].updateValueAndValidity();
from https://stackoverflow.com/a/33377290/217408

The official way of getting what you want is in upcoming features to form validation.
https://youtu.be/kM5QBOWrUVI?t=9m48s
However, you can manually debounce validation by subscribing to value changes, and setting your own error.
testForm.controls['name'].valueChanges
.do(
res => {
if (res) {
this.testForm.controls['name'].setErrors({loading: true});
}
}
)
.debounceTime(500)
.subscribe(
res => {
if (this.nameSub) {
this.nameSub.unsubscribe();
this.nameSub = null;
}
this.nameSub = this.http.get(apiURL + 'exist/?name=' + res).subscribe(
exists => {
this.testForm.controls['name'].setErrors({nameExists: true});
this.nameSub.unsubscribe();
this.nameSub = null;
},
error => {
if (res) {
this.testForm.controls['name'].setErrors(null);
}
this.nameSub.unsubscribe();
this.nameSub = null;
}
);
},
error => {}
);

Related

React hooks one checkbox checked to uncheck other checkbox

Hi I have a parent component which contains a bunch child components which are checkboxes.
parent component is something like this inside:
const [items, setItems] = useState([
{
id:1,
selected: false,
},
{
id:2,
selected: false,
}
]);
const changeSelected = (id) =>
{
items.forEach((item)=>
{
if (item.id === id)
{
item.selected = !item.selected;
}
else{
item.selected = false;
}
})
}
return(
<div>
{items.map((item)=>{
<Child item={item} changeSelected={changeSelected}/>
})}
</div>
)
and in the child component, it has something like this inside:
return(
<div>
<input type="checkbox" checked={props.item.selected} onChange={()=>{props.changeSelected(props.item.id)}} />
</div>
)
I know partially this isnt working is because useState is async but I dont know what to do to make it work, or if I should try a different approach? Thank you
You can refactor your function to this:
const changeSelected = (id) => {
setItems(prev => ([...prev, {id, selected: !prev.filter(x => x.id === id)[0].selected}]))
}
Obviously I forgot to setItem according to the two answers I received. But I think the right way to do it is to make a deep copy of my existing items, and setItems again. For my future reference, here is what I have now working (an example):
let newItems = [...items];
newItems.forEach((item)=>
{
if (item.id === id)
{
item.selected = !item.selected;
}
else{
item.selected = false;
}
})
setProducts(newItems);
You are not updating the state anywhere. You should make a copy of the array and then change the object you want to:
const changeSelected = (id) =>
{ let newItems = [];
items.forEach((item)=>
{
if (item.id === id)
{
newItems.push({ id : item.id , selected : item.selected });
}
else{
newItems.push({ id : item.id , selected : false });
}
})
setItems(newItems);
}
Call setItems to set it.
Note:
Not related to the question but you should use unique keys when iterating over list.
{items.map((item)=>{
<Child key={item.id} item={item} changeSelected={changeSelected}/>
})}

.pipe(takeUntil) is listening when it is not supposed to

We are using .pipe(takeUntil) in the logincomponent.ts. What I need is, it should get destroyed after successful log in and the user is on the landing page. However, the below snippet is being called even when the user is trying to do other activity and hitting submit on the landing page should load different page but the result of submit button is being overridden and taken back to the landing page.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).subscribe(
(result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
// check to see if we're authed (but don't keep listening)
this.auth.authed
.pipe(takeUntilComponentDestroyed(this))
.subscribe((payload: IJwtPayload) => {
if (payload) {
this.auth.accountO
.pipe(takeUntilComponentDestroyed(this))
.subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
}
}
}
}
);
ngOnDestroy() {}
Custom Code:
export function takeUntilComponentDestroyed(component: OnDestroy) {
const componentDestroyed = (comp: OnDestroy) => {
const oldNgOnDestroy = comp.ngOnDestroy;
const destroyed$ = new ReplaySubject<void>(1);
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
destroyed$.next(undefined);
destroyed$.complete();
};
return destroyed$;
};
return pipe(
takeUntil(componentDestroyed(component))
);
}
Please let me know what I am doing wrong.
Versions:
rxjs: 6.5.5
Angular:10.0.8
Thanks
I've done a first pass at creating a stream that doesn't nest subscriptions and continues to have the same semantics. The major difference is that I can move takeUntilComponentDestroyed to the end of the stream and lets the unsubscibes filter backup the chain. (It's a bit cleaner and you don't run the same code twice every time through)
It's a matter of taste, but flattening operators are a bit easier to follow for many.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).pipe(
tap((result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
}),
mergeMap((result: any) => this.auth.authed),
filter((payload: IJwtPayload) => payload != null),
mergeMap((payload: IJwtPayload) => this.auth.accountO),
takeUntilComponentDestroyed(this)
).subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
});
This function doesn't create another inner stream (destroyed$). This way is a bit more back to the basics so it should be easier to debug if you're not getting the result you want.
export function takeUntilComponentDestroyed<T>(comp: OnDestroy): MonoTypeOperatorFunction<T> {
return input$ => new Observable(observer => {
const sub = input$.subscribe({
next: val => observer.next(val),
complete: () => observer.complete(),
error: err => observer.error(err)
});
const oldNgOnDestroy = comp.ngOnDestroy;
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
sub.unsubscribe();
observer.complete();
};
return { unsubscribe: () => sub.unsubscribe() };
});
}

How to delete data using Vue js?

I'm trying delete data but I'm getting this error:
this.jobPosts.filter is not a function
PostJobIndex.vue file:
deleteJobPost: async function(jobPost) {
if (!window.confirm('Are you sure you want to delete this Job Post?')) {
return;
}
try {
await employerService.deleteJobPost(jobPost.id);
this.jobPosts = this.jobPosts.filter(obj => {
return obj.id != jobPost.id;
});
console.log(this.jobPosts);
this.$toast.success("Job Post deleted Successfully!");
} catch (error) {
console.log(error);
this.$toast.error(error.response.data.message);
}
},
I had this same issue with my Update method and I beleive it was because I was trying to map through an object or something instead of an array. In the end I used Object.keys(this.jobPosts).map for my update method and it worked:
Object.keys(this.jobPosts).map(jobPost => {
if (jobPost.id == response.data.id) {
for (let key in response.data) {
jobPost[key] = response.data[key];
}
}
});
But when I do this for Update it doesn't work:
this.jobPosts = Object.keys(this.jobPosts).filter(obj => {
return obj.id != jobPost.id;
});
UPDATED
Here is the code for loading the job posts:
loadJobPosts: async function() {
try {
const response = await employerService.loadJobPosts();
this.jobPosts = response.data;
console.log(this.jobPosts);
} catch (error) {
this.$toast.error('Some error occurred, please refresh!');
}
},
Im using Vuex for state management and I'm using services, that simply contain the axios http requests. That's where this line comes from employerService.loadJobPosts() loadJobPosts() is a function inside my employerService.js file.
I'm also using Laravel for my back end. Here is my JobPostsController.php file:
public function index()
{
$jobPosts = JobPost::all()->where('user_id', Auth::user()->id);
return response()->json($jobPosts, 200);
}
From what I've understood from your code,
this should work for removing jobPost from jobPosts
this.jobPosts = this.jobPosts.filter(obj => {
return obj.id != jobPost.id;
});
I don't know what you're expecting this to do, but it won't do anything useful and will either error or return false for everything.
this.jobPosts = Object.keys(this.jobPosts).filter(obj => {
return obj.id != jobPost.id;
});
filter exists on array types, so I would check where it's getting set and make sure it's an array.
I've included a small snippet in case it's any help.
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: "#app",
data: () => {
return {
jobPosts: [],
deleteJobId: 1
};
},
methods: {
getJobPosts() {
this.jobPosts = [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}];
},
deleteJob() {
if (!this.deleteJobId)
return;
this.jobPosts = this.jobPosts.filter(x => x.id !== this.deleteJobId);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button type="button" #click="getJobPosts">Get Jobs</button>
<div>
<button type="button" #click="deleteJob">Delete Job #</button>
<input type="number" v-model.number="deleteJobId" />
</div>
<ul>
<li v-for="jobPost in jobPosts">
Job Post #{{jobPost.id}}
</li>
</ul>
</div>
You have already answered your own question:
in my data() object, I have this jobPosts: [], but in the console it says Object
As for your second question:
I don't know how to return the data as an array
There are similiar topics here on SO.
I am not familiar with Laravel but assuming you have an eloquent model with JobPost in your index-function according to the docs you should use the .toArray-method:
$jobPosts = JobPost::all()->where('user_id', Auth::user()->id).toArray();
When working with plain collections the values method should do the trick of returning an array instead of an object:
$collection = collect([
10 => ['product' => 'Desk', 'price' => 200],
11 => ['product' => 'Desk', 'price' => 200]
]);
$values = $collection->values();
$values->all();
UPDATE
I just realized that your result is just a stringified JSON object that needs to be converted into an array. Just parse it before processing (take out the JSON.parse(...) if you are already taking care of it in your service), return the object properties as an array and you are good to go:)
this.jobPosts = Object.values(JSON.parse(this.jobPosts)).filter(obj => {
return obj.id != jobPost.id;
});

State updates but Component doesn't re-render

I'm creating a simple react-redux chat application. I managed to display some dummy data from my redux state in my Message component. I succeed to push a new 'message' to the redux state from my Submit component. But the new item doesn´t render in the Message component.
So I tried to console log the previous state and the new state from the messageReducer and it seems to work. I get the state array with all the dummy data + the new pushed object.
Here is the Github repo if needed: https://github.com/MichalK98/Chat.V.2
// Message Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
class Message extends Component {
render() {
return (
<ul id="chatroom">
{this.props.messages.map((msg) => (
<li className={(msg.username == 'You' ? "chat-me" : "")} key={msg.id}>
<p>{msg.message}</p>
<small>{msg.username}</small>
</li>
)).reverse()}
</ul>
)
}
}
const mapStateToProps = (state) => {
return {
messages: state.message.messages
}
}
export default connect(mapStateToProps)(Message);
// Submit Component
...
import { connect } from 'react-redux';
...
class Submit extends Component {
state = {
message: []
}
clear = async () => {
await this.setState({
message: ''
});
}
handleChange = async (e) => {
await this.setState({
message: e.target.value
});
}
handleSubmit = (e) => {
e.preventDefault();
this.props.writeMessage(this.state.message);
this.clear();
}
render() {
return (
<div className="chat-footer">
<form onSubmit={this.handleSubmit} autoComplete="off">
<input onChange={this.handleChange} value={this.state.message} type="text" placeholder="Skriv något..."/>
<button id="btnSend"><SendSvg/></button>
</form>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
messages: state.messages
}
}
const mapDispatchToProps = (dispatch) => {
return {
writeMessage: (message) => { dispatch({type: 'WRITE_MESSAGE', messages: {id: Math.random(), username: 'You', message: message}})}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Submit);
// messageReducer
const initState = {
messages: [
{id: 1, username: 'You', message: 'Hi, data from reducer!'},
{id: 2, username: 'Mattias', message: 'Wow..'},
{id: 3, username: 'Alien', message: 'Awesome!'}
]
}
const messageReducer = (state = initState, action) => {
if (action.type === 'WRITE_MESSAGE') {
state.messages.push(action.messages);
console.log('Action ',action.messages);
console.log('State ',state.messages);
}
return state;
}
export default messageReducer;
I expect that the new data will render in my Message component when I add a new object to the state array in messageReducer.
first of all in your Message Component you should Change mapStateToProps :
const mapStateToProps = (state) => {
return {
messages : state.messages
}
}
And then in your message reducer you should change reducer. this is better way for reducer. you shouldn't directly change state :
const messageReducer = (state = initState, action) => {
switch (action.type) {
case "WRITE_MESSAGE":
return { ...state, messages: [...state.messages, { ...action.messages }] }
default:
return state;
}
}
if you need any help i can help you to complete your project :-)

React Search Filter of Objects not filtering

I'm trying to create a search filter that will filter through facility names that lives in an array of objects.If I hard code an array into the state the filter works, but I need it to drab the info from props. The filtered list is being generated and showing all of the names on the screen but when I type it the textbox to filter nothing happens. What have I overlooked?
class FacilitySearch extends React.Component {
constructor(props) {
super(props);
this.state = {
search: ""
};
}
componentDidMount() {
this.props.dispatch(actions.getFacilitiesList());
}
//The subsr limits the # of characters a user can enter into the seach box
updateSearch = event => {
this.setState({ search: event.target.value.substr(0, 10) });
};
render() {
if (!this.props.facilityList) {
return <div>Loading...</div>
}
let filteredList = this.props.facilityList;
filteredList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
return (
<div>
<input
type="text"
value={this.state.search}
onChange={this.updateSearch.bind(this)}
placeholder="Enter Text Here..."
/>
<ul>
{filteredList.map(facility => {
return <li key={facility.generalIdPk}>{facility.facilityName}</li>;
})}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
facilityList: state.facilityList.facilityList
});
export default connect(mapStateToProps)(FacilitySearch)
The problem is that you are not storing the return value of filter in any variable.
You should do something like:
let filteredList = this.props.facilityList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
From MDN:
The filter() method creates a new array with all elements that pass the test implemented by the provided function.

Resources