I'm using Angular and I have two components. The first one is a navbar that has a search bar, so I put this code to send the content to my second component (home) and do the search:
html navbar:
<input class="form-control border-0 rounded-pill shadow-sm search-input " type="search" value="search" id="searchinput" #searchinput (keyup.enter)="search(searchinput.value)">
<button class="btn btn-outline-secondary bg-white border-0 rounded-pill shadow-sm" type="button" (click)="search(searchinput.value)">
<i class="material-icons">search</i>
</button>
component navbar:
#Output() searchEvent = new EventEmitter<string>();
search(search : string) {
this.searchEvent.emit(search);
}
html home:
<app-navbar (searchEvent)="search($event)"></app-navbar>
component home:
search(search : string) {
...logic code to make the search...
}
And it works, but when I put <app-navbar (searchEvent)="search($event)"> in my HTML, my variables in the navbar stop working correctly. For example, I have a variable called "token", and when my user logs in, the token becomes true. I have a verification in my HTML to change the visual if the token is true or false, but it doesn't work anymore. It's weird, but if I remove the dependency <app-navbar (searchEvent)="search($event)"> from my HomeComponent, my HTML navbar can see the changes that happen again when the user logs in (token = true) or logs out (token = false).
html navbar:
<div *ngIf="!(token$ | async)" class="d-flex">
...buttons to log in and sing in...
</div>
<div *ngIf="(token$ | async)">
...buttons from user...
</div>
component navbar:
ngOnInit(): void {
if(sessionStorage.getItem("token") === null){
this.token$.next(false);
}else{
this.token$.next(true);
}
}
private requestAut = new AutRequestDto();
logIn(email:string, password:string){
this.requestAut .email = email;
this.requestAut .password = password;
this.userService.autenticarUsuario(this.requestAut).pipe(
tap(
(result: any) => {
sessionStorage.setItem("token", result.token);
this.token$.next(true);
// location.reload();
}),
catchError((error) => {
console.log(error.error.message);
return of(error);
})
).subscribe();
}
logout(){
sessionStorage.removeItem("token");
this.token$.next(false);
}
If I press F5, the changes will be applied, or if I use location.reload();, but I'd like to see the changes in real-time instead of using location.reload(). Do you have any idea what could be happening? There are no errors in the console, and the code seems to be executing normally in debug mode.
I already tried using this.cdr.detectChanges() and this.cdr.markForCheck() in the login() method, but nothing works. I also tried using token sync instead of async, but that didn't help either.
I'm expecting that when the token becomes true in the success callback function in my component, my HTML will automatically update and reflect the change. It's strange that the logout works this way, but for the login, I need to press F5 to see the changes.
try changing your home html to messageEvent:
<app-navbar (messageEvent)="search($event)"></app-navbar>
and then change your home.ts to:
search(search : any) {
...logic code to make the search...
}
Related
I have a problem with the structure of my Vue.js components, but I don't understand what it is.
This is my app.js:
require('./bootstrap');
window.Vue = require('vue');
Vue.component('search', require('./components/Search').default);
Vue.component('graph', require('./components/Graph').default);
Vue.component('account', require('./components/Account').default);
Vue.component('design-theme', require('./components/DesignTheme').default);
const app = new Vue({
el: '#app',
data: {
},
methods: {
},
mounted: function () {
},
computed: {
}
});
So I don't have any methods or anything here, it is all in the four individual components. Each component works fine on its own, but when there is more than one in a page, something is off. Consider the Search.vue component. It simply sends an axios request to the server on keyup and shows a list of results:
<template>
<div class="search basic-search">
<input type="text" v-model="search_string" v-on:keyup="search" class="form-control search" placeholder="Search" value=""/>
<div :class="['search-results', active === true ? 'active' : '']">
<div class="search-result" v-for="result in search_results" v-on:click="submit(result.id)">
{{ result.name }}
</div>
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
search_string : '',
search_results : [],
active : false
};
},
methods : {
search : function() {
const axios_data = {
_token : $('meta[name="csrf-token"]').attr('content'),
str : this.search_string
};
axios.post('/search', axios_data).then(response => {
if(response.data.success){
this.search_results = response.data.stocks;
this.active = true;
}
});
},
submit : function(stock_id) {
document.location = "/graphs/" + stock_id;
}
}
}
</script>
This works fine if the Graph.vue component is not included on the page. But, if it is, then search_str always remains empty, even though the search method is called on keyup.
There are no errors in the console - it's just that search_string remains empty when I type (as does the input field).
Perhaps I don't understand something on a conceptual level in Vue.js, but I can't figure out the relation here, or how to adapt the code to this situation.
This is the problematic part of the Graph component, if this is removed then the search works OK.
<vue-range-slider v-model="range" :min="0" :max="100" v-on:drag-end="updateRange"></vue-range-slider>
This is the component in question:
https://github.com/xwpongithub/vue-range-slider
Another interesting side effect (that I just noticed) is that, when this component is on the page, it is impossible to select text with the mouse. It seems like the component is somehow hijacking events, but I don't understand how.
As you identified correctly, the Vue Range Slider component is intercepting the events. There is an open merge request on their github page.
As suggested in the referenced issues, you should change this line in your package.json file:
"vue-range-component": "^1.0.3",
To this one:
"vue-range-component": "Anoesj/vue-range-slider#master",
However, since this is not the default branch of the plugin, you should frequently check the issue on github and switch back to the official branch as soon as the merge request passes.
I'm trying to follow an example integration test from here: https://guides.emberjs.com/release/testing/testing-components/ (Testing Actions)
My problem is that the Test output keeps refreshing automatically, perpetually, for some reason?
Test code:
test('Can handle submit action', async function (assert) {
/*
* THIS TEST HAS PROBLEMS
* THE PAGE CONSTANTLY REFRESHES FOR THIS TEST, NO IDEA WHY, NEED TO INVESTIGATE
*/
assert.expect(1);
// test double for the external action
this.set('externalAction', (actual) => {
const expected = {inputValue: 'test'};
assert.deepEqual(actual, expected, 'submitted value is passed to external action');
});
await render(hbs`{{user-form inputValue="test" saveAction=(action externalAction)}}`);
// click the button to submit the form
await click('#submitButton');
});
Component.js:
import Component from '#ember/component';
import {computed} from '#ember/object';
export default Component.extend({
inputValue: '',
submitText: 'Save',
inputIsValid: computed('inputValue', function () {
return this.inputValue.length > 3;
}),
actions: {
save(inputValue) {
if (this.inputIsValid) {
this.saveAction(inputValue); // pass action handling to route that uses component
}
}
}
});
component template:
<br>
<br>
<form onsubmit={{action "save" inputValue}}>
{{#unless inputIsValid}}
<div style="color: red" class='validationMessage'>
Hey it is not valid!
</div>
{{/unless}}
<label id="inputLabel">{{inputLabel}}</label>
{{input type="text" id="input" placeholder=inputPlaceholder value=inputValue class="form-control"}}
<br>
<button type="submit" id="submitButton" class="btn btn-primary">{{submitText}}</button>
</form>
{{outlet}}
I thought it might be because the form in the template keeps submitting, but that can't be the case since it should only click submit once. Any help much appreciated!
Following #Lux's suggestion written as comment; you need to do the following to prevent the form submission from reloading the page:
save(inputValue, event) {
event.preventDefault()
if (this.inputIsValid) {
this.saveAction(inputValue); // pass action handling to route that uses component
}
}
You receive the event as the last argument and call preventDefault tells the browser to not to handle the event as it would normally. See MDN for a better explanation.
Using Vuex I have a form that when the button is clicked (#click="loader(true)") sends to the loader mutation to change loading to true, which then sets a is-loading class with Bulma CSS to true ('is-loading' : $store.state.index.loading ).
I then receive errors from the server if the form is empty with errors.title, this works fine with the inputs but how do I then set the is-loading class to false if there are errors?
(the code snippet will not work if you run it)
export const state = () => ({
loading: false
});
export const mutations = {
loader(state, value) {
state.loading = value;
}
}
<form #submit.prevent="postThis">
<div class="field">
<div class="control">
<input class="input" :class="{ 'is-danger': errors.title }" type="text" id="title" placeholder="I have this idea to..." autofocus="" v-model="newTitle">
</div>
<p class="is-size-6 help is-danger" v-if="errors.title">
{{ errors.title[0] }}
</p>
</div>
<div class="field">
<div class="control">
<button #click="loader(true)" type="submit" :class="{'is-loading' : $store.state.index.loading }">
Post
</button>
</div>
</div>
</form>
<script>
import {mapMutations,} from 'vuex';
methods: {
...mapMutations({
loader: 'index/loader'
})
}
</script>
The question is about using ...mapMutations, but in case someone want to add business logic, mapAction and mapState would be recommended. I will explain how to make it work with mapAction and mapState since calling API might involve using business logic within your application. Otherwise, I would say, why do you even bother using VueX except for notifying other part of your application that you are loading ;). That being said, here's my answer.
Using the ...mapState you have what you would be searching for, the computed reactivity of the state. This would happen especially during the invoke of the action. The action would then be changing, or what we call commit in VueX, the state (See doc: https://vuex.vuejs.org/guide/state.html)
Let's take your code and change it into a module with a namespace and then use the module in your vue (This is what I would do if the application is big, otherwise the same can be achieved using the mutation or no VueX at all):
const LOADING_STATE = 'LOADING_STATE'
export default {
namespaced: true, // Read the doc about that
state: {
loaded: false
},
mutations: {
[LOADING_STATE]: function (state, isLoading) {
state.loading = isLoading
}
},
actions: {
setLoading ({ commit }, isLoading) {
commit(LOADING_STATE, isLoading)
}
}
}
For your vue file where you have your template and your actions. It would look like this:
<script>
import { mapAction, mapState } from 'vuex'
exports default {
computed: {
...mapState({
// Here you could even have the full computation for the CSS class.
loading: state => state.loadingModule.loading,
// Like this... or you could use the getters that VueX does (search in the documentation since it's out of the scope of your question)
loadingCss: state => { return state.loadingModule.loading ? 'is-loading' : '' }
})
},
methods: {
// Usage of a namespace to avoid other modules in your VueX to have the same action.
...mapActions(['loadingModule/setLoading']),
}
}
</script>
And regarding your html template, you will be able to call the method this['loadingModule/setLoading'](true) or false and then the property that you can react to will be "loading".
While using promises, during your post or get or any other HTTP rest call, don't forget the context. If you're using Axios, after registering it in your VueJs context, I would do
this.$http.get('/my/api')
.then(response => {
// ... some code and also set state to ok ...
})
.catch(e => {
// set state to not loading anymore and open an alert
})
Let's complete your code now considering you're doing your HTTP(S) call somewhere.
<form #submit.prevent="postThis">
<div class="field">
<div class="control">
<!-- Here I would then use a computed property for that class (error). I would even put the a template or a v-if on a div in order to show or not all those html elements. That's you're choice and I doubt this is your final code ;) -->
<input class="input" :class="{ 'is-danger': errors.title }" type="text" id="title" placeholder="I have this idea to..." autofocus="" v-model="newTitle">
</div>
<p class="is-size-6 help is-danger" v-if="errors.title">
{{ errors.title[0] }}
</p>
</div>
<div class="field">
<div class="control">
<button #click="['loadingModule/setLoading'](true)" type="submit" :class="{'is-loading' : loading }">
Post
</button>
</div>
</div>
</form>
First, there is no need to have locally only needed state (loading) in global state (Vuex). So, typical usage looks like this:
<template>
<form>
<div class="field">
<div class="control">
<input
class="input" :class="{ 'is-danger': errors.title }"
type="text"
id="title"
placeholder="I have this idea to..."
autofocus=""
v-model="newTitle"
>
</div>
<p class="is-size-6 help is-danger" v-if="errors.title">
{{ errors.title[0] }}
</p>
</div>
<div class="field">
<div class="control">
<button
#click="postForm"
:class="{'is-loading': isLoading }"
>
Post
</button>
</div>
</div>
</form>
</template>
<script>
export default {
...
data () {
return {
...
newTitle: '',
isLoading: false,
response: null
}
},
methods: {
async postForm () {
try {
this.isLoading = true // first, change state to true
const { data } = await axios.post('someurl', { title: this.newTitle }) // then wait for some async call
this.response = data // save the response
} catch(err) {
// if error, process it here
} finally {
this.isLoading = false // but always change state back to false
}
}
}
}
</script>
if you using vuex like this. I guess you misunderstood vuex. Because you can use for local variable and you can check api result. if you want seperate api request, you have to mapAction in methods and mapGetters in Computed
I am building an application for the first time to store, update, view and delete Client profiles. I followed the Angular tour of heroes to build the basic app and then pieced together the mongodb and express portions from around the net.
I am getting this error in my browser console when i attempt to delete a client profile -
ERROR TypeError: Cannot read property '_id' of undefined
at ClientProfileComp.webpackJsonp.../../../../../src/app/components/clientProfile.component.ts.ClientProfileComp.delete
(clientProfile.component.ts:53)... (etc).
I have confirmed using postman that my express routing is working as intended. I am able to get all/create clients at /api/clients, as well as get, put and delete from /api/clients/:_id (where _id is the autogenerated id for each entry).
I believe the problem is in one of my component files, as the error only occurs when I attempt to delete or view specific client detail, which causes another type of error entirely (CastError). The problem likely began when I attempted to remove all mentions of clientProfile: ClientProfile[]; (or Hero in the case of the tutorial) as I am no longer importing the details from client.ts (hero.ts) since I am using a mongoose schema instead, and I do not believe I should be importing that schema into my front-end angular.
here is the delete section of clientProfile.service.ts:
delete(_id: number): Promise<void> {
const url = `${this.clientProfilesUrl}/${_id}`;
return this.http.delete(url, {headers: this.headers}).toPromise()
.then(() => null).catch(this.handleError);
}
and here is clientProfile.component.ts as requested (the most likely source of my problem being that i replaced all instances of clientProfile: ClientProfile; with clientProfile: any; without knowing what I was doing)
note the commented out import statement.
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
//import { ClientProfile } from '../old/clientProfile';
import { ClientProfileService } from '../services/clientProfile.service';
#Component({
selector: 'app-clientprofile',
templateUrl: '../views/clientProfile.component.html',
styleUrls: [ '../styles/clientprofile.component.css' ]
})
export class ClientProfileComp implements OnInit {
selectedClientProfile: any;
clientProfiles: any = [];
clientProfile: any;
constructor(
private clientProfileService: ClientProfileService,
private router: Router
) { }
gotoDetail(): void {
this.router.navigate(['/detail', this.selectedClientProfile._id]);
}
getClientProfiles(): void {
this.clientProfileService.getClientProfiles().then(clientProfiles => {
this.clientProfiles = clientProfiles;
});
}
ngOnInit(): void {
this.getClientProfiles();
}
onSelect(clientProfile: any): void {
this.selectedClientProfile = clientProfile;
}
add(name: string, address: string): void {
name = name.trim();
address = address.trim();
if (!name) { return; }
this.clientProfileService.create(name, address).then(clientProfile => {
this.clientProfiles.push(clientProfile);
this.selectedClientProfile = null;
this.getClientProfiles();
});
}
delete(clientProfile: any): void {
this.clientProfileService.delete(clientProfile._id).then(() => {
this.clientProfiles = this.clientProfiles.filter(h => h !==
clientProfile);
if (this.selectedClientProfile === clientProfile) { this.selectedClientProfile = null; }
});
}
}
I have been poring over this all day, and have read a lot of similar posts here too - but most of the solutions don't seem to apply to this case. If anyone could point me in the right direction, i'd be really grateful. if any more code is needed to explain what i'm trying to do i will gladly post it.
Based on the error message it seems that the error is here:
this.router.navigate(['/detail', this.selectedClientProfile._id])
It appears that you only set it in onSelect and there are several places in your code that you are setting this.selectedClientProfile to null. That would be the best place to look.
If you'd like to create a plunker that demonstrates your issue, we could look at it further.
As a side note, you are using promises instead of the now more common Observables. If you want to change over to using Observables, I have a complete example of CRUD (create, read, update, and delete) operations here: https://github.com/DeborahK/Angular2-ReactiveForms in the APM folder.
Found out one problem - my delete button in the html file was in a div that only appeared when a client was selected, instead of next to each client. this was a result of a half-finished measure i took to ensure users don't just click delete willy-nilly on each client.
Code before:
<h2>Client Profiles</h2>
<div class="add-client">
<label>Add new client</label>
<input placeholder="Client Name (required)" #clientProfileName />
<input placeholder="Client Address" #clientProfileAddress />
<button (click)="add(clientProfileName.value, clientProfileAddress.value);
clientProfileName.value=''; clientProfileAddress.value=''">
Add</button>
</div>
<ul class="clientProfiles">
<li *ngFor="let clientProfile of clientProfiles"
[class.selected]="clientProfile === selectedClientProfile"
(click)="onSelect(clientProfile)">
<span class="badge">{{clientProfile.idnumber}}</span>
<span>{{clientProfile.name}}</span>
</li>
</ul>
<div *ngIf="selectedClientProfile">
<h2>
{{selectedClientProfile.name | uppercase}} selected
</h2>
<button (click)="gotoDetail()">View Details</button>
<button class="delete"
(click)="delete(clientProfile);
$event.stopPropagation()">Delete</button>
//delete button will only appear when a client is selected
</div>
Code now:
<h2>Client Profiles</h2>
<div class="add-client">
<label>Add new client</label>
<input placeholder="Client Name (required)" #clientProfileName />
<input placeholder="Client Address" #clientProfileAddress />
<button (click)="add(clientProfileName.value, clientProfileAddress.value);
clientProfileName.value=''; clientProfileAddress.value=''">
Add</button>
</div>
<ul class="clientProfiles">
<li *ngFor="let clientProfile of clientProfiles"
[class.selected]="clientProfile === selectedClientProfile"
(click)="onSelect(clientProfile)">
<span class="badge">{{clientProfile.idnumber}}</span>
<span>{{clientProfile.name}}</span>
<button class="delete"
(click)="delete(clientProfile);
$event.stopPropagation()">Delete</button>
//delete button appears next to each client
</li>
</ul>
<div *ngIf="selectedClientProfile">
<h2>
{{selectedClientProfile.name | uppercase}} selected
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
I am not very experienced in Angular 4 so I am not sure how this problem works. I get the following error;
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
Here is my setup. I have a component "menus.component.ts" (MenusComponent) which loads other components using <router-outlet></router-outlet> The error happens at LocalService.placementListShow.value in the code below.
<div class="row">
...
</div>
<!-- end row -->
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-12 col-xl-3" *ngIf="LocalService.placementListShow.value">
<div class="card m-b-20" *ngIf="LocalService.AllPlacements.Loaded && !LocalService.AllPlacements.Loading">
...
<button type="button" class="list-group-item" [ngClass]="{'active': LocalService.AllPlacements.Active.value==placement.id }" (click)="LocalService.AllPlacements.Activate(placement.id)" *ngFor="let placement of LocalService.AllPlacements.Placements">{{placement.title}}</button>
...
</div>
<single-element-loader *ngIf="LocalService.AllPlacements.Loading"></single-element-loader>
</div><!-- end col-->
<div class="col-md-4 col-sm-4 col-xs-12 col-xl-3" *ngIf="LocalService.menuListShow.value">
...
<a [routerLink]="[menu.id]" class="list-group-item" [ngClass]="{'active': LocalService.PlacementMenus.Active.value==menu.id }" (click)="LocalService.PlacementMenus.Activate(menu.id)" *ngFor="let menu of LocalService.PlacementMenus.Menus">{{menu.title}}</a>
...
<single-element-loader *ngIf="LocalService.PlacementMenus.Loading"></single-element-loader>
</div><!-- end col-->
<div class="col-md-8 col-sm-8 col-xs-12 col-xl-9">
<router-outlet></router-outlet>
</div><!-- end col-->
</div>
The idea is that the component will load child components using Angular router. I want to control the visibility of certain widgets of the main component in the child components so I have setup a local service.
#Injectable()
export class LocalService {
menuListShow = new BehaviorSubject(false);
placementListShow = new BehaviorSubject(true);
Menu: Menu = new Menu();
AllPlacements: AllPlacements = new AllPlacements();
PlacementMenus: PlacementMenus = new PlacementMenus();
constructor(
private MenuService: MenuService,
private route: ActivatedRoute,
) {
}
changeMenuComponents(componentName: string): void {
alert ("Changing to: "+ componentName)
let menuState = {
'placementList': (that): void => {
that.menuListShow.next(false);
that.placementListShow.next(true);
},
'menuList': (that): void => {
that.placementListShow.next(false);
that.menuListShow.next(true);
}
};
menuState[componentName](this);
}
}
For example. I have a MenuComponent and EditLinkComponent which will be loaded in the MenusComponent. There are 2 widgets which I would like to show depending on what component is loaded in the main component. But using the service I get the error above.
The following is not so important but to give you more idea about what I am trying to do.
I would like to show a menu placement listing when the user is seeing the index of the MenusComponent and when I click on the menu placement it should show menus in that placement and when I click on the menu it should show links in the menu. When I click on edit link it should show the EditLinkComponent. This happens through Angular router for example; #cms/menus then #cms/menus/{menuid} then #cms/menus/{menuid}/links/{linkid}/edit
The problem is if I refresh at; #cms/menus/{menuid}/links/{linkid}/edit I have get the error;
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
And I think this has to do with attempting to load the placement object and menu object from the server.
Here's how to fix it (you need to manually trigger a change detection):
#Injectable()
export class LocalService {
menuListShow = new BehaviorSubject(false);
placementListShow = new BehaviorSubject(true);
Menu: Menu = new Menu();
AllPlacements: AllPlacements = new AllPlacements();
PlacementMenus: PlacementMenus = new PlacementMenus();
constructor(
private changeDetectorRef: ChangeDetectorRef,
private MenuService: MenuService,
private route: ActivatedRoute,
) {
}
changeMenuComponents(componentName: string): void {
alert ("Changing to: "+ componentName)
let menuState = {
'placementList': (that): void => {
that.menuListShow.next(false);
that.placementListShow.next(true);
},
'menuList': (that): void => {
that.placementListShow.next(false);
that.menuListShow.next(true);
}
};
menuState[componentName](this);
this.changeDetectorRef.detectChanges();
}
}