Refresh dataview on value change

I have a dropdown menu at the top of my table rendered like this:

// 'categories' is a simple key-value object
const selectElement = dv.el('select')
for (const key of Object.keys(categories)) {
    selectElement.appendChild(dv.el('option', key))
}
selectElement

I can then refer to that value further down in the code with selectElement.value. But how can I trigger a view refresh when the value in the dropdown is changed?

Thanks.

Managed to do this with selectElement.onchange = renderTable but the new view is appended to the bottom of the output rather than replacing it. Is there a method to clear the view before rendering a table?

Maybe try container.lastChild.remove();?

This gives :

ReferenceError: container is not defined
    at renderTable (eval at <anonymous> (plugin:dataview)```

Can you share the full codeblock? It’s hard to debug without seeing the whole thing

Sure. Most of it will be irrelevant here I believe, but here it is anyway. It works as expected, except that the select element appends the new view of the table instead of replacing it.

// Map of tags to icons
const categories = {
    '#nixon': '📅',
    '#usa': '🔺',
    '#ussr': '🔹',
    '#johnson': '🔸',
}

// Timeline sections (start and end years)
const sections = [
    ['0', '1900'],
    ['1900', '1930'],
    ['1930', '1940'],
    ['1940', '1950'],
    ['1950', '2025'],
]

const selectElement = dv.el('select')

// Return file link with icon if size is larger than threshold
const getEventLink = (e) =>
    e.file.size > 150
    ? "<span class=\"more-text\">" + e.file.link + "</span>"
    : e.file.link

// Return tags in event as string of icons
const getEventIcon = (e) => {
    let output = ''
    for (const [tag, icon] of Object.entries(categories)) {
        if (e.file.tags.includes(tag)) {
            output += icon
        }
    }
    return output
}

// Render the table
const renderTable = () => {
    // Loop over sections, returning header and table
    for (const section of sections) {
    
        // Don't print section header for "0" (zero)
        section[0] == '0' ? null : dv.header(1, section[0])
        
        // Return events table between the section dates
        dv.table(['', 'Date', 'Event'],
            dv.pages('"events"')
                .where(e =>
                    String(e.date) >= section[0]
                    && String(e.date) < section[1]
                    && e.file.tags.includes(selectElement.value)
                )
                .sort(e => String(e.date))
                .map(e => [
                    getEventIcon(e),
                    e.date,
                    getEventLink(e)
                ]
            )
        )
    }
}

// Dropdown
for (const key of Object.keys(categories)) {
    const option = dv.el('option', key)
    selectElement.appendChild(option)
}

const dropdown = dv.el('span', 'Category:')
dropdown.appendChild(selectElement)

selectElement.onchange = renderTable

dropdown
renderTable()

Try this. It should work, though it’s not the prettiest. My original plan was to add each header and table to a span element, then drop it all at once.

However, dv.table does not return an html element, and I couldn’t figure out how to make it into one without it returning a promise.

```dataviewjs
// Map of tags to icons
const categories = {
    '#nixon': '📅',
    '#usa': '🔺',
    '#ussr': '🔹',
    '#johnson': '🔸',
}

// Timeline sections (start and end years)
const sections = [
    ['0', '1900'],
    ['1900', '1930'],
    ['1930', '1940'],
    ['1940', '1950'],
    ['1950', '2025'],
]

const selectElement = dv.el('select')

// Return file link with icon if size is larger than threshold
const getEventLink = (e) =>
    e.file.size > 150
    ? "<span class=\"more-text\">" + e.file.link + "</span>"
    : e.file.link

// Return tags in event as string of icons
const getEventIcon = (e) => {
    let output = ''
    for (const [tag, icon] of Object.entries(categories)) {
        if (e.file.tags.includes(tag)) {
            output += icon
        }
    }
    return output
}

// Render the table
const renderTable = (container, remove) => {
    // Remove the previous rendered table
    if (remove) {
	    for (const section of sections) {
		    if (section[0] !== '0') {
			    container.lastChild.remove();
		    }
		    container.lastChild.remove();
		}
    }
    
    // Loop over sections, returning header and table
    for (const section of sections) {
    
        // Don't print section header for "0" (zero)
        section[0] === '0' ? null : dv.header(1, section[0])
        
        // Return events table between the section dates
        dv.table(['', 'Date', 'Event'],
            dv.pages('"events"')
                .where(e =>
                    String(e.date) >= section[0]
                    && String(e.date) < section[1]
                    && e.file.tags.includes(selectElement.value)
                )
                .sort(e => String(e.date))
                .map(e => [
                    getEventIcon(e),
                    e.date,
                    getEventLink(e)
                ]
            )
        )
    }
}

// Dropdown
for (const key of Object.keys(categories)) {
    const option = dv.el('option', key)
    selectElement.appendChild(option)
}

const dropdown = dv.el('span', 'Category:')
dropdown.appendChild(selectElement)

selectElement.addEventListener("change", () => renderTable(this.container, true));

dropdown
renderTable(this.container, false);
```

The main idea is you just have to match the number of added elements to the number of removed ones. If you change your header logic, you’ll have to adjust the delete loop as well

Thanks a lot. I’ll try this and write back. I agree, though, that it is a bit of a hack… I’m so surprised there is no built-in method in dataview to auto-refresh from JS (like it does automatically when data changes).

Because I’ve just learned thanks to you that I need to manually remove the previous tables, I tried something a bit cleaner which works just fine. I just added this at the top of the original renderTable function:

this.container.querySelectorAll('.table-view-table').forEach(e => e.parentNode.remove())
this.container.querySelectorAll('h1').forEach(e => e.remove())

This removes all the table elements and their containing divs, as well as the h1 headers.

I also get the pages inside the loop and check if there are any before rendering the table, this gets rids of the “No results” box from Dataview.

Full code below. Thanks a lot!

// Map of tags to icons
const categories = {
    '#nixon': '📅',
    '#usa': '🔺',
    '#ussr': '🔹',
    '#johnson': '🔸',
}

// Timeline sections (start and end years)
const sections = [
  ['0', '1900'],
  ['1900', '1930'],
  ['1930', '1940'],
  ['1940', '1950'],
  ['1950', '2025'],
]

const selectElement = dv.el('select')

// Return file link with icon if size is larger than threshold
const getEventLink = (e) =>
  e.file.size > 150
    ? '<span class="more-text">' + e.file.link + '</span>'
    : e.file.link

// Return tags in event as string of icons
const getEventIcon = (e) => {
  let output = ''
  for (const [tag, icon] of Object.entries(categories)) {
    if (e.file.tags.includes(tag)) {
      output += icon
    }
  }
  return output
}

const renderTable = () => {
  this.container
    .querySelectorAll('.table-view-table')
    .forEach((e) => e.parentNode.remove())

  this.container.querySelectorAll('h1').forEach((e) => e.remove())

  console.log(this.container.querySelectorAll('.table-view-table'))

  // Loop over sections, returning header and table
  for (const section of sections) {
    // Don't print section header for "0" (zero)
    section[0] == '0' ? null : dv.header(1, section[0])

    const events = dv
      .pages('"events"')
      .where(
        (e) =>
          String(e.date) >= section[0] &&
          String(e.date) < section[1] &&
          e.file.tags.includes(selectElement.value)
      )

    // Return events table between the section dates
    if (events.length) {
      dv.table(
        ['', 'Date', 'Event'],
        events
          .sort((e) => String(e.date))
          .map((e) => [getEventIcon(e), e.date, getEventLink(e)])
      )
    }
  }
}

// Dropdown
for (const key of Object.keys(categories)) {
  const option = dv.el('option', key)
  selectElement.appendChild(option)
}

const dropdown = dv.el('span', 'Category:')
dropdown.appendChild(selectElement)

selectElement.onchange = renderTable

dropdown
renderTable()

That is a cleaner solution for sure.

I’d recommend taking out the console.log, though other than that it looks like you’re good to go.

I’ve only skimmed this thread so I might be of target, but not so long ago there was a thread to make dataview refresh the query, through refreshing the view of the actual note.

It might be worthwhile to search up that thread, and see if that could be used. I know for sure that I’m using a button in most of my test queries to trigger that refresh view option, and for my purposes it’s great.

Thanks. I think I’ve read that thread before posting here, but unless I’m wrong it addresses a different issue. What I needed to do was not to trigger a view refresh (as I unfortunately suggest in this thread’s title), but rather tell dataview to render a new view from scratch instead of appending to it. This is not the same thing.

Because I’m generating multiple dataview tables in a loop with JavaScript, there was a need to start with an empty page whenever the dropdown value changes, and from what I understand there is no way to do that directly in Obsidian, except by manually removing the existing elements as @mr_abomination suggested.

Anyway, the current solution works wonders for me. In fact, I thought of turning it into a more generic plugin, but I got to get some work done…

When dataview refreshes a view, it’s run completely and could completely rewrite the output.

The point of refreshing the view is to force dataview to rebuild everything, even though the query itself hasn’t changed. Normally there are cases where changing some dependent variable wouldn’t change the view, but the forced refresh would cause it to happen anyway.

The only reason I would replace a specific element (or two) would be to keep other elements unchanged for some reason.

So how would you suggest implementing the above functionality (i.e. rendering multiple tables, separated by specified headers, based on a custom array of tags, which can be selected from a dropdown menu at the top of the page and which, when changed, refreshes the output)?

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.