I have a img src which just looks like this:
<img [src]="currentImg" />
Which traverses a JSON and grabs the image url.
I have a button underneath the image directly that refreshes the JSON feed.
<a (click)="refreshFeed(url)">Refresh feed</a>
refreshFeed(url) {
this.subscriptions.add(
this.jsonService.getJson().subscribe(() => {
this.currentImg = url.properties.img;
return this.currentImg;
})
)
}
However, while I can see it's fetching the new json, it doesn't seem to update the image url and change it's source at all, also, the image url will always be the same name in the json (img: "image.jpeg")
Any help is appreciated.
First, subscribe is a void function, there is no meaning for it to return a value.
Second, It seems like you're using a different Change Detection Strategy somewhere along the component tree.
Try doing the following, if it works then you definitely use the onPush strategy.
constructor(
...,
private cd: ChangeDetectorRef,
...
) {
...
}
refreshFeed(url: string) {
this.jsonService.getJson()
.pipe(take(1))
.subscribe(data => {
this.currentImg = data.properties.img;
this.cd.detectChanges();
})
}
I used the Rxjs "take" pipe instead of destroying subscriptions.
This is a better approach, try using it where you can.
I would not use any ngIfs. The image will jump every time the user generates a new image.
Hope this helps!
Try like this:
.html
<img [src]="currentImg" *ngIf="showImage"/>
.ts
showImage:boolean
refreshFeed(url) {
this.showImage = false;
this.subscriptions.add(
this.jsonService.getJson().subscribe(() => {
this.currentImg = url.properties.img;
this.showImage = true;
return this.currentImg;
})
)
}
Related
Right now i have the following lines of code:
private async Task SetImageUsingStreamingAsync()
{
var imageStream = await GetImageStreamAsync();
var dotnetImageStream = new DotNetStreamReference(imageStream);
await JSRuntime.InvokeVoidAsync("setImageUsingStreaming",
"image1", dotnetImageStream);
}
Above snippet gets a Stream, and repackages that stream into DotNetStreamReferance, and then passes that DotNetStreamReferance to a JS function:
async function setImageUsingStreaming(imageElementId, imageStream) {
const arrayBuffer = await imageStream.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
document.getElementById(imageElementId).src = url;
}
which populates an html element by ID. This is all fine, and works, but if i want to have a loadingscreen while i get the streams from the DB things get messy:
if(isloading)
{
show loadingscreen
}
else
{
foreach(string in listofstrings)
{
<img id="somename"></img>
}
}
the img element does not "show up", and causes an error in the js snippet if the bool isloading is not changed to false BEFORE populating the images on the site. Which kind of defeats the purpose with a loadingscreen.
Is there a smarter way of doing this? Preferably without using javascript, since i dont really know that language and cannot modify it to my needs.
The above code is a direct reference from .net documentation on blazor(not the loading part, but the stream to image part): https://learn.microsoft.com/en-us/aspnet/core/blazor/images?view=aspnetcore-6.0
if (isloading)
{
// show loadingscreen
}
else
{
// Populate a list with image elements...
List imageElements = new List();
foreach(string in listofstrings)
{
Element image = new Element("img");
image.Id = "somename";
imageElements.Add(image);
}
// ...and append the list to the DOM using RenderElementsAsync
// to ensure that it is rendered only when the list is fully
// populated.
await JSRuntime.InvokeAsync("setImageUsingStreaming", imageElements, dotnetImageStream);
}
The idea is to populate an in-memory list of elements, and then append the list to the DOM in one go, by rendering it asynchronously using RenderElementsAsync.
Note: In your original code, you should invoke the JavaScript function setImageUsingStreaming() asynchronously. That is, you should use InvokeAsync instead of InvokeVoidAsync.
Let me know if this helps...
I figured it based on Yogi's answer in the comment section of my post:
there is absolutely no need to use JS here, i dont know why the blazor docs prefer it that way.
a simple sample on how i did:
private async Task PopulateImageFromStream(Stream stream)
{
MemoryStream ms = new MemoryStream();
stream.CopyTo(ms);
byte[] byteArray = ms.ToArray();
var b64String = Convert.ToBase64String(byteArray);
string imageURL = "data:image/png;base64," + b64String;
}
}
add the imageURL to an array, or simply declare it globaly to get it in HTML:
<img src="#imageURL">
In a parent component I have a stream of Tour[] tours_filtered: Observable<Tour[]> which I assign in the subscribe function of an http request
this.api.getTours().subscribe(
result => {
this.tours_filtered = of(result.tours);
}
)
in the view I display the stream using the async pipe
<app-tour-box [tour]="tour" *ngFor="let tour of tours_filtered | async"></app-tour-box>
Up to here all works as expected. In a child component I have an input text which emits the value inserted by the user to filtering the array of Tour by title.
In the parent component I listen for the emitted values in a function, I switch to new stream of Tour[] filtered by that value using switchMap
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap(
(tours) => of( tours.filter((tour) => tour.name.toLowerCase().includes(term)) )
)
)
}
I thought that the async pipe was constantly listening to reflect the changes to the array to which it was applied and so I thought I didn't have to subscribe in the function above, but nothing change in the view when I type in the input to filtering the results.
The results are updating correctly if I assign the new stream to the original array in the subscribe function
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
).subscribe( val => { this.tours_filtered = of(val); })
}
Is this procedure correct? Could I avoid to subscribe because I already use the async pipe? There is a better way to reach my goal?
EDITED:
Maybe I found a solution, I have to reassing a new stream to the variable just like this
onSearchTitle(term: string) {
this.tours_filtered = of(this.city.tours).pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
);
}
and I don't need to subscribe again, the results in the view change according to the search term typed by the user. Is this the correct way?
I think in your situation the solution should work as follows:
onSearchTitle(term: string) {
this._searchTerm = term;
this.tours_filtered = of(
this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term))
)
}
Because in your example you don't change the observable which is used in ngFor. Thus it's not working.
However, I don't see the reason of using observables here unless this is the first step and you're going to fetch this data from server in future
UPDATE
The best solution for you would be to consider your input as an observable and watch for the changes:
// your.component.ts
export class AppComponent {
searchTerm$ = new BehaviorSubject<string>('');
results = this.search(this.searchTerm$);
search(terms: Observable<string>) {
return terms
.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => {
return of(this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term)))
}
)
)
}
}
// your.template.html
...
<input type="" (input)="searchTerm$.next($event.target.value)">
...
Additionally it would be great to add debounceTime and distinctUntilChanged for better user experience and less search requests.
See full example for the details. Also please, refer to this article for more detailed explanations
I am working on a react file-upload component. I got stuck with a rather trivial issue – I want for each file to show icon corresponding to a file extension. Icons are loaded via css as background images (using inline styles). The problem arises when I don't have an icon for given extension and thus want to show a fallback icon.
– I tried to use multiple css background-image declarations like this:
style={{
backgroundImage: `url(./icons/fallback.svg), url(./icons/${item.extension}.svg)`,
}}
or like this:
style={{
backgroundImage: `url(./icons/fallback.svg)`,
backgroundImage: `url(./icons/${item.extension}.svg)`,
}}
But it doesn't work, the fallback icon is not being used (or in one case I am not able to reproduce, both icon are shown, which is also undesired).
I tried to fetch the file to determine if it does exist, but the node server (i use create-react-app) is configured in a way that returns 200 or 304 even if the file isn't actually present.
I tried to use a solution which creates an image and uses onload and onerror events as beeng suggested in this question, which actually works fine – i am currently using slightly modified implementation of image-exists npm module – but I wasn't able to figure out how to refactor this function to simply return a boolean. Using console.log() and callbacks works fine; returning a boolean results in undefined. I suppose it is due to an asynchronous behaviour od Image methods, but I wasn't able to create a workaround – maybe using a Promise API?
My code:
exists = src => {
const checks = {};
return callback => {
if (src in checks) {
return callback(checks[src]);
}
let img = new Image();
img.onload = function() {
checks[src] = true;
callback(true);
};
img.onerror = function() {
checks[src] = false;
callback(false);
};
img.src = src;
};
};
Render method:
render() {
// So far so good, logs as expected, but not such useful
console.log(this.exists('./icons/jpg.svg')(bool => {
if(bool) {
console.log('yes')
} else {
console.log('no');
}
}));
// ...
}
If I try to return a boolean directly, it results in undefined:
render() {
console.log(this.exists('./icons/jpg.svg')(bool => bool));
// ...
}
You are right, the function does not return a boolean because this is the parameter of the callback of your exists function, which is called asynchronously. The solution is to render your icon asynchronously too, something like...
this.exists(img)(bool => {
if (bool) {
render(img)
} else {
render('fallback.svg');
}
}
O.K. I finally promisify the whole thing. I hooked the former exists function (now checkImage) to a promise chain(saw… massacre…) which is triggered by reading files to upload and results in setState and rerender:
The url checking function:
checkImage = (path, fallback) => {
return new Promise(resolve => {
const img = new Image();
img.src = path;
img.onload = () => resolve(path);
img.onerror = () => resolve(fallback);
});
};
Calling with Promise.all():
// items are array of objects which contains file contents, name, extension etc...
checkIcons = items =>
Promise.all(
items.map(item => {
const url = `./icons/${item.extension}.svg`;
return this.checkImage(url, this.state.fallbackIconUrl).then(result => {
return { ...item, icon: result };
});
})
);
Definitely not the slickiest one in town and it would possibly need some caching (or may not – it does seem the browser can handle this by itself), but works fine.
Every time I load the webpage, I'd have to click the logo in-order my data to fully populate the local array in my component. The data fetched is located in a local JSON file. Having to refresh the page every-single-time is fairly unprofessional/annoying.
Using Angular CLI 1.3.2
Here's where my problem lies:
#Injectable()
export class LinksService implements OnInit{
siteFile : IFile[];
constructor(private http: Http) {
this.getJSON().subscribe(data => this.siteFile = data, error =>
console.log(error));
}
public getJSON(): Observable<any> {
return this.http.get('./assets/docs/links.json')
.map((res:any) => res.json());
}
getAllIpageLinks() : IPageLink[]{
var selectedIPageLinks: IPageLink[] = new Array();
var selectedFileLinks : IFile[] = new Array();
selectedFileLinks = this.siteFile;
for (var i=0; i<selectedFileLinks.length; i++)
{
selectedIPageLinks =
selectedIPageLinks.concat(selectedFileLinks[i].files);
}
return selectedIPageLinks.sort(this.sortLinks);
}
Component:
constructor(private elRef: ElementRef, private linksService: LinksService) {
this._file = this.linksService.getAllIpageLinks();
}
Edit
The title has to be clicked in order for array of IFile[] to completely render. I've tried setting IFile to an empty array (IFile[] = []) The error goes away, however, it will render empty data.
The problem seems to be in the For loop, it can't recognize .length.
Problem :
The codes are correct but the approach is wrong. Subscribing to an Observable getJSON() is async task. Before any data is being returned by getJSON(), you already calls getAllIpageLinks() and therefore you get null value on very first run. I believe since you have injected the service as singleton in component, the data gets populated in subsequent call( on refresh by clicking logo).
Solution:
Apply the changes (that you are making in getAllIpageLinks ) by using map operator on observable.
return the instance of that observable in the component.
subscribe to that observable in the component(not in .service)
Welcome to StackOverflow. Please copy paste your codes in the question instead of giving screenshot of it. I would be able than to give you along the exact codes
Reference Codes :
I haven't tested the syntax but should be enough to guide you.
1. Refactor getAllIpageLinks() as below
public getAllIpageLinks(): Observable<any> {
return this.http.get('./assets/docs/links.json')
.map((res:any) => res.json());
.map(res => {
var selectedIPageLinks: IPageLink[] = new Array();
var selectedFileLinks : IFile[] = new Array();
selectedFileLinks = res;
for (var i=0; i<selectedFileLinks.length; i++)
{
selectedIPageLinks =
selectedIPageLinks.concat(selectedFileLinks[i].files);
}
return selectedIPageLinks.sort(this.sortLinks);
});
}
call above getAllIpageLinks() in your component
and subscribe to it there
Need help to load first the static tags and hit backend API simultaneously to get suggestion list as a part of autocomplete loadTags function as it works only for the first return of the function so I am either able to hit for backend and return the list or I am able to return the static list;
function loadTags(query) {
myLocalTags = _.merge(filteredBasicTags, filteredAdvTags);
apiService.getBackendTags(query).then(function(response) {
myLocalTags = _.values(_.merge(myLocalTags, response.data));
return myLocalTags;
});
//so either I am able to return the apiService Response or the myLocalTags Data for the autocomplete suggestions.
return myLocalTags;
}
You can do something like this:
$scope.autocompleteSuggestions = _.merge(filteredBasicTags, filteredAdvTags);
apiService.getBackendTags(query).then(function(response) {
myLocalTags = _.values(_.merge(myLocalTags, response.data));
$scope.autocompleteSuggestions = myLocalTags;
});