I have the following data coming from an API:
const data = {
id: 1,
name: 'myboard',
columns: [
{
id: 1,
name: 'col1',
cards: [
{ id: 1, name: 'card1' },
{ id: 2, name: 'card2' }
]
},
{
id: 2,
name: 'col2',
cards: [
{ id: 3, name: 'card3' },
{ id: 4, name: 'card4' }
]
},
]
}
As you can see, there are essentially 3 nested levels. The top level contains an id, name and a list of columns. Each column contains an id, name and a list of cards. Each card has an id and name.
I wish to normalize the data for use in Redux as presented here. I am using normalizr to do this as follows:
const card = new schema.Entity('cards');
const column = new schema.Entity('columns', {
cards: [card]
});
const board = new schema.Entity('boards', {
columns: [column]
});
normalize(data, board)
This results in the following:
{
"entities": {
"cards": {
"1": {
"id": 1,
"name": "card1"
},
"2": {
"id": 2,
"name": "card2"
},
"3": {
"id": 3,
"name": "card3"
},
"4": {
"id": 4,
"name": "card4"
}
},
"columns": {
"1": {
"id": 1,
"name": "col1",
"cards": [1, 2]
},
"2": {
"id": 2,
"name": "col2",
"cards": [3, 4]
}
},
"boards": {
"1": {
"id": 1,
"name": "myboard",
"columns": [1, 2]
}
}
},
"result": 1
}
What I can't seem to figure out is how to have each section (ie: cards, columns, boards) split into two sections, namely byId and allIds as per the Redux article referenced above.
Essentially this is to make ordering, sorting etc easier in a React application. Im using the latest version of normalizr (3.2.4).
Here is a CodeSandbox with an example of how you can set up the reducers to handle the normalized state.
Essentially, you will end up with something like this for each of your entities:
// lambda or function - whatever your preference is
const cardsById = (state = {}, action) => {
// if, case, handler function - whatever your preference is
if (action.type === 'ADD_DATA') { // or whatever your initial data load type is
return { ...state, ...action.payload.cards }
}
return state
}
const allCards = (state = [], action) => {
if (action.type === 'ADD_DATA') { // or whatever your initial data load type is
return [...state, ...Object.keys(action.payload.cards)]
}
return state
}
const cards = combineReducers({
byId: cardsById,
allIds: allCards
})
and then combine all of those together:
export default combineReducers({
cards,
columns,
boards
})
The action creators for this are as follows:
const addData = ({ entities }) => ({
type: 'ADD_DATA',
payload: entities // note the rename - this is not required, just my preference
})
// I used a thunk, but theory is the the same for your async middleware of choice
export const getData = () => dispatch => dispatch(addData(normalize(data, board)))
Hope this helps. Remember that you will need to maintain both the byId and allIds for each entity as entities are added or removed.
Related
I have a json tree structure that I want to normalize into something like a hashmap and then denormalize it back to a tree if needed.
I have a very dynamic tree that I want to use as state in my react-redux project, but for that I somehow need to transform the data so that I can access it without having to search elements recursively in the tree each time I want to update/access the state.
const actual = {
id: "1",
type: 'Container',
data: {},
items: [
{
id: "2",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
},
{
id: "3",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
}
]
};
Now I want to transform it into something like:
const expected = {
1: {
id: "1",
type: 'Container',
data: {},
items: [1, 2]
},
2: {
id: "2",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
},
3: {
id: "3",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
}
};
I found a JS lib called Normalizr, but I absolutely don't get how to create the schemas for it to work.
That was my last try, and it returns only the inner two items and also directly the data object inside without id, items around:
const data = new schema.Entity("data");
const item = new schema.Object({ data });
item.define({ items: new schema.Array(item) });
const items = new schema.Array(item);
const normalizedData = normalize(mock, items);
I'm not going to worry too much about the types, since you can alter those to meet your needs. Going off you're example, I will define
interface Tree {
id: string;
type: string;
data: {
title?: string;
text?: string;
items: Tree[];
}
}
interface NormalizedTree {
[k: string]: {
id: string;
type: string;
data: {
title?: string;
text?: string;
items: string[]
}
}
}
and we want to implement function normalize(tree: Tree): NormalizedTree and function denormalize(norm: NormalizedTree): Tree.
The normalize() function is fairly straightforward since you can recursively walk the tree and collect the normalized trees into one big normalized tree:
function normalize(tree: Tree): NormalizedTree {
return Object.assign({
[tree.id]: {
...tree,
data: {
...tree.data,
items: tree.data.items.map(v => v.id)
}
},
}, ...tree.data.items.map(normalize));
}
In English, we are making a single normalized tree with a property with key tree.id and a value that's the same as tree except the data.items property is mapped to just the ids. And then we are mapping each element of data.items with normalize to get a list of normalized trees that we spread into that normalized tree via the Object.assign() method. Let's make sure it works:
const normalizedMock = normalize(mock);
console.log(normalizedMock);
/* {
"1": {
"id": "1",
"type": "Container",
"data": {
"items": [
"2",
"3"
]
}
},
"2": {
"id": "2",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
},
"3": {
"id": "3",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
}
} */
Looks good.
The denormalize() function is a little trickier, because we need to trust that the normalized tree is valid and actually represents a tree with a single root and no cycles. And we need to find and return that root. Here's one approach:
function denormalize(norm: NormalizedTree): Tree {
// make Trees with no children
const treeHash: Record<string, Tree> =
Object.fromEntries(Object.entries(norm).
map(([k, v]) => [k, { ...v, data: { ...v.data, items: [] } }])
);
// keep track of trees with no parents
const parentlessTrees =
Object.fromEntries(Object.entries(norm).map(([k, v]) => [k, true]));
Object.values(norm).forEach(v => {
// hook up children
treeHash[v.id].data.items = v.data.items.map(k => treeHash[k]);
// trees that are children do have parents, remove from parentlessTrees
v.data.items.forEach(k => delete parentlessTrees[k]);
})
const parentlessTreeIds = Object.keys(parentlessTrees);
if (parentlessTreeIds.length !== 1)
throw new Error("uh oh, there are " +
parentlessTreeIds.length +
" parentless trees, but there should be exactly 1");
return treeHash[parentlessTreeIds[0]];
}
In English... first we copy the normalized tree into a new treeHash object where all the data.items are empty. This will eventually hold our denormalized trees, but right now there are no children.
Then, in order to help us find the root, we make a set of all the ids of the trees, from which we will remove any ids corresponding to trees with parents. When we're all done, there should hopefully be a single id left, that of the root.
Then we start populating the children of treeHash's properties, by mapping the corresponding data.items array from the normalized tree to an array of properties of treeHash. And we remove all of these child ids from parentlessTreeIds.
Finally, we should have exactly one property in parentlessTreeIds. If not, we have some kind of forest, or cycle, and we throw an error. But assuming we do have a single parentless tree, we return it.
Let's test it out:
const reconsitutedMock = denormalize(normalizedMock);
console.log(reconsitutedMock);
/* {
"id": "1",
"type": "Container",
"data": {
"items": [
{
"id": "2",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
},
{
"id": "3",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
}
]
}
} */
Also looks good.
Playground link to code
I would recommend .flatMap for this kind of transformations:
const flattenTree = element => [
element,
...element.data.items.flatMap(normalizeTree)
]
This move you from this shape:
{
id: 1,
data: { items: [
{
id: 2,
data: { items: [
{ id: 3, data: { items: [] } },
] }
] }
}
to this one:
[
{ id: 1, data: {...}},
{ id: 2, data: {...}},
{ id: 3, data: {...}},
]
Then once you have a flat array, you can transform it further to remove the references and create an object from entries:
const normalizedTree = element => {
let flat = flattenTree(element)
// only keep the id of each items:
// [{ id: 1, data:{...}}] -> [1]
// for (const el of flat) {
// el.data.items = el.data.items.map(child => child.id)
// }
// note that the for loop will change the initial data
// to preserve it you can achieve the same result with
// a map that will copy every elements:
const noRefs = flat.map(el => ({
...el,
data: {
...el.data,
items: el.data.items.map(child => child.id),
},
}))
// then if you need an object, 2 steps, get entries, [id, elem]:
const entries = noRefs.map(({ id, ...element }) => [id, element])
// then the built-in `Object.fromEntries` do all the work for you
// using the first part of the entry as key and last as value:
return Object.fromEntries(entries)
// if you have multiple objects with the same id's, only the last one
// will be in your returned object
}
i'm attempting to create a Tree Diagram with react-d3-js. It needs to be in a specific format. So i need to convert the initial data that i have to the format.
This is a diagram for a shop to see the distribution chain and who is allowed to make a purchase from specific nodes.
Initial Data:
store.name = 'Absolut Chocolat' //Main Parent
store.shopconditions: [
{
"role": "agent",
"condition": ["owner", "stokist"]
},
{
"role": "stokist",
"condition": ["owner", "master stokist"]
},
{
"role": "master stokist",
"condition": ["owner"]
}
]
// If role is agent, then they are allowed to buy from 'owner' and 'stokist'
Here's the hardcoded ideal output:
orgChart = {
name: 'Absolut Chocolat',
children: [
{ name: 'Agent' },
{
name: 'Stokist',
children: [
{
name: 'Agent',
},
],
},
{
name: 'Master Stokist',
children: [
{
name: 'Stokist',
children: [
{
name: 'Agent',
},
],
},
],
},
],
};
With a few for each loops, i've gotten to the first 2 layers of the intended output but i cannot find a way to get more than that.
Here is what i got so far:
Agent node is not under Master Stokist
Current code:
let chartData = { name: store.name, children: [] };
store.shopconditions.forEach((i) => {
i.condition.forEach((c) => {
if (c === 'owner') {
chartData.children.push({ name: i.role });
}
});
});
const chartDataParser = (data) => {
data.children.map((i) => {
for (const [k, v] of Object.entries(i)) {
store.shopconditions.forEach((c) => {
c.condition.forEach((o) => {
if (o === v) {
if (!i.children) {
i.children = [{ name: c.role }];
} else {
i.children.push({ name: c.role });
}
}
});
});
}
});
};
chartDataParser(chartData);
Current output:
{
name: 'Absolut Chocolat',
children: [
{ name: 'Agent' },
{
name: 'Stokist',
children: [
{
name: 'Agent',
},
],
},
{
name: 'Master Stokist',
children: [
{
name: 'Stokist',
// Missing children: Agent Node
},
],
},
],
};
What the tree diagram should look like:
As you can see under Master Stokist node, Agent is under Stokist
The Agent node is not reached under the stokist node in the right most chain. I need a fix to my current code so it can go to that extra layer. Thanks in advance. Looking forward to learn from your answers.
You can build an object that lists children by role and then use that to recursively build the nodes of the object. Possibly something like the following:
const store = {
name: 'Absolut Chocolat',
shopconditions: [
{ "role": "agent", "condition": ["owner", "stokist"], name: 'Agent' },
{ "role": "stokist", "condition": ["owner", "master stokist"], name: 'Stockist' },
{ "role": "master stokist", "condition": ["owner"], name: 'Master Stockist' },
]
};
const build_role_map = (store) => {
let role_map = Object.fromEntries(
store.shopconditions.map((v) => [v.role, { ...v, children: [] }])
);
role_map.owner = { "role": "owner", "condition": [], children: [], name: store.name };
store.shopconditions.forEach(
({ role, condition }) => {
condition.forEach((parent) => { role_map[parent].children.push(role) })
}
);
return role_map;
};
const build_node = (role_map, { name, children }) => {
let node = { name };
if(children.length > 0)
node.children = children.map((child) => build_node(role_map, role_map[child]));
return node;
};
const build_tree = (store) => {
const role_map = build_role_map(store);
return build_node(role_map, role_map.owner);
};
console.log(build_tree(store));
I'm trying to reorganise/restructure my data to send back to the api. I am mapping values into their respective new properties.
Incoming prop:
const data = [
{
otherProperty: "string",
otherPropertyTwo: "string",
personId: "1269",
peopleGroups: [
{ group: "SENIORS", groupStatus: "paid" },
{ group: "Infants", groupStatus: "not_paid" }
]
}
];
and need to restructure into this whilst not leaving in any other properties other than the ones below:
const statusArrayUpdate = [{
"personid": "1269",
"groups": [
{
"group": "seniors",
"status": "paid"
},
{
"group": "Infants",
"status": "not_paid"
}
]
}]
I tried this but getting undefined on the 2nd mapping, groups property...
const statusArrayUpdate = data.map(d => ({ ...d, personId: d.personId, groups: d.peopleGroups.map(s => [group: s.group, status: s.groupStatus]) }));
You can use map on your data array and just take the personId and rename the peopleGroups of each object, and rename groupStatus to status for each element in groups.
const data = [
{
otherProperty: "string",
otherPropertyTwo: "string",
personId: "1269",
peopleGroups: [
{ group: "SENIORS", groupStatus: "paid" },
{ group: "Infants", groupStatus: "not_paid" }
]
}
];
const statusArrayUpdate = data.map(({ personId, peopleGroups }) => ({
personId,
groups: peopleGroups.map(({ group, groupStatus }) => ({
group,
status: groupStatus
}))
}));
console.log(statusArrayUpdate)
In my angular application i am having the data as follows,
forEachArrayOne = [
{ id: 1, name: "userOne" },
{ id: 2, name: "userTwo" },
{ id: 3, name: "userThree" }
]
forEachArrayTwo = [
{ id: 1, name: "userFour" },
{ id: 2, name: "userFive" },
{ id: 3, name: "userSix" }
]
newObj: any = {};
ngOnInit() {
this.forEachArrayOne.forEach(element => {
this.newObj = { titleOne: "objectOne", dataOne: this.forEachArrayOne };
})
this.forEachArrayTwo.forEach(element => {
this.newObj = { titleTwo: "objectTwo", dataTwo: this.forEachArrayTwo };
})
console.log({ ...this.newObj, ...this.newObj });
}
In my real application, the above is the structure so kindly help me to achieve the expected result in the same way..
The working demo https://stackblitz.com/edit/angular-gyched which has the above structure.
Here console.log(this.newObj) gives the last object,
titleTwo: "ObjectTwo",
dataTwo:
[
{ id: 1, name: "userFour" },
{ id: 2, name: "userFive" },
{ id: 3, name: "userSix" }
]
but i want to combine both and need the result exactly like the below..
{
titleOne: "objectOne",
dataOne:
[
{ id: 1, name: "userOne" },
{ id: 2, name: "userTwo" },
{ id: 3, name: "userThree" }
],
titleTwo: "ObjectTwo",
dataTwo:
[
{ id: 1, name: "userFour" },
{ id: 2, name: "userFive" },
{ id: 3, name: "userSix" }
]
}
Kindly help me to achieve the above result.. If i am wrong in anywhere kindly correct with the working example please..
You're assigning both values to this.newObj, so it just overwrites the first object.
Also, there is no need for your loop. It doesn't add anything.
Instead, you can do:
this.newObjA = { titleOne: "objectOne", dataOne: this.forEachArrayOne };
this.newObjB = { titleTwo: "objectTwo", dataTwo: this.forEachArrayTwo };
console.log({ ...this.newObjA, ...this.newObjB });
**
EDIT **
Having spoken to you regarding your requirements, I can see a different solution.
Before calling componentData, you need to make sure you have the full data. To do this, we can use forkJoin to join the benchmark requests, and the project requests into one Observable. We can then subscribe to that Observable to get the results for both.
The code would look something like this:
createComponent() {
let benchmarks, projects;
let form = this.productBenchMarkingForm[0];
if (form.benchmarking && form.project) {
benchmarks = form.benchmarking.filter(x => x.optionsUrl)
.map(element => this.getOptions(element));
projects = form.project.filter(x => x.optionsUrl)
.map(element => this.getOptions(element));
forkJoin(
forkJoin(benchmarks), // Join all the benchmark requests into 1 Observable
forkJoin(projects) // Join all the project requests into 1 Observable
).subscribe(res => {
this.componentData({ component: NgiProductComponent, inputs: { config: AppConfig, injectData: { action: "add", titleProject: "project", dataProject: this.productBenchMarkingForm[0] } } });
})
}
}
getOptions(element) {
return this.appService.getRest(element.optionsUrl).pipe(
map((res: any) => {
this.dataForOptions = res.data;
element.options = res.data;
return element;
})
)
}
Here is an example in Stackblitz that logs the data to the console
I've been working on a React/Redux application for building a quote. A gross simplification of my state would look something like this:
{
account: { name: 'john doe' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
{ product: {id: 124, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: 'asdf', vip: true }
}
and my reducers would be sliced something like this:
const appReducer = combineReducers<GlobalState>({
account: accountReducer,
lineItems: lineItemReducer,
modifiers: modifersReducer,
});
I've just recently gotten a requirements where I would essentially need to be able to render the entire app multiple times on a single page (basically show 1 or more quotes for different accounts on a single page). So a single state would now need to look something like this:
{
quotes: {
"0": {
account: { name: 'john doe' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
{ product: {id: 124, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: 'asdf', vip: true }
},
"1": {
account: { name: 'billy jean' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: '', vip: false }
},
}
}
But obviously this new state shape doesn't really work with how I've sliced my reducers. Also, seems like I'd have to refactor all my actions so that I know which quote they should be operating on? For example, if I had an action like this:
{
type: 'UPDATE_PRICE'
payload: { productId: 123, newPrice: 15 }
}
Seems like the product 123 on both quotes would be updated.
Maybe there is instead some way I can just render the entire app on the page without having to refactor my entire state? I'm not sure what my best approach would be that wouldn't requirement me to rewrite large portions of the app.
This should give you the idea. It's basically using one reducer inside another one. As simple as using a function within another function body. You can run it on runkit.com as well.
const { createStore, combineReducers } from 'redux';
const UPDATE_ACCOUNT = 'app/updat-account';
const ADD_QUOTE = 'quote/add-quote';
const appActions = {
updateAcount: (q_id, a) => ({ type: UPDATE_ACCOUNT, payload: { q_id, name: a }}),
};
const quoteActions = {
addQuote: q_id => ({ type: ADD_QUOTE, payload: q_id }),
};
const accountReducer = (app = {}, action) => {
const { type, payload } = action;
switch (type) {
case UPDATE_ACCOUNT:
return { ...app, name: payload.name }
default:
return app;
}
};
const appReducer = combineReducers({
account: accountReducer,
lineItems: (app ={}, action) => app, // just a placeholder
modifiers: (app ={}, action) => app, // just a placeholder
});
const quoteReducer = (state = {}, action) => {
const { type, payload } = action;
switch (type) {
case ADD_QUOTE:
return { ...state, [payload]: {} };
case UPDATE_ACCOUNT: {
const app = state[payload.q_id];
return app
? { ...state, [payload.q_id]: appReducer(state[payload.q_id], action) }
: state;
}
default:
return state;
}
}
const store = createStore(quoteReducer);
store.dispatch(quoteActions.addQuote(3));
store.dispatch(quoteActions.addQuote(2));
store.dispatch(appActions.updateAcount(3, 'apple'));
store.dispatch(appActions.updateAcount(4, 'orange')); // non-existent quote
store.getState():
/**
{
"2": {},
"3": {
"account": {
"name": "apple"
},
"lineItems": {},
"modifiers": {}
}
}
*/
Just wanted to add my specific answer here..
Basically I added a new root reducer as norbertpy suggested. However, I also had to add a parameter quoteId to each action to specify which quote the action originated from and should operate on. This was the most time consuming part of the refactor as now each component that dispatches actions must have access to the quote key.
Reducer
const quoteReducer = combineReducers({
account: accountReducer,
lineItems: lineItemReducer,
modifiers: modifersReducer,
});
const rootReducer = (state = {quotes: []}, action) => {
const newQuoteState = quoteReducer(state.quotes[action.quoteId], action);
const newQuotes = {...state.quotes};
newQuotes[action.quoteId] = newQuoteState;
return {...state, ...{quotes: newQuotes}};
};
Action
{
type: 'UPDATE_PRICE'
quoteId: '0',
payload: { productId: 123, newPrice: 15 }
}