Refresh and toggle dataview/source

As some of you know I’m answering some posts in this forum related to dataview and dataviewjs queries. In this regard, I’ve been wanting to have a way to refresh the queries (if dataview decides not to do so itself), and I’ve also been playing around with wanting syntax highlight for both dataview query types.

With the help of buttons and templater I found a rather nifty setup which achieves both goals for me.

When I now write a response requiring either query type I insert the appropriate template from a hotkey, type in the line number of the starting line with the ```dataview, and start developing the query. If I now switch to either live preview or reading view I’ve got two buttons, which switches between either dataview and sql, or dataviewjs and javascript.

In reading view, this looks like the following just after insertion of the template, and insertion of line number 118 (using a multi cursor input):

Which in reading view looks like:

And now I can press either button to switch mode, or hit the To dataview again to force a refresh.

Template for DQL query
```button
name To SQL
type line(<% tp.file.cursor(1) %>) text
replace [<% tp.file.cursor(1) %>,<% tp.file.cursor(1) %>]
action ```sql
```
```button
name To dataview
type line(<% tp.file.cursor(1) %>) text
action ```dataview
replace [<% tp.file.cursor(1) %>,<% tp.file.cursor(1) %>]
```
```sql
<%*
  const queryType = await tp.system.suggester(t => t, ["list", "taskList", "table", "calendar"], "Query type")
  console.log(queryType)
  if ( queryType == "list" ) 
    tR += "LIST\n" + tp.file.cursor(2)
  else if ( queryType == "taskList" )
    tR += "TASK\n" + tp.file.cursor(2)
  else if ( queryType == "table" ) 
    tR += "TABLE " + tp.file.cursor(2) + "\n"
  else if ( queryType == "calendar" )
    tR += "CALENDAR\n" + tp.file.cursor(2)
  tR += "\nWHERE file.folder = this.file.folder AND file.name != this.file.name"
%>
```

I’ve attached some stuff I’m usually enter to keep my test script within the current folder, and excluding the summary note for that question. That part could (and should) of course be edited according to your liking.

Template for DataviewJS query
```button
name To javascript
type line(<% tp.file.cursor(1) %>) text
replace [<% tp.file.cursor(1) %>,<% tp.file.cursor(1) %>]
action ```javascript
```
```button
name To dataviewjs
type line(<% tp.file.cursor(1) %>) text
action ```dataviewjs
replace [<% tp.file.cursor(1) %>,<% tp.file.cursor(1) %>]
```
```javascript
const result = await dv.query(`
<% tp.file.cursor(2) %>
`)

if ( result.successful ) {
  const values = result.value.values
  <% tp.file.cursor(3) %>
<%*
  const queryType = await tp.system.suggester(t => t, ["list", "taskList", "table"], "Query type")
  console.log(queryType)
  if ( queryType == "list" ) 
    tR += "  dv.list(values)"
  else if ( queryType == "taskList" )
    tR += "  dv.taskList(values, false)"
  else if ( queryType == "table" ) 
    tR += "  dv.table(result.value.headers, result.value.values)"
%>
} else
  dv.paragraph("~~~~\n" + result.error + "\n~~~~")
```

I often find myself using DQL queries inside of a dataviewjs query, so therefore my template for this incorporates this, and also sets up the correct variation for including a little bit of error handling.

CSS to float buttons to the right
div:has(>.block-language-button) {
  display: inline-block;
  float: right;
}

div:has(>.block-language-button)+div.HyperMD-codeblock,
div:has(>.block-language-button)+div:has(.language-javascript), 
div:has(>.block-language-button)+div:has(.language-sql),
div:has(>.block-language-button)+div:has(.node-insert-event) {
  clear: both;
}

The following needs to be copied to a snippet inside vault/.obsidian/snippet, and enabled under Settings > Appearance (CSS snippets).

Note that it looks very nice in reading view, whilst in live preview it is kind of centered due to some strangeness related to widths of surrounding divs. I tried just a little to also make it nice there, and failed. I’m usually in either reading view or source mode, but these buttons do work also in live preview, they just don’t look that pretty.

A few notes on setup

The two first templates needs to be installed into your template folder, and I’ve named them dvQuery.md and dvjsResultQuery.md. To assign hotkeys do the following:

  • Go to Settings > Templater (Template Hotkeys)
  • Hit the “Add new hotkey for template” (twice)
  • Add the two templates into corresponding boxes
  • Hit the plus icon after either box. You’ll now see the Settings > Hotkeys configuration with the Templater insert commands focused
  • Select the plus icon next to either template, and hit a hotkey of your choice. I’ve opted for Cmd Ctrl s and Ctrl Cmd Shift S. Make sure the hotkeys are not used already by hovering over the key combination, and see that no error messages shows.

After installing the template, and the CSS snippets if you opt for that, you’re good to go after turning on line numbers at Settings > Editor > Show line number.


Caveat: This solution relies on changing the first line of the code block containing the queries, and it requires the correct line number. This also means that if you change text preceeding the code block, you need to update the line number in all six places in the button code. Otherwise it’ll happy replace another line of your code. You can undo to get back the line, but be forewarned.

Here is another screenshot from a recent answer, where I had both DQL queries and dataviewjs queries. The first shows the result, and the second is currently showing the source code nicely colored (which can be a great help to locate minor errors like missing quotes and braces and so on).

3 Likes

@holroy very, very cool solution you have above to your problem.

I came across this thread while searching for how to create an auto-refreshing Dataview query (e.g. something that refreshes every 1 second or 5 minutes, etc).

I eventually figured it out, and while it isn’t related to your post I thought you might be interested.

edit: Skip down to my later post, it has a much better solution.


This example will show a clock that updates every second:

```dataviewjs
let el
const repeat = setInterval(() => {
  if (el && !el?.checkVisibility()) {
    // Clear the repeat if the note is no longer open
    clearInterval(repeat)
  } else {
    // Remove the current contents of the Dataview block
    // so that we can replace it with new contents
    dv.container.innerHTML = ''
    el = dv.el('div', '')
    
    // Put all your Dataview output here
    dv.header(5, 'This time will keep updating while the note is open:')
    dv.paragraph(moment().format('HH:mm:ss'))
    console.log(moment().format('HH:mm:ss'))
  }
}, 1000)
```
2 Likes

Since then stuff has happened with Dataview, like the addition of the command Dataview: Rebuild current view which allows for a much easier refresh of a query. And addition of native syntax highlighting of the pure dataviewjs queries (at least).

Regarding using setInterval() to refresh we explored this in another thread related to a progress bar, if I remember correctly, and in that thread we had some issues with multiple intervals being set. Are you sure, @AlanG, that when your code is active that it doesn’t trigger multiple intervals (and refreshes) when the note is open?

Have you tried displaying active intervals, and it outputting the interval is from within the callback to verify that you’re only running one instance?

1 Like

Absolutely. I can confirm it only runs one instance. I amended the previous post to output to the console also, so you can paste and check.

The way it works is it stores the interval in a variable, then once it’s out of view it cancels it. This way you don’t end up with multiple intervals running.

I didn’t know about the Rebuild view command, thanks!

That does partially work indeed, but I added an output to the clearing of the interval and added the output of the repeat variable into the mix, and got this output:

image

Here we can see that the script is actually running twice when I first switched to reading mode. This would in a lot of cases cause a second interval to be triggered, but your code related to checking the visibility disables that the first instance and fires up another one. So kudos to you for that checkVisibility() call.

However, there is a little caveat/bug (in dataview) that if you switch back to source mode, it’ll not retrigger when you return to reading mode since the query hasn’t changed so it’s cached… In other words, it’ll work the first after your source has changed, but not on the second run. And this longtime bug of dataview is the reason why I felt the need for adding that rebuild command in the first place, and somewhat counterintuitive the original script switching the code block types also counters.

That being said, your script is a very clean approach on how to regularly change the contents of a generated element.

My extended script
```dataviewjs
let el
const repeat = setInterval(() => {
  if (el && !el?.checkVisibility()) {
    // Clear the repeat if the note is no longer open
    clearInterval(repeat)
    console.log(`Clearing interval: ${ repeat }`)
  } else {
    // Remove the current contents of the Dataview block
    // so that we can replace it with new contents
    dv.container.innerHTML = ''
    el = dv.el('div', '') 
    
    // Put all your Dataview output here
    dv.header(5, 'This time will keep updating while the note is open:')
    dv.paragraph(moment().format('HH:mm:ss'))
    console.log(`${ repeat } : ${ moment().format('HH:mm:ss') }`)
  } 
}, 1000)
```

Do note that the re-running of the script is not always a given, but in my vaults it happens fairly often so I’ve gotten use to the need of coding around it (aka adding spacing or making other minuscule changes to my queries to re-trigger the running of the query).

That’s also solvable :slight_smile:

I rewrote it to handle that, which actually makes the script better overall, so thank you for the suggestion.

Instead of checking the visibility of an element, we add an Observer on the Dataview container, which let’s us know when it goes in and out of view.

If it’s out of view we cancel the interval, and when it comes back into view we start a new interval.

To make it even easier to re-use the code on multiple pages, I turned it into a dv.view().

Put this with your Dataview scripts, called refresh.js:

/*
Call this file refresh.js and store it with your Dataview scripts.

Here's an example of using it to create a clock that updates each second:

dv.view('refresh', {
  contents: () => { 
    dv.paragraph(moment().format('HH:mm:ss'))
  },
  delay: 1000
})

*/
const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    clearInterval(entry.target.interval)
    entry.target.innerHTML = ''
    if (entry.isIntersecting) {
      // Show the user's contents
      input.contents()
      // Set up the interval
      entry.target.interval = setInterval(() => {
        entry.target.innerHTML = ''
        input.contents()
      }, input?.delay || 60000)
    }
  })
})
observer.observe(dv.container)

And then call it from any page like this:

```dataviewjs
dv.view('refresh', {
  contents: () => { 
    dv.header(5, 'This time will keep updating while the note is open:')
    dv.paragraph(moment().format('HH:mm:ss'))
    console.log(moment().format('HH:mm:ss'))
  },
  delay: 1000
})
```

You can see from the console that it will immediately cancel and resume as you change between different notes or the Source and Reading etc views.

You can even call another dv.view(...) from within the refresh block, which makes it pretty unlimited what you can have automatically updating:

```dataviewjs
dv.view('refresh', {
  contents: () => dv.view('some-fancy-view')
})
```
3 Likes