Live filtering of dataviewjs table

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?

I looked at your post, but I’m afraid, I didn’t understand much. Maybe it’s my poor understanding of English, but I’m just confused about what are you trying to do. I’m also not really that much of a technical person either, I just happen to know a bit of javascript.

The basic principles of my script are pretty simple. The table is filtered based on the value, stored in the frontmatter of the current note. Change the value - filtering is changed too. Than we add the button that changes the value in the frontmatter, so we have not to do it manually. The button script takes all the pages, finds the needed properties in them and creates the list of property values to suggest - then puts the chosen value to the frontmatter. Then there is a lot of code to create suggester modals, to apply the different rules for different types of properties, etc. What part are you exactly need help with?

About your question - your mean the button changing color? You need to apply some css to it. The active button gets the class “button-blue” (actually I justh thought that I should change it to something more semantic, but nevermind), then I add the snippet:

button.button-blue {
  background-color: rgba(102,140,179, .2);
  border-color: rgba(102,140,179, .4);
}

Я говорю на русском, поэтому предлагаю перейти на него. Извините за долгий ответ. К сожалению, ваш snippet у меня не действует. Я менял дизайн кнопки в вашей таблице только за счет изменения дефолтной кнопки. С изменением дизайна обычных кнопок проблем нет.

Кроме того, по какой-то причине у меня работает только ваша старая таблица. Новая версия с multiselection выдает ошибку:

И подскажите пожалуйста, как можно добавить фильтрацию страниц. В dataviewjs почти не разбираюсь.

Судя по ошибке, у вас скрипт вообще не подключился. Возможно, путь к файлу неправильно указан. Можете показать ваш код?

Со старой таблицей цвет кнопки работать и не будет, там не было этого функционала. Нужно класс к кнопке добавлять.

Сложно объяснять, как сделать пагинацию, если вы в джаваскрипте не разбираетесь. Это довольно большой кусок кода. Лучше всё-таки попробовать скрипт подключить нормально.

Я собственно ваш код и не менял. Я создал отдельное хранилище, объекты которого я сделал исходя из значений атрибутов в вашем коде. Видимо, я допустил какую-то примитивную ошибку)

```dataviewjs
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('"Test folder"')
 
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)
```

You are using my path to the script file, but your file has different path. Change the first line to match your path:

await dv.view("scripts/views/dv_filter_functions")