Issue with saving an element with localStorage.

https://codepen.io/bsups/pen/bGQopLm I am able to create the element, append it to correct area, save it to the state object, and logging state shows the element in the correct array in the object. When I reload to see if element is still there it just returns as [object Object]. Am i unable to save object of arrays as localstorage?
Brandon
CodePen
bGQopLm
...
43 Replies
Joao
Joao2y ago
Am i unable to save object of arrays as localstorage?
Correct. Local storage can save strings only. The usual procedure to store any other data structure (objects, arrays) is to use JSON.stringifiy to save the object as a string representation. Use JSON.parse to go the other way around: read the string from local storage and turn it into an object.
Errtu
ErrtuOP2y ago
I am currently using JSON.parse(localStorage.getItem("state")) and localStorage.setItem("state", JSON.stringify(state)); my state object looks like const state = JSON.parse(localStorage.getItem("state")) || { stories: ["Stories"], toDo: ["todo"], inProgress: ["inprogress"], test: ["test"], done: ["done"], };
Joao
Joao2y ago
The issue is not with localStorage, but with how you are trying to use it. You cannot save a DOM node inside of it, so it just defaults to an empty object {}. However, that's not how you want to do things anyway. Store the information relevant to build that node element from scratch instead. You also don't want to mix simple strings to be used as section title and the data associated to it in the same array. Keep things separated.
// Separate things a bit to emphasize and make clear this is the deafult state.
// Each section stores only the data needed to re-build the nodes when
// application loads.
const defaultState = {
stories: [],
toDo: [],
inProgress: [],
test: [],
done: [],
};

const state = JSON.parse(localStorage.getItem("state")) || defaultState;

// Later, it would look something like this
const state = {
stories: [
{
id: 'somerandomid',
title: 'Task1',
createdBy: 'You'
},
{
id: 'anotherrandomid',
title: 'Task2',
createdBy: 'Me'
}
]
};
// Separate things a bit to emphasize and make clear this is the deafult state.
// Each section stores only the data needed to re-build the nodes when
// application loads.
const defaultState = {
stories: [],
toDo: [],
inProgress: [],
test: [],
done: [],
};

const state = JSON.parse(localStorage.getItem("state")) || defaultState;

// Later, it would look something like this
const state = {
stories: [
{
id: 'somerandomid',
title: 'Task1',
createdBy: 'You'
},
{
id: 'anotherrandomid',
title: 'Task2',
createdBy: 'Me'
}
]
};
You would then loop over each of the state's array holding this data, and re-create the html elements from that every time. This also has the advantage of being easy to read for you, if you ever need to export/import this data into/from another format like XML, JSON, CSV, etc.
Jochem
Jochem2y ago
I agree with everything Joao is saying, arrays should only contain one type of data, and you should only store the data in localstorage and then rebuild your dom yourself To clarify not being able to store a DOM node in local storage: JSON.stringify turns an object's basic properties into a string representation, but methods are completely lost. DOM node objects are more complex. The methods get lost, and I think they might use getters rather than plain properties, which makes serializing them impossible. Stringifying document.body produces this:
{
"sizzle-1688727523374":
{
"parentNode":[868,0,true]
}
}
{
"sizzle-1688727523374":
{
"parentNode":[868,0,true]
}
}
Joao
Joao2y ago
Really? for me it just gives me an empty object. On Firefox, but on Brave it gives me a lot of data yeah But not very usable either way, it seems pretty much everything is lost and only some bits remain
Jochem
Jochem2y ago
this is on firefox for me
Joao
Joao2y ago
Btw, you may want to add unique ids to each task so that you can later find and target them specifically. Other pieces of interesting data are creation date, modified date for filtering and sorting. MM well, not very useful anyways so 🤷‍♂️
Jochem
Jochem2y ago
yeah, true
Errtu
ErrtuOP2y ago
Thank you both. Looks like this project will be out of my scope.
Joao
Joao2y ago
Not at all, you already have most of the functions you need in place. You just need to refactor things a bit. You can re-create the tasks in the exact same fashion you are creating the prompt to enter the new task:
state.forEach((taskStatus) => {
taskStatus.forEach((task) => {
const el = document.createElement("p");
el.classList.add("item__text");
el.innerText = task.title;
el.id = task.id;
taskStatus.appendChild(el);
});
});
state.forEach((taskStatus) => {
taskStatus.forEach((task) => {
const el = document.createElement("p");
el.classList.add("item__text");
el.innerText = task.title;
el.id = task.id;
taskStatus.appendChild(el);
});
});
If you need additional wrappers for the task, to give it some styles for example, you can add them here as well. In fact, I would recommend creating a dedicated function for this. For example, it can accept the task object and the container it belongs to:
state.forEach((taskStatus) => {
taskStatus.forEach((task) => {
createTaskNode(task, taskStatus);
});
});
state.forEach((taskStatus) => {
taskStatus.forEach((task) => {
createTaskNode(task, taskStatus);
});
});
Or, you might want to have this createTaskNode function return the html element instead of appending directly to the DOM. This can be handy later on for things like editing. In which case this may looks something like:
state.forEach((taskStatus) => {
taskStatus.forEach((task) => {
const taskNode = createTaskNode(task);
taskStatus.appendChild(taskNode);
});
});
state.forEach((taskStatus) => {
taskStatus.forEach((task) => {
const taskNode = createTaskNode(task);
taskStatus.appendChild(taskNode);
});
});
Either way you came too far to give up now 😄
Errtu
ErrtuOP2y ago
Nah I had everything working except this. But this changes a lot of other code. Need to figure out how to update each state section when an element leaves and enters a different boards, and it the correct order to match in its order as seen. I really do appreciate the help. Now I know you can’t save elements with LS. I’ll work on something else and come back once I know more.
Errtu
ErrtuOP2y ago
@joao6246 I decided to try this, it wont let me use a forEach loop over state. I had to loop over by each objects array. state[addTo.value].forEach((item) => { createNewItem(item.task, item.creator, item.board, item.id); }); https://codepen.io/bsups/pen/BaGwrEy I can get it to work off desktop, but it doesnt populate the element in codepen
Brandon
CodePen
BaGwrEy
...
Errtu
ErrtuOP2y ago
This is shortest I could come up with to loop over the state object. No idea why id doesnt load in the codepen browser. const iterateOverState = (board) => { state[board].forEach((item) => { createNewItem(item.task, item.creator, item.board, item.id); }); }; const loadItemHTML = () => { iterateOverState("stories"); iterateOverState("toDo"); iterateOverState("inProgress"); iterateOverState("test"); iterateOverState("done"); }; loadItemHTML();
Joao
Joao2y ago
Ah, yes you need to convert the state object into an iterator using one of Object.keys, Object.values or Object.entries. What I wrote above was more like pseudo-code to relay the idea, so I didn't pay attention to the details. (Sorry I was in the middle of writing this but had to go, I will try tomorrow to give a better response)
Errtu
ErrtuOP2y ago
Thank you, for all the assistance and talking me through it. Probably done working on it this afternoon. I was able to change it a lot to fit what you said. All I have left it figuring out how to remove the elements data from its current state location, on drag start, then moving it to its new locations state on drag end. Only issue I have run into is. I put 3 test entries in each array, and the first one stories, gets all 18 elements added to it, but the other 4 only get their designated 3 as seen in the image. Still not sure why the code pen doesn't populate the state. https://codepen.io/bsups/pen/GRwMGgW https://github.com/bsupinski/Kanban-Board https://bsupinski.github.io/Kanban-Board/
Brandon
CodePen
GRwMGgW
...
GitHub
GitHub - bsupinski/Kanban-Board: Kanban board
Kanban board. Contribute to bsupinski/Kanban-Board development by creating an account on GitHub.
Joao
Joao2y ago
That seems to come from line 144 inside the create new item function, you probably forgot to delete that line. I would also recommend to simplify the number of parameters provided to createNewItem, currently this seems redundant:
const iterateOverState = (board) => {
state[board].forEach((item) => {
createNewItem(item.task, item.creator, item.board, item.id);
});
};
const iterateOverState = (board) => {
state[board].forEach((item) => {
createNewItem(item.task, item.creator, item.board, item.id);
});
};
Would probably be best to just give the item directly. This way you also avoid another subtle problem: the third parameter refers to the item's board but inside that funciton you refer to that as "value" so it's hard to keep track of that.
const iterateOverState = (board) => {
state[board].forEach((item) => {
createNewItem(item);
});
};
const iterateOverState = (board) => {
state[board].forEach((item) => {
createNewItem(item);
});
};
This simplifies the way you call this function since it's a single parameter. This has two additional advantages 1) You are now forced to use the correct property names of the item object, so the code is immediately much easier to read and understand 2) You can use destructuring:
function createNewItem({ task, creator, board, id }) {}
function createNewItem({ task, creator, board, id }) {}
So even on the function definition this is now much simpler since you can extract property names in any order, and only the properties that you actually need in this function (in this case it's all of them, but with a larger object may not have been the case). I think it's a bit redundant that that each item keeps track of which board it belongs to. The state object already has that information implicitly and you can use that to further simplify things in your code:
const iterateOverState = (board) => {
state[board].forEach((item) => {
createNewItem(item, board);
});
};

function createNewItem({ task, creator, board, id }, board) {

// ...

// boards.forEach((board) => {
// if (board.classList.contains(`${value}`)) board.append(itemWrapper);
// });

board.appendChild(itemWrapper);
}
const iterateOverState = (board) => {
state[board].forEach((item) => {
createNewItem(item, board);
});
};

function createNewItem({ task, creator, board, id }, board) {

// ...

// boards.forEach((board) => {
// if (board.classList.contains(`${value}`)) board.append(itemWrapper);
// });

board.appendChild(itemWrapper);
}
No need to check all boards for all items and, incidentally, no need to add ambiguous CSS classes on the html (what that the class "onProgress" mean). To iterate over an object's keys / values you can apply one of these methods:
const loadItemHTML = () => {
// iterateOverState("stories");
// iterateOverState("toDo");
// iterateOverState("inProgress");
// iterateOverState("test");
// iterateOverState("done");

// Option 1:
// for (const board in state) {
// state[board].forEach(item => {
// createNewItem(item.task, item.creator, item.board, item.id);
// });
// }

// Option 2:
// Object.keys(state).forEach(board => {
// state[board].forEach(item => {
// createNewItem(item.task, item.creator, item.board, item.id);
// });
// });

// Option 3:
// Object.values(state).forEach(board => {
// board.forEach(item => {
// createNewItem(item.task, item.creator, item.board, item.id);
// });
// });
};
const loadItemHTML = () => {
// iterateOverState("stories");
// iterateOverState("toDo");
// iterateOverState("inProgress");
// iterateOverState("test");
// iterateOverState("done");

// Option 1:
// for (const board in state) {
// state[board].forEach(item => {
// createNewItem(item.task, item.creator, item.board, item.id);
// });
// }

// Option 2:
// Object.keys(state).forEach(board => {
// state[board].forEach(item => {
// createNewItem(item.task, item.creator, item.board, item.id);
// });
// });

// Option 3:
// Object.values(state).forEach(board => {
// board.forEach(item => {
// createNewItem(item.task, item.creator, item.board, item.id);
// });
// });
};
This is more programatically, no need to specify which key you need to pass and risk spelling mistakes or needless updates in multiple places later on if you need to change the name of a board, or add more boards, etc.
Errtu
ErrtuOP2y ago
Thank you. I actually tried the first two yesterday but forgot to reference the state first in their second line. Couldn't figure out why it was matching to documentation i was looking at but not working. I think I did the board: addTo.value to maybe use when I try figuring out items changing state when they drag and drop into a new board. But will not be needing it I believe. Thank you again. @joao6246 hmmm how does it know what board it should be appended to. The board the function is referencing to is the state section I think
Joao
Joao2y ago
Yes, that's right it's the state object. If you consider just the first task:
const defaultState = {
stories: [
{
task: "test1story",
creator: "test1story",
board: "stories",
id: Math.floor(Math.random() * 100000),
},
]
};
const defaultState = {
stories: [
{
task: "test1story",
creator: "test1story",
board: "stories",
id: Math.floor(Math.random() * 100000),
},
]
};
It's clear that this task belongs to stories, to it's redundant to store a property to have this information. And the only time that you access this task is when you are looping over the state object, and therefore by the time you have a reference to the task you also must have a reference to the board in belongs to. Notice how on all of the examples above on how to loop over an object's properties, you always have a reference to the board and the task (called item).
Errtu
ErrtuOP2y ago
when i try board.appendChild(itemWrapper) i get appendChild is not a function
Joao
Joao2y ago
So with a slight modification on the create item function, many parts of the code can be simplified. Are you using this function declaration: function createNewItem({ task, creator, board, id }, board) {? I just noticed a big issue on this one, that would explain the problem you are having, did you spot it?
Errtu
ErrtuOP2y ago
The issue i think it is having is elements i want them to be a child of or not being reference it is assuming state is state.board err the board is state.board
Joao
Joao2y ago
Ah I see, I made another mistake. Well first of all the issue on the function declaration above is that I'm repeating board twice but that's not the real problem. And yes, you were right, I thought it would refer to the state directly however it needs to find the correct node in the DOM to append to, but you can use that same piece of information either way. document.getElementById(boardCategory).appendChild(itemWrapper);
Errtu
ErrtuOP2y ago
I think can just somehow get the state.board idex and do a boards[i].appenchilld index
Joao
Joao2y ago
Yes, something like that would work as well. I noticed you used ids on the HTML so I decided to use that.
const createNewItem = ({ task, creator, board, id }, boardCategory) => {
// ...
document.getElementById(boardCategory).appendChild(itemWrapper);
};
const createNewItem = ({ task, creator, board, id }, boardCategory) => {
// ...
document.getElementById(boardCategory).appendChild(itemWrapper);
};
Errtu
ErrtuOP2y ago
Another issue I think I may have down the line, is I will need to clear out all items of each node, since I iterate over the whole state when i create a new task. Is that bad practice to clear and reset?
Joao
Joao2y ago
That's a good observation, and no that's pretty much how you'd need to do it (and how frameworks like React do it as well, only with more fancy behind the scenes magic to make things efficient).
Errtu
ErrtuOP2y ago
I imagine with bigger state, a user could see a delay as it processes the creation.
Joao
Joao2y ago
The main idea is that you some state and you mimic that state in DOM. Then, you make changes to the state which you have to reflect again on the DOM. So, you don't manipulate the DOM directly, it has to happen in the state first. Yes, that's right. In practice however, this is only noticeable with a lot of changes and/or animations happening all at once. And that's precisely what frameworks are optimized for, making things very efficient. For example by only applying the changes exactly where needed. In case you are wondering, it is a tradeoff between performance for the benefit of readability. But premature optimization is not a very good idea, best to get things working first, and then see if and where optimizations are in order.
Errtu
ErrtuOP2y ago
Understandable. I dont see where boardCategory is referencing
Joao
Joao2y ago
I had to change the name to avoid repeating "board" as an argument, but you could actually not use that at all if you don't want to and just use the task's board property:
const createNewItem = ({ task, creator, board, id }) => {
// ...
document.getElementById(board).appendChild(itemWrapper);
};
const createNewItem = ({ task, creator, board, id }) => {
// ...
document.getElementById(board).appendChild(itemWrapper);
};
Or, if you decide to provide it with the item and the board separately (meaning that you would remove the board property from each task object):
const createNewItem = ({ task, creator, id }, board) => {
// ...
document.getElementById(board).appendChild(itemWrapper);
};
const createNewItem = ({ task, creator, id }, board) => {
// ...
document.getElementById(board).appendChild(itemWrapper);
};
Errtu
ErrtuOP2y ago
Thank you that works. How would you get the index of the board in that loop? I know this way just give you the 3 index values of the items. Do you have to do it with looping over object.entries? const iterateOverState = () => { for (const board in state) { state[board].forEach((item,) => { createNewItem(item, i); }); } };
Joao
Joao2y ago
The second argument of forEach is the current index:
const iterateOverState = () => {
for (const board in state) {
state[board].forEach((item, idx) => {
createNewItem(item, idx);
});
}
};
const iterateOverState = () => {
for (const board in state) {
state[board].forEach((item, idx) => {
createNewItem(item, idx);
});
}
};
But I don't think you need it? Or are you planning on accessing the index on the boards variable?
Errtu
ErrtuOP2y ago
I messed up, meant to have it like const iterateOverState = () => { for (const board in state) { state[board].forEach((item, i) => { createNewItem(item, i); }); } }; But yes, when looping over the object how do you get the index of each board.
Joao
Joao2y ago
There is no index for object properties. The key itself, in this case board, is the "index". Using this for ... in loop turns it into an array so you can use that to extract the index from the forEach function, but there's no guarantee that the properties in the state are iterated over in the same order as they are written. Sorry, no, using Object.keys (or another variant) would turn it into an array.
Errtu
ErrtuOP2y ago
So i wouldnt be able to get the number index of each board in the state and pass it on to do boards[index].appendChild
Joao
Joao2y ago
No, you would have to query the DOM each time. In any case, you don't want to do it this way because if later you need to change things, that's two places you have to check: the HTML order and the state properties order (which is not guaranteed to be iterated in order any way, so it's not very relevant). Another option of course is to have each board section stored as a separate variable upfront.
Errtu
ErrtuOP2y ago
Ah so converting an object to an array can changes the order. I had that in an earlier version but didn't think it looked clean
Joao
Joao2y ago
If it helps, I think this app is a great candidate for OOP. Encapsulating each board can help with the logic since you only keep track of the corresponding DOM node. But I'm not sure if you are familiar with object oriented programming (classes, getters, setters, etc)
Errtu
ErrtuOP2y ago
I understand those somewhat, getters and setters still confused me in definition. I did a project with Module Controller View format few weeks ago. Base of what I wanted is done, still a work in project.
Errtu
ErrtuOP2y ago
GitHub
GitHub - bsupinski/weatherAPI: Weather API front end project using ...
Weather API front end project using weatherAPI. Contribute to bsupinski/weatherAPI development by creating an account on GitHub.
Joao
Joao2y ago
Don't worry about it then, you almost have it done anyway so this can be an exercise for some other time later on.
Errtu
ErrtuOP2y ago
Do you mean creating a base class, then a seperate class for each board?
Joao
Joao2y ago
Yeah, something like that. I guess it would look a bit like this (pseudo-code):
new Board({
title: 'Stories',
slug: 'stories',
background: 'red',
order: 1,
selector: 'section__stories'
});
new Board({
title: 'Stories',
slug: 'stories',
background: 'red',
order: 1,
selector: 'section__stories'
});
Just a quick thought, then each board could have it's own render method to "draw" it's inner state to the DOM. So each board only has to worry about its own thing, keeping thins separate and simple. It's very possible this approach is very much influenced by React, sort of mimicking how it's done by all the major frameworks.
Want results from more Discord servers?
Add your server