How to store DOM elements inside localStorage?

I’m trying to store created dom element references inside localStorage, so that when I visit the page again, they will be itterated over and then displayed. I tried using JSON.stringfy, but this didn’t work as when I retrieved the item from localStorage, I just got empty objects. Here is my code:

const createTaskElements = () => {
  const tasks = document.querySelector('#tasks');
  const task = document.createElement('div');
  const heading = document.createElement('h2');
  const details = document.createElement('p');
  const due = document.createElement('p');
  const priority = document.createElement('p');
  const button = document.createElement('button');

  return {
    tasks,
    task,
    heading,
    details,
    due,
    priority,
    button,
  };
};

const taskAttributes = (
  { task, heading, details, due, priority, button },
  folderID
) => {
  task.className = `task ${folderID}`;
  task.id = `task${count}`;

  heading.innerHTML = document.querySelector('#title').value;
  details.innerHTML = document.querySelector('#details').value;
  due.innerHTML = `<strong>Due:</strong>  ${
    document.querySelector('#date').value
  }`;
  priority.innerHTML = `<strong>priority:</strong>  ${
    document.querySelector('#priority').value
  }`;

  button.setAttribute('type', 'button');
  button.className = 'remove-task';
  button.innerHTML = '<i class="fas fa-times fa-2x"></i>';
};

const taskAppend = ({
  tasks,
  task,
  heading,
  details,
  due,
  priority,
  button,
}) => {
  task.appendChild(heading);
  task.appendChild(details);
  task.appendChild(due);
  task.appendChild(priority);
  task.appendChild(button);
  tasks.appendChild(task);
};
const createTask = (folderID) => {
  count += 1;

  const taskID = `task${count}`;
  const taskElements = createTaskElements();

  taskAttributes(taskElements, folderID);
  taskAppend(taskElements);

  foldersObj[folderID][taskID] = taskElements;
  localStorage.setItem(folderID, JSON.stringify(foldersObj[folderID]));
};

Well you wouldn’t stringify the elements themselves. You would need to store the attributes and type of element into something like an array or JavaScript object that you can then stringify. When reading it back out, you would read the string and rebuild the element using createElement etc.

Now you could use JSON for this or you could come up with your own “serialization scheme” to represent elements you want to save to and from, but ultimately it has to be a string and then you use that string to rebuild the object… probably manually.

Another idea would be to use something like a string <div attr='whatever'></div> and store that, then on the retrieval, dump that to an element’s .innerHTML property. Of course you have to then make sure never to allow the user the option to build their own elements and have it inserted into the page as it might pose a security risk.

But I hope you get the idea.

2 Likes

The code seems unnecessarily complicated. Here is my approach (you can add more than one task):

The localStorage simply stores the innerHTML so it can be displayed when the page is visited again.

I have not included functionality for more than one “folder” of tasks.

I might have the wrong end of the stick here, but is the data coming from a form? If not could the inputs be wrapped in a form?

If so, then you could create an object of name/value pairs using the FormData object

const taskForm = document.querySelector('#taskForm') <-- form element

// inside of handler store this?
const tasks = Object.fromEntries(new FormData(taskForm))

Then I would have thought you could build your html dynamically using that stored data.

Example codepen

3 Likes

All these ideas are essentially the same thing, build up some kind of representation of the elements you want to see on the other side and then after retrieval, build the elements you need to put into the page. The point here is that localStorage stores strings and not elements, not objects etc. How you represent that data is up to you. I think the others here have shown you a variety of ways to build such a representation and then getting at those values on the other side to dynamically build out the HTML.

But again, I want to stress, is that don’t blindly trust all the data stored in localStorage and make sure you always validate it because these values can be manipulated by the user. :slight_smile:

2 Likes

unfortunately, this approach doesn’t work, when you try to append it to tasks div or set it to innerHTML of tasks div.

<!-- <div class="task" id="task1">
            <h2>Title</h2>
            <p>
              Lorem ipsum dolor sit, amet consectetur adipisicing elit.
              Architecto praesentium ex
            </p>
            <p><strong>Due:</strong></p>
            <p><strong>Priority:</strong></p>
            <button class="remove-task">
              <i class="fas fa-times fa-2x"></i>
            </button>
          </div> -->
        <div class="task" id="task1"><h2>dfsdf</h2><p> dsfs</p><p><strong>Due:</strong>  2021-05-17</p><p><strong>priority:</strong>  high</p><button type="button" class="remove-task"><i class="fas fa-times fa-2x"></i></button></div>

this is the result of console.log(localStorage.getItem(folderID));

Here I have added a function retrieve() that executes on page load, gets the “Folder1” item from localStorage and updates the innerHTML of the “tasks” div.

The added function also restores the on click functionality of the “Delete Task” buttons.

This version now updates localStorage after a task is deleted (I’ve included alerts so you can see when localStorage is updated).

After adding at least one task, you should be able to confirm it works by refreshing your browser or pasting the CodePen URL into another browser tab.

I have been toying around with a few ideas. Taking the formData approach into account and building the HTML using a separate template string — Possibly a more flexible approach

As Martyr2 mentioned the input does need to be sanitized, so I have been looking at DOMPurify as one option. The CDN can be found here

In an ideal world it would be nice to be able to pass a template sting into a function to be evaluated/parsed — unfortunately a template string is evaluated within the execution context it is defined in.
e.g.

//global execution context

const template = `<p>${somevalue}</p>` // throws error Uncaught ReferenceError: somevalue is not defined

// we never get to here
function parse (template, data) { .... }

parse(template, {...})

One way around this I could think of was to wrap the template in a function. That way it could be evaluated when needed.

<body>
  <div id='output'></div>
  <script src='https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.7/purify.min.js'></script>
  <script>
    // define the template as a separate piece of data
    // Due to a template string being immediately evaluated within it's context
    // I have wrapped it in a function so that it can be evaluated when required.
    const taskTemplate = ({id, taskTitle, task}) => (
      `
        <div id='${id}'>
            <h1>${taskTitle}</h1>
            <p>${task}</p>
        </div>
      `
    )

    // data that might come from localStorage or formData
    const data = {
      id: 'task1',
      taskTitle: 'Walk the Dog',
      task: 'Take Shep for a walk<img src=x onerror="alert(\'Malicious Attack\')">' // nasty code included
    }

    const { sanitize } = DOMPurify // For brevity DOMPurify.sanitze destructured

    // sanitize each data string value
    // returning a sanitized object
    const sanitizeData = (data, opts = {} /* see DOMPurify options */) =>
      Object.fromEntries(
        Object
          .entries(data)
          .map(([key, value]) => [key, sanitize(value, opts)])
      )

    // data safe to store and output?!
    const sanitizedData = sanitizeData(
      data,
      { ALLOWED_TAGS: [] } // remove tagged blocks from strings
    )

    output.innerHTML = taskTemplate(sanitizedData)
    /* output
      <div id="output">
        <div id="task1">
            <h1>Walk the Dog</h1>
            <p>Take Shep for a walk</p>
        </div>
      </div>
    */
  </script>
</body>

EJS Embedded Javascript Templating

A second idea. I have used pug and handlebars, but EJS is definitely my favourite of the bunch. Similar in a way to using PHP for templating. The link for the CDN can be found here

So very much the same as above with a couple of small differences

<body>
  <div id='output'></div>
  <script src='https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.7/purify.min.js'></script>
  <script src='https://cdn.jsdelivr.net/npm/ejs@3.1.6/ejs.min.js'></script>
  <script>
    // ejs template string
    // No need to wrap this in a function
    const taskTemplate =
      `
        <div id='<%= id %>'>
            <h1><%= taskTitle %></h1>
            <p><%= task %></p>
        </div>
      `
    // same as before
    const data = {
      id: 'task1',
      taskTitle: 'Walk the Dog',
      task: 'Take Satan for a walk<img src=x onerror="alert(\'Malicious Attack\')">'
    }

    const { sanitize } = DOMPurify

    const sanitizeData = (data, opts = {}) =>
      Object.fromEntries(
        Object
          .entries(data)
          .map(([key, value]) => [key, sanitize(value, opts)])
      )

    const sanitizedData = sanitizeData(data, { ALLOWED_TAGS: [] })

    // this time render the template with ejs
    output.innerHTML = ejs.render(taskTemplate, sanitizedData)

  </script>
</body>

Lastly a bit rusty when it comes to localStorage, but a quick test

const data = {
  id: 'task1',
  taskTitle: 'Walk the Dog',
  task: 'Take Shep for a walk'
}

localStorage.setItem('formData', JSON.stringify(data))

// output retrieved data
console.log(JSON.parse(localStorage.getItem('formData')))

// {id: "task1", taskTitle: "Walk the Dog", task: "Take Shep for a walk"}

Just some ideas. There maybe better alternatives :slight_smile:

I’m not convinced of the need to sanitise the data: any malicious content in localStorage will not affect anyone else’s computer. However if the task data is being uploaded to the server, then the sanitising should be done by PHP at the server. Also if uploaded to the server, why use localStorage at all?

Does that make sense? :confused:

Whilst I’m aware of avoiding client side risks, I have to say I’m not particularly knowledgeable on how these attacks are implemented.

That said a quick google and a read through the following article does give a better idea.

From the client side, fake password prompts, retrieving cookies and page re-directs are a few of the things which stand out from the above article.

Better to be safe both client side and server side I would have thought :slight_smile:

Just a thought but how about using a document fragment, which is an object, and stringily that.

Thanks for your suggestion.

Going back to the title of this thread “How to store DOM elements inside localStorage?”, I have been assuming that the requirement is to store DOM elements that are visible on the HTML page so they can be displayed when visiting the page again. In my view the simplest way is just to store the innerHTML of the DOM that contains the elements to be stored. There’s no need to convert to a JavaScript object and no need to convert back again when visiting the page again. Of course I may be missing something!

We don’t know the full functionality of the page so it may well still make sense to have task data as a JavaScript object for other purposes, such as to sort by due date or to sort by priority.

1 Like

I tend to let the DOM do things rather than do things myself. Appending a form element(I assume the values are in a form), to a document fragment is simpler and less error prone than creating an object and copying values to it. And appending the form from the fragment to the form’s parent element is simple.
But we don’t know the full story so I could be barking up the wrong tree.

Can you stringify a document fragment? I don’t think you can. I think the only option is to stringify the fragment’s innerHTML.

I’m not sure I am with you there. FormData will create a simple lightweight object of keys and values. No other noise to consider.

Just taking the simple example form from my codepen above

<form id='tasks'>
  <input type="text" id="title" name='title' placeholder="title">
  <input type="text" id="details" name="details" placeholder="details">
  <input type="date" id="due" name="due" placeholder="due">
  <input type="text" id="priority" name="priority" placeholder="priority">
</form>
<button id='create-task'>Create Task</button>

Going down your route of appending that form to a document fragment and console.dir what we are working with and proposing to stringify

#document-fragment
baseURI: "http://127.0.0.1:5501/formdata.html"
childElementCount: 1
> childNodes: NodeList [form#tasks]
> children: HTMLCollection [form#tasks, tasks: form#tasks]
> firstChild: form#tasks
> firstElementChild: form#tasks
isConnected: false
> lastChild: form#tasks
> lastElementChild: form#tasks
nextSibling: null
nodeName: "#document-fragment"
nodeType: 11
nodeValue: null
> ownerDocument: document
parentElement: null
parentNode: null
previousSibling: null
textContent: "\n    \n    \n    \n    \n  "
> __proto__: DocumentFragment

Bearing in mind there are hundreds if not a thousand or so child elements/properties to consider. Furthermore are they values? Are they livedata, references and can all the properties be copied? These are the sort of thoughts that go through my head anyway.

In the end I would say a bit more difficult to work with and less efficient than just storing simple data

{
    title: "Doctors Appointment", 
    details: "Get my vaccine", 
    due: "2021-05-04", 
    priority: "urgent"
}

I’m not saying I’m completely right, but looking at working models in node, react etc they appear to follow a pattern of passing data into html templates/components. Data tends to be a separate entity, which makes a lot of sense to me.