Live filtering of dataviewjs table

I figured the way to filter dataviewjs tables without editing the code, you need to only click the button. For it to work you also need the Buttons plugin and the Templater plugin.

Here is the demo:

demo

And here is the code. It’s a bit complicated, but I refactored it so it is easy enough to use and edit. You can add as many filters as you want to.

```dataviewjs
// Don't change this code

const {createButton} = app.plugins.plugins["buttons"]
const tp = app.plugins.plugins["templater-obsidian"].templater.current_functions_object

let current = dv.current()
let currentFile = app.vault.getAbstractFileByPath(current.file.path)

const changeFilter = async(prop) => {
  let propName = "filter-" + prop
  let values = pages.map(p => p[prop])
  values = [...new Set(values)]
  values.unshift("all")
  let val = await tp.system.suggester(values, values)
    app.fileManager.processFrontMatter(currentFile, (frontmatter) => { 
		frontmatter[propName] = val
	})
  }

const filterButton = async(name, prop) => {
  createButton({
	app, 
	el: this.container, 
	args: {
		name: name}, 
	clickOverride: {
		click: await changeFilter, 
		params: [prop]
	}
  })
}

const filterFunction = async(prop) => {
  let propName = "filter-" + prop
  let filter = current[propName]
  if (filter != "all" && filter != null) {
	filteredPages = filteredPages.filter(p => p[prop] == filter)
  }
}




// Only edit the code below

let pages = dv.pages('"Test folder"')
let filteredPages = [...pages]


// Apply filters
await filterFunction("color")
await filterFunction("number")

// Add filter buttons
await filterButton("Filter color", "color")
await filterButton("Filter number", "number")


let headers = ["Name", "Color", "Number"]
let rows = filteredPages.map(p => [p.file.link, p.color, p.number])

dv.table(headers, rows)
```

Unfortunately this code doesn’t support multiselect or complicated data formats, but it can be tweaked to support it probably.


Update: I actually liked this idea so much, that I decided to use the same principle to create pagination and also add new notes to the table. As a result I created absolutely monstous script, that is too big to put here, so you can find it on my Github.

Here is the demo:


Update: Okay, so I decided that that huge script was to hard to reuse, so I separated it in two files, removing all the hard coding stuff into separate module.

Now if you want to use it, you need to download this file and put it to your vault, and then copy this code and put it in your dataviewjs codeblock. Now it is very straightforward and easy to edit, and I am pretty happy with this solution.

13 Likes

This is great. This is something I was tinkering with a while back (Filterable Dataview Table / Change Work Mode / Filter Note List), but js is above my pay grade. Your implementation is MUCH better.

I notice that the filters are persistent through note closings and even Obsidian restarts. How long will it stay filtered?

Actually, I think I found a bug; after testing the Obsidian restart, I am no longer able to click the buttons. Just nothing happens when I click.

Filters values are saved in the note frontmatter, so they will stay forever unless you click the buttons again or edit frontmatter manually.

If buttons don’t work after restart, it is problem with templater. I use templater’s functions for the suggester, but they don’t work untill some template is activated at least once. I recommend workaround: create empty template and set it as a startup template in templater’s options. Then everything should work after restart.

2 Likes

Filters values are saved in the note frontmatter, so they will stay forever unless you click the buttons again or edit frontmatter manually.

That’s clever. I really like that. I didn’t notice because I keep mine hidden.

If buttons don’t work after restart, it is problem with templater. I use templater’s functions for the suggester, but they don’t work untill some template is activated at least once. I recommend workaround: create empty template and set it as a startup template in templater’s options. Then everything should work after restart.

Thanks for the workaround. I’ll give that a shot.

I actually liked this idea so much, that I decided to use the same principle to create pagination and also add new notes to the table. And added the ability to filter by list properties too (only by one at once). As a result I created absolutely monstous script, that is too big to put here, so you can find it on my Github.

Here is the demo:

1 Like

Okay, so I decided that that huge script was to hard to reuse, so I separated it in two files, removing all the hard coding stuff into separate module.

Now if you want to use it, you need to download this file and put it to your vault, and then copy this code and put it in your dataviewjs codeblock. Now it is very straightforward and easy to edit, and I am pretty happy with this solution.

6 Likes

That is aaaawwesome!!! :slight_smile:
I hope that becomes a core feature!!! Great work! :slight_smile:

You really made an awesome feature

But can I customize the suggester?

You can, but you need to edit the main code file for this, and it’s not very convenient.
I’m currently in the process of rewriting the whole code to make it easier to use and customize without fiddling with javascript to much…

3 Likes

This is great! Is there any update on the rewrite? Just wondering if I should try implementing as is, or there’s something upcoming to wait for.

If anyone’s interested, I’ve redesigned it to suit my needs and, above all, to avoid using the buttons plugin:

const tp = app.plugins.plugins["templater-obsidian"].templater.current_functions_object;
const checkMark = "✅ ";

let current = dv.current();
let currentFile = app.vault.getAbstractFileByPath(current.file.path);

const changeFilter = async(prop) => {
	let propName = "filter-" + prop
	let values = pages.map(p => p[prop])
	values = [...new Set(values)]
	values.unshift("all")
	const config = {
	  "suggestions": ["Done", ...values],
	  "values": ["Done", ...values],
	  "responses": []
	};
	
	let response;
	while (response !== "Done") {
	response = await tp.system.suggester(config.suggestions, config.values, true, "Selection");
	if (response !== "Done") {	
		let rIndex = config.responses.indexOf(response);
		if (rIndex > -1) {
				config.responses.splice(rIndex, 1);					
		} else {				
			config.responses.push(response);
		}
		let vIndex = config.values.indexOf(response);
		let suggestion = config.suggestions[vIndex];
		if (suggestion.startsWith(checkMark)) {
			config.suggestions[vIndex] = suggestion.replace(checkMark,"");
		} else {
			config.suggestions[vIndex] = checkMark + suggestion;
		}
	}
}
    app.fileManager.processFrontMatter(currentFile, (frontmatter) => { 
		frontmatter[propName] = config.responses
	})
}

const button_status = dv.el('button', 'status' + '(' + current['filter-status'] + ')');
button_status.onclick = async() => {
	await changeFilter("status");
}
const button_genres = dv.el('button', 'genres' + '(' + current['filter-genres'] + ')');
button_genres.onclick = async() => {
	await changeFilter("genres");
}
const button_type = dv.el('button', 'type' + '(' + current['filter-type'] + ')');
button_type.onclick = async() => {
	await changeFilter("type");
}
const button_score = dv.el('button', 'score' + '(' + current['filter-score'] + ')');
button_score.onclick = async() => {
	await changeFilter("score");
}
const button_langue = dv.el('button', 'langue' + '(' + current['filter-langue'] + ')');
button_langue.onclick = async() => {
	await changeFilter("langue");
}

const filterFunction = async(prop) => {
  let propName = "filter-" + prop
  let filter = current[propName]
  if (typeof(filter)=="object" && !filter.includes("all") && filter.length != 0) {
	  filteredPages = filteredPages.filter(p => filter.includes(p[prop]))
  }
}

let pages = dv.pages('"04.1📚 Ressources/films"').sort(p => p.file.name)
let filteredPages = pages

// Apply filters
await filterFunction("status");
await filterFunction("type");
await filterFunction("support");
await filterFunction("score");

// Add filter buttons
dv.span(button_status, button_genres, button_type, button_score, button_langue);

dv.paragraph("Nombre de jeux : " + filteredPages.length)

let headers = ["Cover","Name", "status", "type", "genres", "score","langue"]
let rows = filteredPages.map(p => ["![|60](" + p.cover + ")",p.file.link, p.status, p.type + ", " + p.genres, p.score, p.langue])
dv.table(headers, rows)

Think links are broken :frowning:

I’ve not tested either version, so maybe this is a given, but can you select multiple values in any of these filters? I’m contemplating on adapting this to task filtering, where it would be interesting to select status from a range of status characters and display more than one at the time.

For the original version of the post, I don’t think so, maybe in the version on his github but I think the links are not active.
In the version I shared, yes I can, I was inspired by a code I found, I don’t know where, to make a multi select that I added, it’s this part here that creates the menu and every time you select a value it checks it and redisplays the menu with that value checked…:

const config = {
	  "suggestions": ["Done", ...values],
	  "values": ["Done", ...values],
	  "responses": []
	};
	
	let response;
	while (response !== "Done") {
	response = await tp.system.suggester(config.suggestions, config.values, true, "Selection");
	if (response !== "Done") {	
		let rIndex = config.responses.indexOf(response);
		if (rIndex > -1) {
				config.responses.splice(rIndex, 1);					
		} else {				
			config.responses.push(response);
		}
		let vIndex = config.values.indexOf(response);
		let suggestion = config.suggestions[vIndex];
		if (suggestion.startsWith(checkMark)) {
			config.suggestions[vIndex] = suggestion.replace(checkMark,"");
		} else {
			config.suggestions[vIndex] = checkMark + suggestion;
		}
	}

And as a tip, I recently changed the system that allows me to have the templater suggestion to quickadd because templater doesn’t work as well with dataviewjs I think (templater has to have already been activated once via a template for it to trigger properly, I think).
So I changed my code for this:

let quickadd = app.plugins.plugins.quickadd.api;
const checkMark = "✅ ";

let current = dv.current();
let currentFile = app.vault.getAbstractFileByPath(current.file.path);

const changeFilter = async(prop) => {
	let propName = "filter-" + prop
	let values = pages.map(p => p[prop]).filter(p => p !== null)
	values = [...new Set(values)]
	values.unshift("all")
	const config = {
	  "suggestions": ["Done", ...values],
	  "values": ["Done", ...values],
	  "responses": []
	};
	
	let response;
	while (response !== "Done") {
	response = await quickadd.suggester(config.suggestions, config.values);
	if (response !== "Done") {	
		let rIndex = config.responses.indexOf(response);
		if (rIndex > -1) {
				config.responses.splice(rIndex, 1);					
		} else {				
			config.responses.push(response);
		}
		let vIndex = config.values.indexOf(response);
		let suggestion = config.suggestions[vIndex];
		if (suggestion.startsWith(checkMark)) {
			config.suggestions[vIndex] = suggestion.replace(checkMark,"");
		} else {
			config.suggestions[vIndex] = checkMark + suggestion;
		}
	}
}
    app.fileManager.processFrontMatter(currentFile, (frontmatter) => { 
		frontmatter[propName] = config.responses
	})
}

const button_status = dv.el('button', 'status' + '(' + current['filter-status'] + ')');
button_status.onclick = async() => {
	await changeFilter("status");
}
const button_genres = dv.el('button', 'genres' + '(' + current['filter-genres'] + ')');
button_genres.onclick = async() => {
	await changeFilter("genres");
}
const button_type = dv.el('button', 'type' + '(' + current['filter-type'] + ')');
button_type.onclick = async() => {
	await changeFilter("type");
}
const button_score = dv.el('button', 'score' + '(' + current['filter-score'] + ')');
button_score.onclick = async() => {
	await changeFilter("score");
}
const button_langue = dv.el('button', 'langue' + '(' + current['filter-langue'] + ')');
button_langue.onclick = async() => {
	await changeFilter("langue");
}

const filterFunction = async(prop) => {
  let propName = "filter-" + prop
  let filter = current[propName]
  if (typeof(filter)=="object" && !filter.includes("all") && filter.length != 0) {
	  filteredPages = filteredPages.filter(p => filter.includes(p[prop]))
  }
}

let pages = dv.pages('"04.1📚 Ressources/films"').sort(p => p.file.name)
let filteredPages = pages

// Apply filters
await filterFunction("status");
await filterFunction("type");
await filterFunction("support");
await filterFunction("score");

// Add filter buttons
dv.span(button_status, button_genres, button_type, button_score, button_langue);

dv.paragraph("Nombre de jeux : " + filteredPages.length)

let headers = ["Cover","Name", "status", "type", "genres", "score","langue"]
let rows = filteredPages.map(p => ["![|60](" + p.cover + ")",p.file.link, p.status, p.type + ", " + p.genres, p.score, p.langue])
dv.table(headers, rows)

That’s kind of correct, since Templater indeed need that one time activation. However, since Templater also allow for a startup template, which in my case is empty, this can very easily be avoided from becoming an issue since that startup template is enough for us to use the Templater functions.

I might need to investigate further then in how to do this kind of filtering with a task query as data source. It sounds promising…

You can have multiselection with the new version of my script, you can find it here: https://github.com/anareaty/obsidian-snippets/blob/main/Dataview%20helpers/dv_filter_functions.js.

Unfortunately I can not find time to write the full explanation for how it works, because it got pretty complicated at this point. But there is the example code for the table with multiple views and filters. Just describe your own paths and properties in the props arrays.

await dv.view("_/scripts/views/dv_filter_functions")
const df = new dataviewFunctions
let current = dv.current()
let view = current.view
let views = ["View1", "View2"]
let defaultView = "View1"
let paginationNum = 20
let filteredPages = []
let props = []
let  newNote = {
		buttonName: "+", 
		fileName: "New note", 
		templatePath: "_/templates/New note.md", 
		folderName: "Inbox", 
		open: true
	}
let pages = dv.pages()
 
await df.changeViewButton(views, "view", defaultView)

if (!view) view = defaultView



if (view == "View1") {

  pages = pages.filter(p => p.type == "type 1" )
  
  props = [
  {prop: "link", type: "file prop", header: "Name"},
  {prop: "tags", type: "list", multiSelect: true, span: true, buttonName: "Tags", header: "Tags"}
  ]
    
   newNote.templatePath = "_/templates/Template 1.md"
   newNote.folderName = "Folder 1"
 

} else if (view == "View2") {

  pages = pages.filter(p => p.type == "type 2" )
  
  props = [
  {prop: "link", type: "file prop", header: "Name"},
  {prop: "category", type: "text", buttonName: "Category", header: "Category"},
  {prop: "cover", type: "text", imageWidth: 100, header: "Cover"},
  {prop: "check", type: "boolean", header: "Check", buttonName: "Check"}
  ]
    
   newNote.templatePath = "_/templates/Template 2.md"
   newNote.folderName = "Folder 2"
 
}
 
filteredPages = [...pages]
await df.newEntryButton(newNote)
await df.createTable(props, pages, filteredPages, paginationNum)
 

Props can have types “text”, “list”, “boolean” and “file props” (for file properties, like link or path). List props can have multiSelect option. Text props can have imageWidth option to turn link into image. Text and list can have span option, to wrap values in spans with separate classes, so you can style them with css. If you omit header, property will not show in table, and if you omit buttonName there will be no button.

I’m trying to make all this easily editable, so here what I came to. Still a bit rough. There are a lot of additional functions in the script.

Also this version do not require any additional plugins, but it is better to use it with Templater or CustomJS installed. Otherwise this script will create the mock plugin to reach Obsidian API, which is a bit clumsy solution.

1 Like

This looks awesome! Perhaps it’s obvious but I’m pretty new to dataview and dataviewjs, so I was wondering how exactly I would get the filtering option to show up. I have downloaded the github file and placed it in my obsidian and I have Templater installed, but I haven’t figured out how to use it. I think I’m missing a step. What else should I be doing to implement it?

Thank you sm! This is super cool :slight_smile:

The script is only half of the code, the other half you should write it the note in the dataviewjs code block. Try to copy the code in my previous post and edit it for your needs.

1 Like

Awesome, thank you! I will do

Hello, @reaty. Can you look at this thread? I want to make a thematic classification system (UDC) by using a hierarchical suggester. It is used to classify entities, which are then filtered in your table by selecting a nested tag. I do not have programming skills, which is why I cannot implement this technically.

I would like to know your opinion about this idea, is it really possible to do this and how difficult is it?

And I have one more question:

  • How did you make the button active if a value is selected in it?