How to display complex json object with following response,
const movie = "AVTAR";
const displayData = {
"actor": [{
"id": 1,
"name": "john"
}, {
"id": 2,
"name": "peter"
}],
"actress": [{
"id": 1,
"name": "ema"
}, {
"id": 2,
"name": "molly"
}]
}
on UI I need to show like,
Avatar >> actor >> john
Avatar >> actor >> peter
Avatar >> actress >> ema
Avatar >> actress >>molly
I am using react functional component with es map function
I like to learn from looking at code examples, and when I started I also struggle, hope this helps.
// First flat the displayData object into an array of objects in the form { type, id, name }
const flatData = Object.entries(displayData).flatMap(([k, v]) => {
// Because type (actor / actress) is an array we have to iterate over its keys and values
const inner = v.map(({id, name}) => {
// Return an object with typem, id and name
return {type: k, id, name};
});
// return the previously constructed object
return inner;
// Flattens the array
}).flat();
// Iterate the data and display its content
flatData.forEach( (data) => {
// Destructure the values from the data object
const {type, id, name} = data
console.log(`${movie} >> ${type} >> ${name}`);
});
Any questions just ask.
So based on the description you're creating little breadcrumbs for each cast member of a movie. Because each breadcrumb will need a unique id it's always better to wrangle the data to ensure that this is possible rather than use the map iteration id in the component. This will also ensure that you get to make very simple components which are easy to test.
So in order to do that we need to change the data from that complex model to a simpler one: an array of objects that we can iterate over, and each object will have its own unique id (at the moment they don't - actors and actresses share ids at the moment).
There are two steps to that:
Add the cast member role to each of their objects
Re-id all of the objects so each is unique.
Let's start with that first.
const data={actor:[{id:1,name:"john"},{id:2,name:"peter"}],actress:[{id:1,name:"ema"},{id:2,name:"molly"}]};
// For each cast member add their role (actor/actress)
// Do this by mapping over an array, and then
// returning a new object that includes the type
function addRole(data, type) {
return data[type].map(n => {
return { ...n, type };
});
}
// Re-id all of the cast information
// Flatten the arrays, map over them, and return a
// new array where each object has a consecutive id
function reId(arrs) {
return arrs
.flat()
.map((n, id) => {
return { ...n, id: id + 1 };
});
}
// Add all that together
const wrangled = reId([
addRole(data, 'actor'),
addRole(data, 'actress')
]);
console.log(wrangled);
Once you've done that you can throw the title and the wrangled data into the component, and then build the breadcrumbs for each movie's cast member.
// Accepts a title, and some wrangled data
// We `map` over the data and for each cast member
// we use the Breadcrumb component to
// generate a breadcrumb for that cast member
// Note we use the new generated id as each breadcrumb key
function Movie({ title, data }) {
return (
<section>
{data.map(char => {
return (
<Breadcrumb
key={char.id}
title={title}
char={char}
/>
)
})}
</section>
);
}
// Breadcrumb accepts a title, and the character data
// and produces an inline list breadcrumb for that character
function Breadcrumb({ title, char }) {
return (
<ul>
<li>{title}</li>
<li>{char.type}</li>
<li>{char.name}</li>
</ul>
);
}
const data={actor:[{id:1,name:"john"},{id:2,name:"peter"}],actress:[{id:1,name:"ema"},{id:2,name:"molly"}]};
function addRole(data, type) {
return data[type].map(n => ({ ...n, type }));
}
function reId(arrs) {
return arrs.flat().map((n, id) => ({ ...n, id: id + 1 }));
}
const wrangled = reId([ addRole(data, 'actor'), addRole(data, 'actress') ]);
ReactDOM.render(
<Movie title="Avatar" data={wrangled} />,
document.getElementById('react')
);
ul { list-style: none; margin-left: 0; padding-left: 0;}
li { display: inline; text-transform: uppercase;}
li:first-child { color: red; }
li:not(:last-child)::after { content: ' >> '; color: black;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Related
I need to be able to receive data from an external API and map it dynamically to classes. When the data is plain object, a simple Object.assign do the job, but when there's nested objects you need to call Object.assign to all nested objects.
The approach which I used was to create a recursive function, but I stumble in this case where there's a nested array of objects.
Classes
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
}
Function to initialize a class
function create(instance: object, data: any) {
for (const [key, value] of Object.entries(instance)) {
if (Array.isArray(value)) {
for (const element of data[key]) {
// get the type of the element in array dynamically
const newElement = new User();
create(newElement, element)
value.push(newElement);
}
} else if (typeof value === 'object') {
create(value, data[key]);
}
Object.assign(value, data);
}
}
const orgWithError = Object.assign(new Organization(), { admin: { id: 'admin-external' }});
console.log(orgWithError.admin.getFullName()); // orgWithError.admin.getFullName is not a function
const org = new Organization();
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
create(org, data);
// this case works because I manually initialize the user in the create function
// but I need this function to be generic to any class
console.log(org.users[0].getFullName()); // "name surname"
Initially I was trying to first scan the classes and map it and then do the assign, but the problem with the array of object would happen anyway I think.
As far as I understand from your code, what you basically want to do is, given an object, determine, what class it is supposed to represent: Organization, Account or User.
So you need a way to distinguish between different kinds of objects in some way. One option may be to add a type field to the API response, but this will only work if you have access to the API code, which you apparently don't. Another option would be to check if an object has some fields that are unique to the class it represents, like admin for Organization or account for User. But it seems like your API response doesn't always contain all the fields that the class does, so this might also not work.
So why do you need this distinction in the first place? It seems like the only kind of array that your API may send is array of users, so you could just stick to what you have now, anyway there are no other arrays that may show up.
Also a solution that I find more logical is not to depend on Object.assign to just assign all properties somehow by itself, but to do it manually, maybe create a factory function, like I did in the code below. That approach gives you more control, also you can perform some validation in these factory methods, in case you will need it
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
static fromApiResponse(data: any) {
const org = new Organization()
if(data.id) org.id = data.id
if(data.admin) org.admin = User.fromApiResponse(data.admin)
if(data.users) {
this.users = org.users.map(user => User.fromApiResponse(user))
}
return org
}
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
static fromApiResponse(data: any) {
const user = new User()
if(data.id) user.id = data.id
if(data.name) user.name = data.name
if(data.account)
user.account = Account.fromApiResponse(data.account)
return user
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
static fromApiResponse(data: any) {
const acc = new Account()
if(data.id) acc.id = data.id
if(data.money) acc.money = data.money
return acc
}
}
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const organization = Organization.fromApiResponse(data)
I can't conceive of a way to do this generically without any configuration. But I can come up with a way to do this using a configuration object that looks like this:
{
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
}
and a pointer to the root node, 'org'.
The keys of this object are simple handles for your type/subtypes. Each one is mapped to an object that has a _ctor property pointing to a constructor function, and a collection of other properties that are the names of members of your object and matching properties of your input. Those then are references to other handles. For an array, the handle is [surrounded by square brackets].
Here's an implementation of this idea:
const create = (root, config) => (data, {_ctor, ...keys} = config [root]) =>
Object.assign (new _ctor (), Object .fromEntries (Object .entries (data) .map (
([k, v]) =>
k in keys
? [k, /^\[.*\]$/ .test (keys [k])
? v .map (o => create (keys [k] .slice (1, -1), config) (o))
: create (keys [k], config) (v)
]
: [k, v]
)))
class Organization {
constructor () { this.id = 'org1'; this.admin = new User(); this.users = [] }
}
class User {
constructor () { this.id = 'user1'; this.name = 'name'; this.account = new Account() }
getFullName () { return `${this.name} surname`}
}
class Account {
constructor () { this.id = 'account1'; this.money = 10 }
calculate () { return 10 * 2 }
}
const createOrganization = create ('org', {
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
})
const orgWithoutError = createOrganization ({ admin: { id: 'admin-external' }});
console .log (orgWithoutError .admin .getFullName ()) // has the right properties
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const org = createOrganization (data)
console .log (org .users [0] .getFullName ()) // has the right properties
console .log ([
org .constructor .name,
org .admin .constructor.name, // has the correct hierarchy
org .users [0]. account. constructor .name
] .join (', '))
console .log (org) // entire object is correct
.as-console-wrapper {min-height: 100% !important; top: 0}
The main function, create, receives the name of the root node and such a configuration object. It returns a function which takes a plain JS object and hydrates it into your Object structure. Note that it doesn't require you to pre-construct the objects as does your attempt. All the calling of constructors is done internally to the function.
I'm not much of a Typescript user, and I don't have a clue about how to type such a function, or whether TS is even capable of doing so. (I think there's a reasonable chance that it is not.)
There are many ways that this might be expanded, if needed. We might want to allow for property names that vary between your input structure and the object member name, or we might want to allow other collection types besides arrays. If so, we probably would need a somewhat more sophisticated configuration structure, perhaps something like this:
{
org: { _ctor: Organization, admin: {type: 'usr'}, users: {type: Array, itemType: 'usr'} },
usr: { _ctor: User, account: {type: 'acct', renameTo: 'clientAcct'} },
acct: { _ctor: Account }
}
But that's for another day.
It's not clear whether this approach even comes close to meeting your needs, but it was an interesting problem to consider.
I'm facing a problem with the search. It is a front-end search rather than a remote search, I'm using react.js because it is a requirement in the problem and created a component named App. My task is to display and highlight the matching parts according to the type value.
I will appreciate it. If you provide me a good solution for this.
Let me tell you the whole scenario. I'm dividing this problem into 3 parts.
Part 1: What is the shape of the data?
The shape of the data is this:
src/data.js:
export default [
{
id: 1,
name: 'Wordpress',
list: [
{
id: 1,
name: 'Best Mobile App Builder',
slug: '/'
},
{
id: 2,
name: 'Best Wordpress Themes',
slug: '/'
},
{
id: 3,
name: 'Best Website Creator',
slug: '/'
},
{
id: 4,
name: 'Best Wordpress Builder',
slug: '/'
}
]
},
{
id: 2,
name: 'SaaS',
list: [
{
id: 1,
name: 'Appointment Scheduling Software',
slug: '/'
},
{
id: 2,
name: 'Design Services',
slug: '/'
},
{
id: 3,
name: 'Online Cloud Storage',
slug: '/'
},
{
id: 4,
name: 'Remote PC Access',
slug: '/'
}
]
},
];
Note:
Basically this is my filter function.
src/filter.js:
import _ from 'lodash';
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
/**
* Returns the new filtered array with highlighted parts.
* #param data {Array<Object>} - The collection to iterate over.
* #param inputValue {string} - The input value.
* #return {Array} - Returns the new filtered array.
*/
export const filterByNames = (data, inputValue) => {
// Create a dynamic regex expression object with ignore case sensitivity
const re = new RegExp(_.escapeRegExp(inputValue), 'i');
const results = data.filter((object) => {
if (re.test(object.name)) {
return true;
} else {
return object.list.some((item) => {
if (re.test(item.name)) {
// Calculates the characters to highlight in text based on query
const matches = match(item.name, inputValue);
// Breaks the given text to parts based on matches.
// After that create a new property named `parts` and assign an array to it.
item['parts'] = parse(item.name, matches);
return true;
} else {
return false;
}
});
}
});
return results;
};
The search is working fine but facing 2 major issues.
When the above match of the name property occurs, then it stops and does not go much deeper. The same thing is happening with the nested list name property.
When the filtration happens behind the scenes we're mutating the original data by adding a new property named parts which contains highlighted parts and it is an array of objects. But I don't want to mutate the original data instead wants to return the new filtered array which contains parts property.
See this.
WORKING DEMO :
Part 2: Which third-party libraries I'm using for filter and highlighting?
lodash string function escapeRegExp for escapes the RegExp
special characters.
autosuggest-highlight match function to calculates the
characters to highlight in text based on the query.
After that, from the same library parse function help us to break the given text to parts based on matches. In the end, it will
return an array of objects with the match string and highlight
boolean flag. So it's easy for us to bold the highlighted parts on the UI.
Part 3: App component
import React, { useState } from 'react';
import { filterByNames } from './filter';
import data from './data';
/**
* Return the JSX for the List
* #param data {Array<Object>} - The collection to iterate over.
* #return {null|*} - Returns the JSX or null.
*/
const renderList = (data) => {
if (Array.isArray(data) && data.length > 0) {
return data.map((object) => {
return (
<div key={object.id}>
<h1>{object.name}</h1>
<ul className="list">
{object.list.map((item) => {
return (
<li key={item.id}>
{item.parts ? (
<a href={item.slug}>
{item.parts.map((part, index) => (
<span
key={index}
style={{ fontWeight: part.highlight ? 700 : 400 }}
>
{part.text}
</span>
))}
</a>
) : (
<a href={item.slug}>{item.name}</a>
)}
</li>
)
})}
</ul>
</div>
)
})
} else {
return null
}
};
// Main App Component
const App = () => {
const [value, setValue] = useState('');
const onChangeHandler = (event) => {
const { target } = event;
const val = target.value;
setValue(val);
};
const results = !value ? data : filterByNames(data, value);
return (
<div className="demo">
<input type="text" value={value} onChange={onChangeHandler}/>
<div className="demo-result">
{ renderList(results) }
</div>
</div>
);
};
export default App;
Here is the revised code.
export const filterByNames = (data, inputValue) => {
// Create a dynamic regex expression object with ignore case sensitivity
const re = new RegExp(_.escapeRegExp(inputValue), "i");
const clonedData = _.cloneDeep(data);
const results = clonedData.filter((object) => {
return object.list.filter((item) => {
if (re.test(item.name)) {
// Calculates the characters to highlight in text based on query
const matches = match(item.name, inputValue);
// Breaks the given text to parts based on matches.
// After that create a new property named `parts` and assign an array to it.
item["parts"] = parse(item.name, matches);
return true;
} else {
return false;
}
}).length > 0 || re.test(object.name);
});
return results;
};
Forked link here.
https://codesandbox.io/s/search-frontend-forked-e3z55
Here is the code having solved both
export const filterByNames = (data, inputValue) => {
// Create a dynamic regex expression object with ignore case sensitivity
const re = new RegExp(_.escapeRegExp(inputValue), "i");
// since we cannot directly mutate the data object, why not copy it here ? (or if the data is bigger and copying is also not an option then consider using two arrays of data, one for the mutation and one default maybe)
let data_ = JSON.parse(JSON.stringify(data));
// filter and return the newer copy of the object.
const results = data_.filter((object) => {
// since we need the highlighting in both cases, on top level, or even in nested level so create separate function for that.
let highLightEm = (list) => {
return object.list.some((item) => {
if (re.test(item.name)) {
// Calculates the characters to highlight in text based on query
const matches = match(item.name, inputValue);
// Breaks the given text to parts based on matches.
// After that create a new property named `parts` and assign an array to it.
item["parts"] = parse(item.name, matches);
return true;
} else {
return false;
}
});
};
if (re.test(object.name)) {
// check for the highlighting in the inner name
highLightEm(object);
return true;
} else {
return highLightEm(object);
}
});
return results;
};
https://codesandbox.io/s/search-frontend-forked-kxui9?file=/src/filter.js
MirageJS provides all model ids as strings. Our backend uses integers, which are convenient for sorting and so on. After reading around MirageJS does not support integer IDs out of the box. From the conversations I've read the best solution would be to convert Ids in a serializer.
Output:
{
id: "1",
title: "Some title",
otherValue: "Some other value"
}
But what I want is:
Expected Output:
{
id: 1,
title: "Some title",
otherValue: "Some other value"
}
I really want to convert ALL ids. This would included nested objects, and serialized Ids.
I think you should be able to use a custom IdentityManager for this. Here's a REPL example. (Note: REPL is a work in progress + currently only works on Chrome).
Here's the code:
import { Server, Model } from "miragejs";
class IntegerIDManager {
constructor() {
this.ids = new Set();
this.nextId = 1;
}
// Returns a new unused unique identifier.
fetch() {
let id = this.nextId++;
this.ids.add(id);
return id;
}
// Registers an identifier as used. Must throw if identifier is already used.
set(id) {
if (this.ids.has(id)) {
throw new Error('ID ' + id + 'has already been used.');
}
this.ids.add(id);
}
// Resets all used identifiers to unused.
reset() {
this.ids.clear();
}
}
export default new Server({
identityManagers: {
application: IntegerIDManager,
},
models: {
user: Model,
},
seeds(server) {
server.createList("user", 3);
},
routes() {
this.resource("user");
},
});
When I make a GET request to /users with this server I get integer IDs back.
My solution is to traverse the data and recursively convert all Ids. It's working pretty well.
I have a number of other requirements, like removing the data key and embedding or serializing Ids.
const ApplicationSerializer = Serializer.extend({
root: true,
serialize(resource, request) {
// required to serializedIds
// handle removing root key
const json = Serializer.prototype.serialize.apply(this, arguments)
const root = resource.models
? this.keyForCollection(resource.modelName)
: this.keyForModel(resource.modelName)
const keyedItem = json[root]
// convert single string id to integer
const idToInt = id => Number(id)
// convert array of ids to integers
const idsToInt = ids => ids.map(id => idToInt(id))
// check if the data being passed is a collection or model
const isCollection = data => Array.isArray(data)
// check if data should be traversed
const shouldTraverse = entry =>
Array.isArray(entry) || entry instanceof Object
// check if the entry is an id
const isIdKey = key => key === 'id'
// check for serialized Ids
// don't be stupid and create an array of values with a key like `arachnIds`
const isIdArray = (key, value) =>
key.slice(key.length - 3, key.length) === 'Ids' && Array.isArray(value)
// traverse the passed model and update Ids where required, keeping other entries as is
const traverseModel = model =>
Object.entries(model).reduce(
(a, c) =>
isIdKey(c[0])
? // convert id to int
{ ...a, [c[0]]: idToInt(c[1]) }
: // convert id array to int
isIdArray(c[0], c[1])
? { ...a, [c[0]]: idsToInt(c[1]) }
: // traverse nested entries
shouldTraverse(c[1])
? { ...a, [c[0]]: applyFuncToModels(c[1]) }
: // keep regular entries
{ ...a, [c[0]]: c[1] },
{}
)
// start traversal of data
const applyFuncToModels = data =>
isCollection(data)
? data.map(model =>
// confirm we're working with a model, and not a value
model instance of Object ? traverseModel(model) : model)
: traverseModel(data)
return applyFuncToModels(keyedItem)
}
})
I had to solve this problem as well (fingers crossed that this gets included into the library) and my use case is simpler than the first answer.
function convertIdsToNumbers(o) {
Object.keys(o).forEach((k) => {
const v = o[k]
if (Array.isArray(v) || v instanceof Object) convertIdsToNumbers(v)
if (k === 'id' || /.*Id$/.test(k)) {
o[k] = Number(v)
}
})
}
const ApplicationSerializer = RestSerializer.extend({
root: false,
embed: true,
serialize(object, request) {
let json = Serializer.prototype.serialize.apply(this, arguments)
convertIdsToNumbers(json)
return {
status: request.status,
payload: json,
}
},
})
How can I make sure that no duplicates are displayed with vue inside a template ?
I my case the data is an array of objects and key that has a value of an object with multiple objects within it. So this would be a nested v-for in vue template syntax.
{
"matches": [
{
"birthday": "1/29/2019",
"household": {
"0": {
"relationship": "brother"
},
"1": {
"relationship": "brother"
}
}
}
]
}
I would only like to display 1 unique relationship per household. Please see fiddle for further examination https://jsfiddle.net/sxmhv3t7/
You can use computed property to make matches array unique.
For example:
computed: {
uniqueMatches () {
return this.matches.map(item => {
let households = Object.values(item.household)
let relationships = households.map(item => item.relationship)
const uniqueRelationships = [...new Set(relationships)]
item.household = uniqueRelationships.reduce((acc, cur, idx) => {
acc[idx] = {}
acc[idx].relationship = cur
return acc
}, {})
console.log(item)
return item
})
}
}
and then use uniqueMatches instead of matches in template
Demo in jsfiddle
You could massage the data a bit and create a uniqueHouseholdMembers array property on each match in the matches array and then just print out those values:
matches.forEach(match => {
let houseHolds = Object.values(match.household);
match.uniqueHouseholdMembers = houseHolds.reduce((acc, household) => {
// check if household member has already been added to our growing list
let isUniqueRelationship = !acc.includes(household.relationship);
// add household member if unique
if (isUniqueRelationship) {
acc.push(household.relationship);
}
return acc;
}, []);
});
// output using the data you provided:
// match[0].uniqueHouseholdMembers -> ['brother']
// match[1].uniqueHouseholdMembers -> ['sister']
// and if you have a 3rd match entry with more household members:
// match[2].uniqueHouseholdMembers -> ['brother', 'father', 'stranger']
I'm trying to return the data inside each array in an object.
This is my Json:
"student_clubs": { // brought in as object
"art": { // object
"illustration": ["Done"], // Array
"design": ["Done"] // Array
},
"sports": { // object
"Basketball": ["Incomplete"], // Array
"Hockey": ["Done"] // Array
}
},
I'm trying to display it like this:
art:
illustration: done design: done
sports:
Basketball: Incomplete Hockey: Done
my jsx:
import React, { PropTypes } from 'react';
const ClubsPropTypes = {
student_clubs: PropTypes.object.isRequired,
student_grades: PropTypes.object.isRequired,
};
class Clubs extends React.Component {
render() {
const StudentClubs = this.props.student_clubs;
const Grades = this.props.grades;
return (
<div className="">
{(StudentClubs.art || []).map(art => (
<div className="row">
<ul>
<p>art</p>
<li>illustration: {art.illustration}</li>
<li>design: {art.design}</li>
</ul>
</div>
))}
... same code for sports (seems repetitive)
</div>
);
}
}
Clubs.propTypes = ClubsPropTypes;
module.exports = Clubs;
Right now I'm getting Uncaught TypeError: (StudentClubs.art || []).map is not a function. Why is that? How can I fix this? Also is there a better way to loop through these objects? :\
StudentClubs.art is an object so you cant use it with map.
You can loop over an object using Object.keys() which returns an array of the object keys, see: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
You can loop over an object keys like:
Object.keys(myobject).map((key) => {
// do stuff with myobject[key]
})
Or using for in, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
eg:
for (let key in myobject) {
if (myobject.hasOwnProperty(key)) {
// do stuff with myobject[key]
}
}
So then you can loop over the different clubs and for each of those clubs loop over their keys - display the key name and the list of values (Eg. "done").
If you can, adjust your JSON so that student_clubs is an array of objects and each of those is an array of objects containing the sub category string and a status string.
You dont need an array for Done / Incomplete.
In your json art property is not an array, is an object, and objects has not the map function.
I don't understand why you might want to have an array to specify the state of the subproperty (sport or art).
I would do as follows:
"student_clubs": [{
"art":[
{
"discipline":"illustration",
"status: "Done"
},
{
"discipline:"Design",
"status":"Done"
}
],
"sports":[
{
"discipline":"Basketball",
"status":"Incomplete"
},
{
"discipline":"Hockey",
"status":"whatever"
}
]
]}
So now you can map througth each club, and inside each club you can map througth each "discipline" and do whatever you want in an easier way I think.