i have a gallery of images in the main view, and I want to be able to click each image to open a new child view showing details (eg text such as image title). my hardcoded version -- where images and text are stored in frontend -- works. but when I store the data in a MySQL database, i'm running into a problem -- eg unable to drill down into the details when an image is clicked.
the data seems to be fetched properly from backend to frontend because the images of the gallery are showing up. in my mind, the difference (between the hardcoded version and database-connected version) lies largely in react router v4's setup, perhaps a syntax issue that is preventing the paths from matching or preventing data from being passed to the child view:
ReactDOM.render(
..
<Route path="/articles/:PostId" render={(props) => (<Post articles={this.state.blurbs} {...props} />)} />
when it comes to errors, I get different messages depending on the browser:
one says
"TypeError: Unable to get property 'blurbs' of undefined or null reference;"
other says
"TypeError: this.state is undefined"
for brevity, my array state is initialized as such:
class App extends React.Component
..
this.state = {
blurbs: []
}
the value of the array state is updated as such:
componentDidMount()
..
let self = this;
..
return response.json();
...
.then(function(data) {
self.setState({blurbs: data});
})
in child component, data is rendered based on unique id in database as such:
render
..
return
..
{this.props.articles[this.props.match.params.PostId].Title}
so going back, what's wrong with the syntax in react router assuming everything above makes sense? tia
The issue is that the data coming from the API (eg: MySQL) is likely an array. However, you're treating it like a js object.
In your child component's render function, you need to use the findIndex method to find which element of the array that contains the value of your PostId URL parameter (docs). This is assume your table has an id element that is your primary key.
render() {
const idx = this.props.articles.findIndex(x => x.id === this.props.match.params.PostId);
const article = idx > -1 ? this.props.articles[idx] : {};
return (
...
{article.Title}
...
);
to bring "closure" to this thread, i'm sharing what worked for me. instead of passing all props from parent to child via react router, i ended up passing some props from parent to child through the image link.
the issue -- that props was not completely getting from parent to child -- was not immediately clear to me. i narrowed down my issue by tinkering around (eg substituting parts of the project with hardcode to identify the problematic parts of the code).
i found a thread where someone else was experiencing similar blockage in data transfer via react router. after several roadblocks, here are the changes that got me moving forward again.
in parent component, state reference was removed from react router to look as such:
ReactDOM.render(
..
<Route path="/articles/:PostId" render={(props) => (<Post {...props}/>)} />
in parent component, state reference was added to image link to use Link as a data transfer medium as such:
render() {
return (
<div>
{this.state.blurbs.map(image => (
<Link key={image.PostId}
to={{pathname: `/articles/${image.PostId}`,
state: { blurbs: this.state.blurbs }
}}>
<img src={image.src}/>
</Link>
))
}
</div>
in child component, this.props.location.state.blurbs was added as replacement to make the data (originally fetched in parent) accessible to child as such:
render
..
return
..
{this.props.location.state.blurbs[this.props.match.params.PostId].Title}
here's the link to the other thread:
How do i pass state through React_router?
Related
I was using OMDBapi to get the details of different movies. I successfully fetched the result and it returns a json object like this;
{"Title":"WWA: The Inception","Year":"2001","Rated":"N/A","Released":"26 Oct 2001","Runtime":"N/A","Genre":"Action, Sport","Director":"N/A","Writer":"Jeremy Borash","Actors":"Bret Hart, Jeff Jarrett, Brian James, David Heath","Plot":"N/A","Language":"English","Country":"Australia","Awards":"N/A","Poster":"https://m.media-amazon.com/images/M/MV5BNTEyNGJjMTMtZjZhZC00ODFkLWIyYzktN2JjMTcwMmY5MDJlXkEyXkFqcGdeQXVyNDkwMzY5NjQ#._V1_SX300.jpg","Ratings":[{"Source":"Internet Movie Database","Value":"6.0/10"}],"Metascore":"N/A","imdbRating":"6.0","imdbVotes":"22","imdbID":"tt0311992","Type":"movie","DVD":"N/A","BoxOffice":"N/A","Production":"N/A","Website":"N/A","Response":"True"}
Note that we get this type of object from the api if we want to get a particular movie details and that is what i was doing. Now to show the different details to a user, i started parsing this JSON object which works fine but when i try to get the value of the Value key present inside the Ratings key, it returns undefined.
I am working with react-native. After getting the data, i stored it inside the state, named it as details. Then to get it;
this.state.details.Title //if i wanted to get the Title and it works fine.
Then for Value inside Ratings;
this.state.details.Ratings[0].Value
But it returns undefined.
Also note that this works fine in pure Javascript as i parsed the dict in the browser console in the same way and it returned the correct value.
Here is more code;
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const response = await fetch(`http://www.omdbapi.com/?i=${this.props.navigation.getParam('i')}&apikey=******`) // where this.props.navigation.getParam('i') is the omdbid of the movie
const result = await response.json()
this.setState({details: result})
}
Here is error log;
undefined is not an object (evaluating 'this.state.details.Ratings[0]')
You're most likely trying to access state object before fetch has done it's job .... it's an async op ... so you should make sure your data is ready before rendering...
if (this.state.details) {
// start rendering...
}
More Explanation
your setState function should be executed right after fetch has finished its job, and since it's an async operation, it's going to take some time ...During that time, render function is executed with no state.details --> causing your issue ...
That's why you should check for state before rendering ... besides, the optional chaining trick Silversky Technology mentioned in his answer
If the value property you are accessing from the object might be not available for all the movies in the data you are getting from API response so it might cause you to error when accessing key from undefined objects.
To overcome the issue there is a way, you can try a fix as below:
this.state.details.Ratings[0]?.Value
The ? symbol lets the javascript not give an error when the value key not available in the object. it will make the accessing of property optional.
When storing objects in states it often causes problems as you are doing in line
this.setState({details: result})
Save result after strigifying it like
JSON.stringify(result)
this.setState({details: result})
Then when fetching form state, parse it back to object by
var result = JSON.parse(this.state.details)
Then you should be able to access it
You can access Ratings[0].Value by
this.state.details.Ratings && this.state.details.Ratings[0].Value
like,
<Text> {this.state.details.Ratings && this.state.details.Ratings[0].Value} </Text>
I have a model:
export default Model.extend({
title: attr('string'),
attributes: attr('jsonb')
});
Where attributes is a custom json filed stored as jsonb in Postgres.
let say:
{
"name":"Bob",
"city":""
}
So I can easily manipulate attributes using template
<form.element .. #property="attributes.city"/> or model.set('attributes.city','city name')
Problem: hasDirtyAttributes do not changing because technically we have old object. But when I try to copy object let say
JSON.parse(JSON.stringify(this.get('attributes')) hasDirtyAttributes works as expected
So how to write some Mixin for a Model or other workaround which on the change of any attribute property will mark hasDirtyAttributes as true. I will update whole object so doesn't matter which property actually was changed.
Same problem: https://discuss.emberjs.com/t/hasdirtyattributes-do-not-work-with-nested-attributes-json-api/15592
existing solution doesn't work for me at all:
ember-dirtier
ember-data-relationship-tracker
ember-data-model-fragments (a lot of changes under the hood and broke my app)
Update:
Some not perfect idea that help better describe what I'm want to achieve:
Let say we adding observer to any object fileds:
export default Model.extend({
init: function(){
this._super();
this.set('_attributes', Object.assign({}, this.get('attributes'))); //copy original
Object.keys(this.get('attributes')).forEach((item) => {
this.addObserver('attributes.'+ item, this, this.objectObserver);
});
}
...
})
And observer:
objectObserver: function(model, filed){
let privateFiled = '_' + filed;
if (model.get(privateFiled) != model.get(filed)) { //compare with last state
model.set(privateFiled, this.get(filed));
model.set('attributes', Object.assign({}, this.get('attributes')) );
}
}
It's works, but when I change one filed in object due to copying object objectObserver faired again on every filed. So in this key changing every filed in object I mark observed filed as dirty
The further ember development will reduce using of event listener and two-way binding, actually Glimmer components supports only One-way Data Flow. So to be friendly with future versions of emberusing one-way data flow is good approach in this case to. So In my case as I use ember boostrap solution looks like
<form.element #controlType="textarea" #onChange={{action 'attributeChange'}}
where attributeChange action do all works.
New Glimmer / Octane style based on modifier and looks like:
{{!-- templates/components/child.hbs --}}
<button type="button" {{on "click" (fn #onClick 'Hello, moon!')}}>
Change value
</button>
Although I feel this answer is relatively close to my problem and got some reputation, I don't get it right. I read a lot of posts on how to use the "new" style of Observer-pattern ((...).pipe(map(...)).subscribe(...) and *ngFor="... | async") in Angular and now also stumbled across How to Avoid Observables in Angular. I don't want to avoid reactive behaviour; I want to have changes in the REST-API to be reflected "live" to the user without reloading the Observer. That's why I want to subscribe an object (and therefore also its properties) to an Observable (and the 'might-be-there' values from the data-stream in it), right?
In my template I have:
<p>Werte:<br><span *ngFor="let attribute of _attributes | slice:0:5; index as h">
{{attribute.name}}: <strong>{{getParamNameAt(h)}}</strong> </span><br>
<span *ngFor="let attribute of _attributes | slice:5: _attributes.length; index as h">
{{attribute.name}}: <strong>{{getParamNameAt(h + 5)}}</strong> </span></p>
In my component I have:
private _attributes: Attribute[];
constructor(private attributesService: BkGenericaAttributesService) {
attributesService.getAttributes().subscribe({ next: attributes => this._attributes = attributes });
}
getParamNameAt(h: number): string {
let attrVal = this.bkHerstArtNr.charAt(h);
return attributes[h].subModule.find(param => param.subModuleValue === attrVal).subModuleName;
}
and as service I have:
const localUrl = '../../assets/json/response.json';
#Injectable()
export class BkGenericaAttributesMockService implements BkGenericaAttributesService {
constructor(private http: HttpClient) {
}
getAttributes(): Observable<Attribute[]> {
return this.http.get<Attribute[]>(localUrl).pipe(
tap((attributes : Attribute[]) => attributes.map((attribute : Attribute) => console.log("Piping into the http-request and mapping one object after another: " + attribute.name))),
map((attributes : Attribute[]) => attributes.map((attribute : Attribute) => new Attribute(attribute.id, attribute.name, attribute.title, attribute.description,
(attribute.parameters ? attribute.parameters.map((parameter : Parameter) => new Parameter(parameter.id,
parameter.name, parameter.value)) : [])))));
}
My problem running the application at this point is the 'to-create-on-stream' Attribute-objects and "nested" Parameters[]-array (created by pushing Parameter-objects into it) pushed into the _attributes-array from the httpClient's Observable: Piping into the http-request and mapping one object after another: undefined.
Apart from this - is my construct the right way to read values from a JSON-file (or alternatively an API-stream, which may change while a user visits the SPA) into properties of multiple objects displayed on the Angular view?
With the answer mentioned above - or the way I thought I have to translate it into my code - I start to doubt that I'm really understanding (and using) the reactive Angular way with Data-Providers <= Observables => Operators => Subscribers and finally Observers displayed to the user.
I really am confused (as you can read), because a lot of answers and descriptions that I found so far use either older patterns (before Angular 5.5?) and/or partially contradict each other.
Do I handle the API-changes in the right place? Has the array for the template's *ngFor-directives to be an Observer (handled with | async) or will the changes of respectively within the array be handled by the model behind the template and the template grabs its new values (with interpolation and property binding) and also directives with a change in the components properties without asyncing?
Briefly:
Is there a for-dummies default instruction to "stream-read" values from a http-request into multiple Typescript-objects and their properties concurrently displayed in a template with on-stream-changing directives rendered only in the relevant DOM nodes, the Angular 8 opinionated way? "Stream-reading" meaning: pulling and pushing (only) if there are changes in the API, without wasting resources.
If I understand right, you want the server to push changes to the client, as soon as they happen, so that the client can react and render accordingly, right ?
this.http.get is a single call, that will resolve with a snapshot of the data. Meaning that it won't automatically update just because some data changed in your backend.
If you want to notify the client about new data or even send that data directly to the client, you'll need websockets.
There is also some problems with code:
if you .subscribe(), you'll need to .unsubscribe(), otherwise you'll end up with a memory leak.
param => param.value === attrVal, where is attrVal coming from, I don't see it being set aynwhere in the method ?
You should use the async pipe, as it unsubscribes automatically for you.
You don't neet to create class instances via new in your service, instead your Attribute typing should be an interface.
I'm using Angular 4 to develop an app which is mainly about displaying data from DB and CRUD.
Long story short I found that in Angular 4 the component html doesn't like displaying loosely typed object (leaving the space blank while displaying other things like normal with no warning or error given in console) even if it can be easily displayed in console.log output, as shown in a string.
So I made a function in the service file to cast the values into a set structure indicating they're strings.
So now something like this works:
HTML
...
<div>{{something.value}}</div>
...
Component.ts
...
ngOnInit() {
this.route.params.subscribe(params => {
this.pkey = params['pkey'];
this.service.getSomethingById(this.pkey)
.then(
something => {
this.something = this.service.convertToStructure(something);
},
error => this.errorMessage = <any>error);
});
}
...
Code of the function convertToStructure(something)
convertToStructure(someArr: myStructure): myStructure {
let something: myStructure = new myStructure();
something.value = someArr[0].value;
return something;
}
But as I dig into other files for copy and paste and learn skills from what my partner worked (we're both new to Angular) I found that he did NOT cast the said values into a fixed structure.
He thought my problem on not being able to display the values (before I solved the problem) was because of me not realizing it was not a plain JSON object {...} but an array with a single element containing the object [{...}] .
He only solved half of my problem, cause adding [0] in html/component.ts was not able to make it work.
Component.ts when it did NOT work
...
ngOnInit() {
this.route.params.subscribe(params => {
this.pkey = params['pkey'];
this.service.getSomethingById(this.pkey)
.then(
something => {
console.log(something[0].value); //"the value"
this.something = something[0]; //html can't find its value
},
error => this.errorMessage = <any>error);
});
}
...
HTML when it did NOT work
...
<div>{{something[0].value}}</div> <!--Gives error on the debug console saying can't find 'value' of undefined-->
...
And of course when I'm using the failed HTML I only used this.something = something instead of putting in the [0], and vice versa.
So I looked into his code in some other page that display similar data, and I found that he used *ngFor in html to extract the data and what surprised me is that his html WORKED even if both of our original data from the promise is identical (using the same service to get the same object from sever).
Here's what he did in html:
...
<div *ngFor="let obj of objArr" ... >
{{obj.value}}
</div>
...
His html worked.
I'm not sure what happened, both of us are using a raw response from the same service promise but using for loop in html makes it automatically treat the value as strings while me trying to simply inject the value fails even if console.log shows a double quoted string.
What's the difference between having the for loop and not having any for loop but injecting the variable into html directly?
Why didn't he have to tell Angular to use the set structure indicating the values are strings while me having to do all the trouble to let html knows it's but a string?
The difference here is as you said that your JSON is not simple object , its JSON Array and to display data from JSON array you need loop. So, that is why your friends code worked and yours did not. And please also add JSON as well.
Here is a screenshot from their docs about <Link> component
What state do they mean? A Redux state?
How does it look like to pass a state? Like this?
pathname: '/foo',
query: {
x: this.props.x,
},
state: store.getState()
It's a piece of information that you'd like to send to the next page. Nothing to do with Redux. It's a plain object. I believe Flipkart is a very nice example of how it can be used to improve user experience:
Go to a Flipkart search page on a mobile device (or simulate one using Chrome DevTools)
Tap on one of the items
You'll see that the transition happens instantly and pieces of information like product images, title, rating and price are readily available on the product page. One way to implement that is passing the state they had already loaded on the search page onto the next one:
<Link
to={`/product/${id}`}
state={{
product,
}}
/>
And then:
function ProductPage(props) {
// Always check because state is empty on first visit
if (props.location.state.product) {
console.log(props.location.state.product);
// { id: '...', images: [...], price: { ... } }
}
}
There are two ways to pass data from one route to another via Link.
URL Parameter.
As state.
URL parameter help when the route params contain strings for example we want to route to a particular profile:
<Link to='/azheraleem'>Visit Profile</Link>
However, the later i.e. the state helps us pass data from one route to another which is complex data structure. (objects/arrays).
As per the react router documentation, in case of passing data from one route to another it can be done as per the below code sample:
<Link
to={{
pathname: "/profile",
search: "?name=azheraleem",
state: { fromDashboard: true }
}}
/>
The pathname is the link to the route while the search attribute contains the query string parameters, thus the on clicking the link the URL will form something like:
http://localhost:3000/profile?name=azheraleem.
But the state variable value can be accessed in the called route using the useLocation hook:
import { useLocation } from "react-router";
const profile() => {
let data = useLocation();
console.log(data.state.fromDashboard);
}
The the state property of the to prop is the param of pushState method of History DOM object described here
That props used in push/replace methods of router as described here for transitions to a new URL, adding a new entry in the browser history like this:
router.push('/users/12')
// or with a location descriptor object
router.push({
pathname: '/users/12',
query: { modal: true },
state: { fromDashboard: true }
})
It also mentioned here:
router.push(path)
router.push({ pathname, query, state }) // new "location descriptor"
router.replace(path)
router.replace({ pathname, query, state }) // new "location descriptor"
state is a property that's part of the object you can provide to the to prop of the <Link> component.
It is particularly useful if you want to send data from the current view to one the <Link> directs you to, without using common techniques such as setting URL parameters or using libraries, such as Redux.
There isn't much official information about the state key, but here's what I found in the source code of that component:
Links may pass along location state and/or query string parameters
in the state/query props, respectively.
So basically, it's like sending props to a component from a parent. Here, you are sending "state" from the current view to the target view. That's about it, really.
In simple term state in <Link/> component is use to pass information from one view to other view through router in form of object.On other page it can be access using prop.location.state.
(Note: on browser refresh state no longer contain information)
To pass state in Link:
<Link to={{pathname: "/second_page", state: {id: 123}}} />
To access id in second page view:
let id = props.location.state.id;
For more Link properties : React Router Link