I'm missing something when passing data from a component to another component. I use #Input to pass the data, that i get from an http.get request. The thing is, i get an error while trying to access an attribute from the passed input while the request hasn't been resolved.
//news.ts
import {Component} from 'angular2/core';
import {Http, HTTP_PROVIDERS} from 'angular2/http';
import {Pagination} from './pagination';
#Component({
selector: 'news',
templateUrl: 'app/news.html',
viewProviders: [HTTP_PROVIDERS],
directives: [Pagination]
})
export class News {
news = [];
pagination: Pagination;
constructor(http: Http) {
http.get('http://test.com/data')
.map(res => res.json())
.subscribe(news => this.news = news);
}
}
//pagination.ts
import {Component, Input} from 'angular2/core';
#Component({
selector: 'pagination',
templateUrl: 'app/pagination.html'
})
export class Pagination {
// page: int = 1;
#Input() config;
constructor() {
// this.page = this.config.number;
}
}
//Pagination.html
Page {{config.total}}
config.total generates an error on load. But doing {{config}} seems to work though.
Any ideas ?
Thanks
There are two solutions for this:
You can use Elvis operator in your pagination.html
Page {{config?.total}}
This is from Angular documentation:
The Elvis operator (?) means that the employer field is optional and if undefined, the rest of the expression should be ignored.
Second solution would be to use Async Pipe:
https://angular.io/docs/ts/latest/api/common/AsyncPipe-class.html
In this case you would need to rewrite your code.
Variable decorated with #Input() is not available in the constructor. You have to wait until Angular resolves the binding and access it later in component's lifecycle:
ngOnInit() {
this.page = this.config.number;
}
#Vlado Tesanovic I just tried the second solution because i might need to handle the data in the constructor.
What i did :
//news.ts
constructor(http: Http) {
// http.get('http://lechiffonbleu.com:1337/news/test')
// .map(res => res.json())
// .subscribe(news => this.news = news);
this.news = new Promise((resolve, reject) => {
resolve = http.get('http://lechiffonbleu.com:1337/news/test')
.map(res => res.json())
.subscribe(news => this.news = news);
console.log(this.news);
console.log(resolve);
});
}
Well i can't figure out how to resolve the promise correctly so it doesn't throw an error in the #input in pagination.ts
Related
I am trying to write a test with the new cypress 6 interceptor method (Cypress API Intercept). For the test I am writing I need to change the reponse of one endpoint after some action was performed.
Expectation:
I am calling cy.intercept again with another fixture and expect it to change all upcomming calls to reponse with this new fixture.
Actual Behaviour:
Cypress still response with the first fixture set for the call.
Test Data:
In a test project I have recreated the problem:
test.spec.js
describe('testing cypress', () => {
it("multiple responses", () => {
cy.intercept('http://localhost:4200/testcall', { fixture: 'example.json' });
// when visiting the page it makes one request to http://localhost:4200/testcall
cy.visit('http://localhost:4200');
cy.get('.output').should('contain.text', '111');
// now before the button is clicked and the call is made again
// cypress should change the response to the other fixture
cy.intercept('http://localhost:4200/testcall', { fixture: 'example2.json' });
cy.get('.button').click();
cy.get('.output').should('contain.text', '222');
});
});
example.json
{
"text": "111"
}
example2.json
{
"text": "222"
}
app.component.ts
import { HttpClient } from '#angular/common/http';
import { AfterViewInit, Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
public text: string;
public constructor(private httpClient: HttpClient) { }
public ngAfterViewInit(): void {
this.loadData();
}
public loadData(): void {
const loadDataSubscription = this.httpClient.get<any>('http://localhost:4200/testcall').subscribe(response => {
this.text = response.body;
loadDataSubscription.unsubscribe();
});
}
}
app.component.html
<button class="button" (click)="loadData()">click</button>
<p class="output" [innerHTML]="text"></p>
Slightly clumsy, but you can use one cy.intercept() with a Function routeHandler, and count the calls.
Something like,
let interceptCount = 0;
cy.intercept('http://localhost:4200/testcall', (req) => {
req.reply(res => {
if (interceptCount === 0 ) {
interceptCount += 1;
res.send({ fixture: 'example.json' })
} else {
res.send({ fixture: 'example2.json' })
}
});
});
Otherwise, everything looks good in your code so I guess over-riding an intercept is not a feature at this time.
As of Cypress v7.0.0 released 04/05/2021, cy.intercept() allows over-riding.
We introduced several breaking changes to cy.intercept().
Request handlers supplied to cy.intercept() are now matched starting with the most recently defined request interceptor. This allows users to override request handlers by calling cy.intercept() again.
So your example code above now works
cy.intercept('http://localhost:4200/testcall', { fixture: 'example.json' });
// when visiting the page it makes one request to http://localhost:4200/testcall
cy.visit('http://localhost:4200');
cy.get('.output').should('contain.text', '111');
// now cypress should change the response to the other fixture
cy.intercept('http://localhost:4200/testcall', { fixture: 'example2.json' });
cy.get('.button').click();
cy.get('.output').should('contain.text', '222');
Cypress command cy.intercept has the
times parameter that you can use to create intercepts that only are used N times. In your case it would be
cy.intercept('http://localhost:4200/testcall', {
fixture: 'example.json',
times: 1
});
...
cy.intercept('http://localhost:4200/testcall', {
fixture: 'example2.json',
times: 1
});
See the cy.intercept example in the Cypress recipes repo https://github.com/cypress-io/cypress-example-recipes#network-stubbing-and-spying
const requestsCache = {};
export function reIntercept(type: 'GET' | 'POST' | 'PUT' | 'DELETE', url, options: StaticResponse) {
requestsCache[type + url] = options;
cy.intercept(type, url, req => req.reply(res => {
console.log(url, ' => ', requestsCache[type + url].fixture);
return res.send(requestsCache[type + url]);
}));
}
Make sure to clean requestsCache when needed.
how would i go about attaching axios / axios interceptor globally to nuxt (so its available everywhere), same how i18n is attached ?
The idea is that i would like to have a global axios interceptor that every single request goes through that interceptor.
Thanks
you can create a plugin called axios (/plugins/axios.js)
import Vue from 'vue';
import axios from 'axios';
axios.interceptors.request.use((config) => {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
Vue.use(axios);
then define this in nuxt.config.js
module.exports = {
//....
plugins: [
'~/plugins/axios',
],
//....
};
thats all, your interceptor is now working globally
It's hidden in the documentation - https://nuxtjs.org/docs/2.x/directory-structure/plugins
See number 3 of the first photo:
// plugins/axios.js
export default function ({ $axios, redirect }) {
$axios.onError(error => {
if (error.response.status == 404) {
redirect('/sorry')
}
})
}
then define this in nuxt.config.js
module.exports = {
//....
plugins: [
'~/plugins/axios',
],
//....
};
Maybe will be helpful for someone.
It just sets the lang parameter for every request.
Сreate a plugin called axios (/plugins/axios.js). Put it there:
export default function ({ $axios, app, redirect }) {
$axios.onRequest(config => {
config.params = config.params || {}; // get existing parameters
config.params['lang'] = app.i18n.locale;
})
$axios.onError(error => {
const code = parseInt(error.response && error.response.status)
if (code === 400) {
redirect('/400')
}
})
}
Add in nuxt.config.js:
module.exports = {
plugins: [
'~/plugins/axios'
]
};
Create a new module, call it request.js for example.
import axios from 'axios'
const instance = axios.create({
baseURL: 'http://example.org' // if you have one
})
// Put all interceptors on this instance
instance.interceptors.response.use(r => r)
export default instance
Then simply import that instance whenever you need it and use it like it was a normal axios instance:
import request from './request'
await request.get('/endpoint')
// or use promises
request.get('/endpoint').then(data => data)
If you really need it globally you can use the following code in your entry point of the application:
import request from './request'
global.request = request
// use it:
await request.get('example.org')
Or you can add it to the vue protype
Vue.prototype.$request = request
// in your component:
this.$request.get()
I'd advice against it though.
I have a question regarding the TypeAhead as I didn't want to pollute the git space backlog.
I setup the typeahead to work with my own observable based on the async demo (I'm pulling the google prediction data) and the typehead kind of works, but has the refresh (or change detection) issue where I'm typing the correct address but the highlighted results are always one or two letters 'behind' in terms of highlighting, or the results are missing as the search might have been narrowed down. The component does update if I for example press the key left or right, which tells me there must be some detection issue.
If there any way I could force it do detect changes? I've tried to run the change detector right after the asyncaction but that didn't help. Thanks heaps
Here is the stackblitz code
https://stackblitz.com/edit/angular-ufgm4x
To see what I struggle to understand where is the delay, try to follow these steps:
quickly type in e.g. '30 Manni'
wait for the response, wait a little, let's say 3 sec
then press 'k' and wait and don't interact with the app...wait and then only after couple of seconds the component updates (match highlight). Or press 'k', wait a little and interact with the app and you will see the highlight kicks-in.
It appears that this is not the google place lookup response time as they are quite good. There must be something else.
This odd behavior is especially noticeable with the search delay
[typeaheadWaitMs]="1000"
export class TypeaheadComponent {
asyncSelected: string;
typeaheadLoading: boolean;
typeaheadNoResults: boolean;
dataSource: Observable<any>;
constructor(private geocoder: GeocodeService,
private chd: ChangeDetectorRef,
private zone: NgZone) {
this.dataSource = Observable.create((observer: any) => {
// Runs on every search
observer.next(this.asyncSelected);
}).mergeMap((token: string) => this.geocoder.getSuggestions(token)).do(() => {
setTimeout(() => {
this.chd.detectChanges(); // --> Doesn't do anything
}, 200);
});
}
changeTypeaheadLoading(e: boolean): void {
this.typeaheadLoading = e;
}
typeaheadOnSelect(e: TypeaheadMatch): void {
console.log('Selected value: ', e.value);
}
}
public getSuggestions(keyword: string): Observable<object> {
if (typeof google === 'undefined') {
return new Observable<object>();
}
const autocompleter = new google.maps.places.AutocompleteService();
return new Observable<object>((observer) => {
// Prepare the callback for the autocomplete
const onPredictionsReady = (predictions: any[]) => {
observer.next(predictions || []);
observer.complete();
};
// do the search
autocompleter.getPlacePredictions({ input: keyword }, onPredictionsReady);
});
}
I had the same problem, here's how I solved it:
My service:
import {Injectable} from '#angular/core';
import {MapsAPILoader} from '#agm/core';
import {Observable} from 'rxjs/Rx';
import {} from 'googlemaps';
#Injectable()
export class GooglePlacesService {
googleAutocompleteService;
constructor(private mapsAPILoader: MapsAPILoader) {
this.mapsAPILoader.load().then(() => {
this.googleAutocompleteService = new google.maps.places.AutocompleteService();
});
}
getPredictions(inputText: string) {
const callback = this.googleAutocompleteService.getPlacePredictions.bind(this.googleAutocompleteService);
const observable = Observable.bindCallback(callback, (predictions, status) => {
if (status !== google.maps.places.PlacesServiceStatus.OK) {
return [];
} else {
return predictions;
}
});
return observable({
input: inputText
});
}
}
My component (ts):
import {Component, NgZone, OnInit} from '#angular/core';
import {} from 'googlemaps';
import {Observable} from 'rxjs/Observable';
import AutocompletePrediction = google.maps.places.AutocompletePrediction;
import {GooglePlacesService} from '../../api/google-places.service';
#Component({
selector: 'search-location-input',
templateUrl: './search-location-input.component.html',
styleUrls: ['./search-location-input.component.css']
})
export class SearchLocationInputComponent implements OnInit {
inputText = '';
predictions: Observable<AutocompletePrediction[]>;
constructor(private googlePlacesService: GooglePlacesService,
private ngZone: NgZone) {
}
ngOnInit() {
this.predictions = Observable.create((observer: any) => {
this.googlePlacesService.getPredictions(this.inputText)
.subscribe((result: any) => {
this.ngZone.run(() => observer.next(result));
});
});
}
}
My component (html):
<input [(ngModel)]="inputText"
[typeahead]="predictions"
typeaheadOptionField="description"
[typeaheadWaitMs]="200"
type="text">
I'm really struggling trying to test a request in VueJS using Mocha/Chai-Sinon, with Axios as the request library and having tried a mixture of Moxios and axios-mock-adaptor. The below examples are with the latter.
What I'm trying to do is make a request when the component is created, which is simple enough.
But the tests either complain about the results variable being undefined or an async timout.
Am I doing it right by assigning the variable of the getData() function? Or should Ireturn` the values? Any help would be appreciated.
Component
// Third-party imports
import axios from 'axios'
// Component imports
import VideoCard from './components/VideoCard'
export default {
name: 'app',
components: {
VideoCard
},
data () {
return {
API: '/static/data.json',
results: null
}
},
created () {
this.getData()
},
methods: {
getData: function () {
// I've even tried return instead of assigning to a variable
this.results = axios.get(this.API)
.then(function (response) {
console.log('then()')
return response.data.data
})
.catch(function (error) {
console.log(error)
return error
})
}
}
}
Test
import Vue from 'vue'
import App from 'src/App'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
let mock = new MockAdapter(axios)
describe('try and load some data from somewhere', () => {
it('should update the results variable with results', (done) => {
console.log('test top')
mock.onGet('/static/data.json').reply(200, {
data: {
data: [
{ id: 1, name: 'Mexican keyboard cat' },
{ id: 2, name: 'Will it blend?' }
]
}
})
const VM = new Vue(App).$mount
setTimeout(() => {
expect(VM.results).to.be.null
done()
}, 1000)
})
})
I am not sure about moxios mock adaptor, but I had a similar struggle. I ended up using axios, and moxios, with the vue-webpack template. My goal was to fake retreiving some blog posts, and assert they were assigned to a this.posts variable.
Your getData() method should return the axios promise like you said you tried - that way, we have some way to tell the test method the promise finished. Otherwise it will just keep going.
Then inside the success callback of getData(), you can assign your data. So it will look like
return axios.get('url').then((response) {
this.results = response
})
Now in your test something like
it('returns the api call', (done) => {
const vm = Vue.extend(VideoCard)
const videoCard = new vm()
videoCard.getData().then(() => {
// expect, assert, whatever
}).then(done, done)
)}
note the use of done(). That is just a guide, you will have to modify it depending on what you are doing exactly. Let me know if you need some more details. I recommend using moxios to mock axios calls.
Here is a good article about testing api calls that helped me.
https://wietse.loves.engineering/testing-promises-with-mocha-90df8b7d2e35#.yzcfju3qv
So massive kudos to xenetics post above, who helped in pointing me in the right direction.
In short, I was trying to access the data incorrectly, when I should have been using the $data property
I also dropped axios-mock-adaptor and went back to using moxios.
I did indeed have to return the promise in my component, like so;
getData: function () {
let self = this
return axios.get(this.API)
.then(function (response) {
self.results = response.data.data
})
.catch(function (error) {
self.results = error
})
}
(Using let self = this got around the axios scope "problem")
Then to test this, all I had to do was stub the request (after doing the moxios.install() and moxios.uninstall for the beforeEach() and afterEach() respectively.
it('should make the request and update the results variable', (done) => {
moxios.stubRequest('./static/data.json', {
status: 200,
responseText: {
data: [
{ id: 1, name: 'Mexican keyboard cat' },
{ id: 2, name: 'Will it blend?' }
]
}
})
const VM = new Vue(App)
expect(VM.$data.results).to.be.null
VM.getData().then(() => {
expect(VM.$data.results).to.be.an('array')
expect(VM.$data.results).to.have.length(2)
}).then(done, done)
})
I want to display a loader for every fetch done by my app, preventing the user from submiting the same request multiple times. For this I changed the HttpClient configuration to intercept the requests and responses during configuration in main.ts
let httpClient = new HttpClient();
httpClient.configure(config => {
config.withInterceptor({
request(request) {
// displayLoader();
return request;
},
response(response) {
// hideLoader();
return response;
},
});
});
If it was plain js I would do something like document.body.appendChild... but Aurelia doens't let me do that.
Is there a way to display a custom view dynamically, without changing the route or having to insert a new loading view on every component that is doing a request?
Also if you have a better approach for this problem I'm open to suggestions.
Here's an option that might work for you. In your main.js, have something that looks like this:
import {HttpClient} from 'aurelia-fetch-client';
import {EventAggregator} from 'aurelia-event-aggregator';
export function configure(aurelia) {
const container = aurelia.container;
const httpClient = container.get(HttpClient);
const ea = container.get(EventAggregator);
httpClient.configure(config => {
config.withInterceptor({
request(request) {
ea.publish('http-request', request);
return request;
},
response(response) {
ea.publish('http-response', response);
return response;
}
});
});
aurelia.use
.standardConfiguration()
.developmentLogging()
.singleton(HttpClient, httpClient);
aurelia.start().then(() => aurelia.setRoot());
}
Then, in app.js or whatever you are using for your app's root page. Have something like this:
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
#inject(EventAggregator)
export class App {
displayLoader = false;
constructor(ea) {
this.ea = ea;
ea.subscribe('http-request', () => this.displayLoader = true);
ea.subscribe('http-response', () => this.displayLoader = false );
}
...
Then simply use databinding w/if.bind="displayLoader" or show.bind="displayLoader" in app.html.