Angular subscription is not working as I expect - javascript

So I have a shop page with a child component called FilterBarComponent and onInit I want it to emit all the category as by default I want all the products in the shop to be rendered, but on my homePageComponent I have a button that allows a user to navigate to the shopPage and view a specific category for e.g a button that says "view shirts". My problem is that the default categories array occurs after the subscriber function finishes and also in the subscriber the event emitter does not fire.
Here is another question of mine that relates to this problem.
Angular EventEmitter is not emitting in subscriber
FilterBarComponent
categories = [];
#Output() filteredCategory = new EventEmitter<any>();
#Output() maxiumPriceEmitter = new EventEmitter<any>();
categorySub: Subscription;
formatLabel(value: number) {
return 'R' + value;
}
constructor(private shopService: ShopService) {}
ngOnInit() {
this.initCategories();
this.filterCategories();
this.updateCategories();
}
filterCategories() {
this.shopService.filterCategories.subscribe(
(fCategory: string) => {
this.categories.map(category => {
category.checked = category.name === fCategory;
});
this.updateCategories();
});
}
initCategories() {
this.categories = [
{ name: 'dress', checked: true, displayName: 'Dresses' },
{ name: 'top', checked: true, displayName: 'Shirts' },
{ name: 'skirt', checked: true, displayName: 'Skirts/Pants' },
{ name: 'purse', checked: true, displayName: 'Purse' },
{ name: 'bag', checked: true, displayName: 'Bags' },
];
}
updateCategories() {
const categories = this.categories
.filter((category) => {
return category.checked;
});
console.log(categories);
this.filteredCategory.emit(categories);
}
in the console at first I get the correct result
but then categories array resets
[{}]
{name: "top", checked: true, displayName: "Shirts"}
[{…}, {…}, {…}, {…}, {…}]
{name: "dress", checked: true, displayName: "Dresses"}
1: {name: "top", checked: true, displayName: "Shirts"}
2: {name: "skirt", checked: true, displayName: "Skirts/Pants"}
3: {name: "purse", checked: true, displayName: "Purse"}
4: {name: "bag", checked: true, displayName: "Bags"}
length: 5
the Observable in ShopService
filterCategories = new BehaviorSubject("category");

I owe this answer to #Józef Podlecki for another question that he answered of mine.
I need to use a BehaviorSubject instead of a regular subject in the ShopService
filterCategories = new BehaviorSubject("all");
Filter Bar Component
ngOnInit() {
this.initCategories();
this.filterCategories();
}
filterCategories() {
this.shopService.filterCategories.subscribe((fCategory: string) => {
if (fCategory === 'all') {
this.updateCategories();
} else {
this.categories.map((category) => {
category.checked = category.name === fCategory;
});
this.updateCategories();
}
});
}

Related

How to handle the change of the previous object

I'm currently making a nav bar.
I create an array storing the info of the tabs in the nav bar.
tabs: [
{ name: 'All',id: "dash.courses.all", to: 'all', current: false },
{ name: 'Self-paced Learning',id: "dash.courses.oneToOneClasses", to: 'selfPacedLearningComp', current: false },
{ name: '30 Days Challenge',id: "dash.courses.selfPacedLearningComp", to: 'thirtyDaysChallenge', current: false },
{ name: 'Group Classes',id: "dash.courses.groupClasses", to: 'groupClasses', current: false },
{ name: '1-to-1 Classes',to: "dash.courses.thirtyDaysChallenge", to: 'oneToOneClasses', current: false },
]
When a new route is clicked it updates the newly clicked tab to allow the current property to be true.
How would you change the previous nav item to false. As currently they all change to true one by one as they are clicked.
I think if I store a value as previous
setCurrent(tab)
{
let newArr = this.tabs.map(obj => {
if (obj.id === tab) {
return {...obj, current: true};
}
return obj;
})
this.tabs = newArr
console.log(newArr)
}
},
This is what i've got atm, it has to go around 3 routes ahead till the one before vanishes...
<script>
export default {
components: {
},
data()
{
return {
prevTab: null,
tabs: [
{ name: 'All',id: "dash.courses.all", to: 'all', current: false },
{ name: 'Self-paced Learning',id: "dash.courses.oneToOneClasses", to: 'selfPacedLearningComp', current: false },
{ name: '30 Days Challenge',id: "dash.courses.selfPacedLearningComp", to: 'thirtyDaysChallenge', current: false },
{ name: 'Group Classes',id: "dash.courses.groupClasses", to: 'groupClasses', current: false },
{ name: '1-to-1 Classes',to: "dash.courses.thirtyDaysChallenge", to: 'oneToOneClasses', current: false },
]
}
},
methods: {
setCurrent(tab)
{
this.prevTab = this.$route.name
let newArr = this.tabs.map(obj => {
if (obj.id === tab) {
return {...obj, current: true};
}
if(obj.id === this.prevTab) {
return {...obj, current: false}
}
return obj;
})
console.log('previous ',this.prevTab)
console.log('route name', this.$route.name)
this.tabs = newArr
}
},
mounted()
{
this.prevTab = this.$route.name
const newArr = this.tabs.map(obj => {
if (obj.id === this.$route.name) {
return {...obj, current: true};
}
return obj;
});
this.tabs = newArr
}
}
Create a watcher on the route name
watch: {
"$route.name": {
handler(routeName) {
this.tabs.forEach((tab) => (tab.current = routeName === tab.name));
},
// force eager callback execution
immediate: true,
},
}
Usually you can just use the watcher routeName value above to run whatever side effect you want but if tracking current on each tab is really necessary the above code will get the job done.

Fast access to json tree data structure

I have a reducer which holds tree data structure (more then 100_000 items total). This is what the data looks like
[
{
text: 'A',
expanded: false,
items:
[
{
text: 'AA',
expanded: false
},
{
text: 'AB',
expanded: false,
items:
[
{
text: 'ABA',
expanded: false,
},
{
text: 'ABB',
expanded: false,
}
]
}
]
},
{
text: 'B',
expanded: false,
items:
[
{
text: 'BA',
expanded: false
},
{
text: 'BB',
expanded: false
}
]
}
]
What I need to do is access this items really fast using text as an id (need to toggle expanded each time user clicks on item in a treeview). Should I just copy whole structure in to dictionary or is there a better way?
Maybe the following will help, let me know if you need more help but please create a runnable example (code snippet) that shows the problem:
const items = [
{
text: 'A',
expanded: false,
items: [
{
text: 'AA',
expanded: false,
},
{
text: 'AB',
expanded: false,
items: [
{
text: 'ABA',
expanded: false,
},
{
text: 'ABB',
expanded: false,
},
],
},
],
},
{
text: 'B',
expanded: false,
items: [
{
text: 'BA',
expanded: false,
},
{
text: 'BB',
expanded: false,
},
],
},
];
//in your reducer
const mapItems = new Map();
const createMap = (items) => {
const recur = (path) => (item, index) => {
const currentPath = path.concat(index);
mapItems.set(item.text, currentPath);
//no sub items not found in this path
if (!item.items) {
return;
}
//recursively set map
item.items.forEach(recur(currentPath));
};
//clear the map
mapItems.clear();
//re create the map
items.forEach(recur([]));
};
const recursiveUpdate = (path, items, update) => {
const recur = ([current, ...path]) => (item, index) => {
if (index === current && !path.length) {
//no more subitems to change
return { ...item, ...update };
}
if (index === current) {
//need to change an item in item.items
return {
...item,
items: item.items.map(recur(path)),
};
}
//nothing to do for this item
return item;
};
return items.map(recur(path));
};
const reducer = (state, action) => {
//if you set the data then create the map, this can make
// testing difficult since SET_ITEM works only when
// when you call SET_DATA first. You should not have
// side effects in your reducer (like creating the map)
// I broke this rule in favor of optimization
if (action.type === 'SET_DATA') {
createMap(action.payload); //create the map
return { ...state, items };
}
if (action.type === 'SET_ITEM') {
return {
...state,
items: recursiveUpdate(
mapItems.get(action.payload.text),
state.items,
action.payload
),
};
}
return state;
};
//crate a state
const state = reducer(
{},
{ type: 'SET_DATA', payload: items }
);
const changed1 = reducer(state, {
type: 'SET_ITEM',
payload: { text: 'A', changed: 'A' },
});
const {
items: gone,
...withoutSubItems
} = changed1.items[0];
console.log('1', withoutSubItems);
const changed2 = reducer(state, {
type: 'SET_ITEM',
payload: { text: 'ABB', changed: 'ABB' },
});
console.log('2', changed2.items[0].items[1].items[1]);
const changed3 = reducer(state, {
type: 'SET_ITEM',
payload: { text: 'BA', changed: 'BA' },
});
console.log('3', changed3.items[1].items[0]);
If all you wanted to do is toggle expanded then you should probably do that with local state and forget about storing expanded in redux unless you want to expand something outside of the component that renders the item because expanded is then shared between multiple components.
I think you may mean that the cost of handling a change of expansion is really high (because potentially you close/open a node with 100000 leaves and then 100000 UI items are notified).
However, this worries me as I hope only the expanded UI items exist at all (e.g. you don't have hidden React elements for everything, each sitting there and monitoring a Redux selector in case its part of the tree becomes visible).
So long as elements are non-existent when not expanded, then why is expansion a status known by anything but its immediate parent, and only the parent if it's also on screen?
I suggest that expansion state should be e.g. React state not Redux state at all. If they are on screen then they are expanded, optionally with their children expanded (with this held as state within the parent UI element) and if they are not on screen they don't exist.
Copy all the individual items into a Map<id, Node> to then access it by the ID.
const data = []// your data
// Build Map index
const itemsMap = new Map();
let itemsQueue = [...data];
let cursor = itemsQueue.pop();
while (cursor) {
itemsMap.set(cursor.text, cursor);
if (cursor.items)
for (let item of cursor.items) {
itemsQueue.push(item);
}
cursor = itemsQueue.pop();
}
// Retrieve by text id
console.log(map.get('ABB'));
// {
// text: 'ABB',
// expanded: false,
// }

JavaScript, looping, and functional approach

Data Structure coming back from the server
[
{
id: 1,
type: "Pickup",
items: [
{
id: 1,
description: "Item 1"
}
]
},
{
id: 2,
type: "Drop",
items: [
{
id: 0,
description: "Item 0"
}
]
},
{
id: 3,
type: "Drop",
items: [
{
id: 1,
description: "Item 1"
},
{
id: 2,
description: "Item 2"
}
]
},
{
id: 0,
type: "Pickup",
items: [
{
id: 0,
description: "Item 0"
},
{
id: 2,
description: "Item 2"
}
]
}
];
Each element represents an event.
Each event is only a pickup or drop.
Each event can have one or more items.
Initial State
On initial load, loop over the response coming from the server and add an extra property called isSelected to each event, each item, and set it as false as default. -- Done.
This isSelected property is for UI purpose only and tells user(s) which event(s) and/or item(s) has/have been selected.
// shove the response coming from the server here and add extra property called isSelected and set it to default value (false)
const initialState = {
events: []
}
moveEvent method:
const moveEvent = ({ events }, selectedEventId) => {
// de-dupe selected items
const selectedItemIds = {};
// grab and find the selected event by id
let foundSelectedEvent = events.find(event => event.id === selectedEventId);
// update the found event and all its items' isSelected property to true
foundSelectedEvent = {
...foundSelectedEvent,
isSelected: true,
items: foundSelectedEvent.items.map(item => {
item = { ...item, isSelected: true };
// Keep track of the selected items to update the other events.
selectedItemIds[item.id] = item.id;
return item;
})
};
events = events.map(event => {
// update events array to have the found selected event
if(event.id === foundSelectedEvent.id) {
return foundSelectedEvent;
}
// Loop over the rest of the non selected events
event.items = event.items.map(item => {
// if the same item exists in the selected event's items, then set item's isSelected to true.
const foundItem = selectedItemIds[item.id];
// foundItem is the id of an item, so 0 is valid
if(foundItem >= 0) {
return { ...item, isSelected: true };
}
return item;
});
const itemCount = event.items.length;
const selectedItemCount = event.items.filter(item => item.isSelected).length;
// If all items in the event are set to isSelected true, then mark the event to isSelected true as well.
if(itemCount === selectedItemCount) {
event = { ...event, isSelected: true };
}
return event;
});
return { events }
}
Personally, I don't like the way I've implemented the moveEvent method, and it seems like an imperative approach even though I'm using find, filter, and map.
All this moveEvent method is doing is flipping the isSelected flag.
Is there a better solution?
Is there a way to reduce the amount of looping? Maybe events should be an object and even its items. At least, the lookup would be fast for finding an event, and I don't have to use Array.find initially. However, I still have to either loop over each other non selected events' properties or convert them back and forth using Object.entries and/or Object.values.
Is there more a functional approach? Can recursion resolve this?
Usage and Result
// found the event with id 0
const newState = moveEvent(initialState, 0);
// Expected results
[
{
id: 1,
type: 'Pickup',
isSelected: false,
items: [ { id: 1, isSelected: false, description: 'Item 1' } ]
}
{
id: 2,
type: 'Drop',
// becasue all items' isSelected properties are set to true (even though it is just one), then set this event's isSelected to true
isSelected: true,
// set this to true because event id 0 has the same item (id 1)
items: [ { id: 0, isSelected: true, description: 'Item 0' } ]
}
{
id: 3,
type: 'Drop',
// since all items' isSelected properties are not set to true, then this should remain false.
isSelected: false,
items: [
{ id: 1, isSelected: false, description: 'Item 1' },
// set this to true because event id 0 has the same item (id 2)
{ id: 2, isSelected: true, description: 'Item 2' }
]
}
{
id: 0,
type: 'Pickup',
// set isSelected to true because the selected event id is 0
isSelected: true,
items: [
// since this belongs to the selected event id of 0, then set all items' isSelected to true
{ id: 0, isSelected: true, description: 'Item 0' },
{ id: 2, isSelected: true, description: 'Item 2' }
]
}
]
One of the problems with the current solution is data duplication. You are basically trying to keep the data between the different items in sync. Instead of changing all items with the same id, make sure there are no duplicate items by using an approach closer to what you would find in a rational database.
Let's first normalize the data:
const response = [...]; // data returned by the server
let data = { eventIds: [], events: {}, items: {} };
for (const {id, items, ...event} of response) {
data.eventIds.push(id);
data.events[id] = event;
event.items = [];
for (const {id, ...item} of items) {
event.items.push(id);
data.items[id] = item;
}
}
This should result in:
const data {
eventIds: [1, 2, 3, 0], // original order
events: {
0: { type: "Pickup", items: [0, 2] },
1: { type: "Pickup", items: [1] },
2: { type: "Drop", items: [0] },
3: { type: "Drop", items: [1, 2] },
},
items: {
0: { description: "Item 0" },
1: { description: "Item 1" },
2: { description: "Item 2" },
},
};
The next thing to realize is that the isSelected property of an event is computed based on the isSelected property of its items. Storing this would mean more data duplication. Instead calculate it though a function.
const response = [{id:1,type:"Pickup",items:[{id:1,description:"Item 1"}]},{id:2,type:"Drop",items:[{id:0,description:"Item 0"}]},{id:3,type:"Drop",items:[{id:1,description:"Item 1"},{id:2,description:"Item 2"}]},{id:0,type:"Pickup",items:[{id:0,description:"Item 0"},{id:2,description:"Item 2"}]}];
// normalize incoming data
let data = { eventIds: [], events: {}, items: {} };
for (const {id, items, ...event} of response) {
data.eventIds.push(id);
data.events[id] = event;
event.items = [];
for (const {id, ...item} of items) {
event.items.push(id);
data.items[id] = item;
item.isSelected = false;
}
}
// don't copy isSelected into the event, calculate it with a function
const isEventSelected = ({events, items}, eventId) => {
return events[eventId].items.every(id => items[id].isSelected);
};
// log initial data
console.log(data);
for (const id of data.eventIds) {
console.log(`event ${id} selected?`, isEventSelected(data, id));
}
// moveEvent implementation with the normalized structure
const moveEvent = (data, eventId) => {
let { events, items } = data;
for (const id of events[eventId].items) {
items = {...items, [id]: {...items[id], isSelected: true}};
}
return { ...data, items };
};
data = moveEvent(data, 0);
// log after data applying `moveEvent(data, 0)`
console.log(data);
for (const id of data.eventIds) {
console.log(`event ${id} selected? `, isEventSelected(data, id));
}
// optional: convert structure back (if you still need it)
const convert = (data) => {
const { eventIds, events, items } = data;
return eventIds.map(id => ({
id,
...events[id],
isSelected: isEventSelected(data, id),
items: events[id].items.map(id => ({id, ...items[id]}))
}));
};
console.log(convert(data));
Check browser console, for better ouput readability.
I'm not sure if this answers solves your entire problem, but I hope you got something useful info out of it.

Rendering React/Redux app multiple times on a single page?

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 }
}

apply multiple filters react

I have 2 buttons which when clicked should filter by novelty or offer , I am able to make it so that when novelty is clicked it will filter by this but I am unable to make it so that if both are click it will filter by both novelty and offer
How can I make it so that when both novelty and offer are clicked it will filter by both of these?
https://www.webpackbin.com/bins/-KpVGNEN7ZuKAFODxuER
import React from 'react'
export default class extends React.Component {
constructor() {
super()
this.state = {
products: [
{ id: 1, novelty: true, offer: false, name: 'test1' },
{ id: 2, novelty: true, offer: true, name: 'test2' },
{ id: 3, novelty: false, offer: true, name: 'test3' }
],
display: 'all',
filters: [
{novelty:'true'},
{offer: 'true'}
]
}
}
setCategory (category) {
this.setState({
display: category
});
}
render() {
return(
<div>
<button onClick={()=>this.setCategory(true)}>Akce</button>
<button onClick={()=>this.setCategory(true)}>Offer</button>
{
this.state.products.filter( product =>
products.offer === this.state.display ||
this.state.display==='all')
.map(product =>
<div>{product.name}</div>
)
}
</div>
)
}
}
Here is the final version I've come up with:
import React from 'react'
export default class extends React.Component {
constructor() {
super()
this.state = {
products: [
{ id: 1, novelty: true, offer: false, name: 'test1' },
{ id: 2, novelty: true, offer: true, name: 'test2' },
{ id: 3, novelty: false, offer: true, name: 'test3' }
],
filters: {
novelty: true,
offer: true
}
}
}
setCategory (category) {
this.setState((state) => ({
filters: Object.assign({}, state.filters, { [category]: !state.filters[category] })
}));
}
render() {
console.log(this.state.filters)
return(
<div>
<button onClick={()=>this.setCategory('novelty')}>Akce</button>
<button onClick={()=>this.setCategory('offer')}>Offer</button>
{ this.state.products
.filter(product => product.novelty === this.state.filters.novelty || product.offer === this.state.filters.offer)
.map(product =>
<div key={product.id}>{product.name}</div>
)}
</div>
)
}
}
https://www.webpackbin.com/bins/-KpVHqfkjeraq6pGvHij
A few things:
Using a boolean instead of a string in your case is more adapted. (true instead of 'true').
display: 'all' isn't required for your use case. You can compute this value from your filters if you need to.
setCategory receive which category you want to set as a param.
I would rename setCategory to setFilter
Also, I'm using the asycnhronous version of setState. This allows you to hand in a function.
this.setState((state) => ({
filters: Object.assign({}, state.filters, { [category]: !state.filters[category] })
}));
Here I'm using Object.assign to create a new Object. I populate him with state.filters and finally I update the filter you want to.
category will either be novelty or offer and thanks to that I'm using the shorthand version of [category].
To conclude, I also update your filter function to check the product.novelty against the filter.novelty or the product.offer with the filter.offer

Categories

Resources