Fantastic. Thank you so much!
Hi all,
I am new to Obsidian and dataview.
I am currently investigating using dataviewjs to extract the notes contains a certain string in a field.
For example, I have to extract all the notes with Name field contains string âJohnâ.
It might be Name :: John 123/ Name :: John abc/ Name :: Johnny
I am currently using
dv.list(dv.pages("").where(k => k.name.includes("John")))
But it prompted me the error:
Evaluation Error: TypeError: Cannot read property âincludesâ of undefined at eval
May I have your help on this?
Thank You so much
I only use dataview (not in JS), but do you need JS for it? I think the following may do it:
list from ""
where contains(Name, "John")
Thanks for the recommendation @atiz .
Yes I would like to use JS as I want to do the grouping with another fields into different sections with headings using let group of
.
I have also tried the dataview groupby
with the row.Name
but it can only be in the same section.
This script is fantastic, but it did have some trouble if there were multiple tables next to each other. For anyone else with that problem, here is the small edit to fix that.
// Source https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript
const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;
const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2, undefined, { numeric: true })
)(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
document.querySelectorAll('table').forEach(table => {
// do the work...
Array.from(table.querySelectorAll('th')).forEach(th => th.style.cursor = "pointer");
table.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
const tbody = table.querySelector('tbody');
Array.from(tbody.querySelectorAll('tr'))
.sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
.forEach(tr => tbody.appendChild(tr));
})));
})
wow, this is really impressive!
Iâve also seen your other posts about the inline countdown feature
<%+* let edate = moment(â12-24â, âMM-DDâ); let from = moment().startOf(âdayâ); edate.diff(from, âdaysâ) >= 0 ? tR += edate.diff(from, âdaysâ) : tR += edate.add(1, âyearâ).diff(from, âdaysâ) %>
Can this be also included in this table?
Dear Moonbase,
I edited your code to allow an extra column of countdowns to the next birthday
var start = moment().startOf('day');
var end = moment(start).add(dv.current().duration);
var dateformat = "YYYY-MM-DD";
if (dv.current().dateformat) { dateformat = dv.current().dateformat; }
// info text above table, {0}=duration, {1}=start date, {2}=end date
// parameters can be left out, or the string empty
var infotext = "Upcoming birthdays for {0} from now ({1} â {2})<br><br>";
//======================================================================
function nextBirthday(birthday) {
// Get personâs next birthday on or after "start"
// returns a moment
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldnât harm if already a string
var bday = moment(birthday.toString());
var bdayNext = moment(bday).year(start.year());
if (bdayNext.isBefore(start, 'day')) {
bdayNext.add(1, "year");
}
return bdayNext;
}
function turns(birthday) {
// Get the age in years a person will turn to on their next birthday
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldnât harm if already a string
var bday = moment(birthday.toString());
return nextBirthday(birthday).diff(bday, 'years');
}
function countdown(birthday){
var bday = moment(birthday.toString())
const setTime = new Date(bday);
const nowTime = new Date();
const restSec = setTime.getTime() - nowTime.getTime();
const day = parseInt(restSec / (60*60*24*1000));
const str = day + "怩";
return str;
}
function showBirthday(birthday) {
// Determine if this birthday is in the range to be shown
// including the start date, excluding the end date
// because that comes from a duration calculation
// for use with "where", returns true or false
if (birthday) {
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldnât harm if already a string
var bday = moment(birthday.toString());
var bdayNext = nextBirthday(birthday);
if (bdayNext.isBetween(start, end, 'day', '[)')) {
return true;
} else {
return false;
}
} else {
return false;
}
}
function sortByNextBirthday(a, b) {
// comparator function for "sort"
if (nextBirthday(a).isBefore(nextBirthday(b))) {
return -1;
}
if (nextBirthday(a).isAfter(nextBirthday(b))) {
return 1;
}
// theyâre equal
return 0;
}
//======================================================================
dv.paragraph(infotext.format(moment.duration(dv.current().duration.toString()).humanize(), start.format(dateformat), end.format(dateformat)));
dv.table(
["ć§ć", "çæ„/æĄć°æ„", "ćčŽéŸïŒèćČïŒ","ćèźĄæ¶"],
dv.pages(dv.current().searchterm)
// use a function to see if this birthday is in range to be shown
.where(p => showBirthday(p.birthday))
// use a comparator function to sort by next birthday
.sort(p => p.birthday, 'asc', sortByNextBirthday)
.map(p => [
p.file.link,
p.birthday ? nextBirthday(p.birthday).format(dateformat) : 'â',
turns(p.birthday),
countdown(nextBirthday(p.birthday)),
])
);
this was a good workaround for the bug with WHERE no longer working as intended. thank you!
Iâve been utilizing my daily notes to track my habits as a list under a âHabitsâ header. I wrote some dataviewjs to help me keep up to date on these habits in a weekly roll-up in such a way that I am not required to define the habits in dataview.
This in combination with the Templater and Periodic Notes plugins let me automatically track habits in Obsidian with ease. Hopefully this can inspire other uses for dataviewjs.
var start = moment("2021-10-17");
var end = moment("2021-10-24");
var dailies = '"Daily Notes"';
var daily_fmt = 'YY-MM-DD';
let days = dv.pages(dailies).file
.where(f => moment(f.name, daily_fmt).isBetween(start, end, 'day', '[]'))
.sort(f => f.day);
let keys = ["Date"]
let habits = {}
days.forEach(day => {
habits[day.name] = {}
day.tasks.filter(task => task.link.subpath == "Habits").forEach(habit => {
if (keys.indexOf(habit.text.toLowerCase()) == -1) {
keys.push(habit.text.toLowerCase())
}
habits[day.name][habit.text.toLowerCase()] = habit.completed
})
})
dv.table(keys.map(k => k[0].toUpperCase() + k.slice(1)), Object.keys(habits).map(k => {
return [dv.fileLink(k)].concat(keys.slice(1).map(i => habits[k][i] ? "â" : ""))
}))
Also, I decode the timestamp from the file name as Iâve found relying on the system for accurate timestamps for creation and modification is unreliable with my syncing solution. ymmv
This is incredible and what I was looking for. Thank you!
Iâve tried to update your code for my YAML format of:
dates:
birthday: 2000-01-01
anniversary: 2000-01-01
But for some reason as soon as I change .where(p => showBirthday(p.birthday))
to .where(p => showBirthday(p.dates.birthday))
it completely bugs out. When I update the .sort
and .map
functions using p.dates.birthday
, they work fine. Itâs just the where() function.
Do you have any idea what I am missing? Or can it not be done? Thanks again
List of non-existing pages
The following snippet shows a list of all non-existing pages in your vault, i.e. topics which are mentioned [[using wikilinks]], but for which there is no destination.
Each item in the generated list is a link, that creates the note if you click it.
# List of non-existing pages
```dataviewjs
let r = Object.entries(dv.app.metadataCache.unresolvedLinks)
.filter(([k,v])=>Object.keys(v).length)
.flatMap(([k,v]) => Object.keys(v).map(x=>dv.fileLink(x)))
dv.list([...new Set(r)])
```
Note: Iâm rather new to Obsidian (only one week), its api, and the dataview plugin and I donât know if there is a simpler way to do this. Critizism is welcome!
Hello,
Last week I shared my amateur task management set up with notes as tasks.
I asked whether a clickable Done button could update the Done
field, and thus remove the task from the list.
@asauceda94 helpfully suggested this could be done using MetaEdit and Dataview JS. I was able to get partway to the conversion. But I wonder if any JS experts could help finish the job, thanks in advance.
convert from standard dataview table
TABLE WITHOUT ID
file.link as File_________________________________,
do-date as "Do_Date",
Priority as Priority________,
due-date as "Due_Date",
recur-length as "Recur Length",
do-date + default(recur-length, "") as "Next_Date",
project as Project______,
FROM #â
/S/1_Active and !"âïž System"
WHERE done = null,
do-date < date(tomorrow)
SORT priority asc, do-date asc, file.mtime desc
to DataviewJS table
const {update} = this.app.plugins.plugins["metaedit"].api;
const buttonMaker = (pn, pv, fpath) => {
const btn = this.container.createEl('button', {"text": "Done"});
const file = this.app.vault.getAbstractFileByPath(fpath)
btn.addEventListener('click', async (evt) => {
evt.preventDefault();
await update(pn, pv, file);
});
return btn;
}
dv.table([
"Done",
"File_________________________________",
"Do_Date",
"Priority",
"Due_Date",
"Recur Length",
"Next_Date",
"Project______",
],
dv.pages("#â
/S/1_Active")
.sort(t => t["priority"], 'asc')
.sort(t => t["do-date"], 'asc')
.where(t => !t.done)
.map(t =>
[buttonMaker('Done', 'Done', t.file.path),
t.file.link,
t["do-date"],
t.priority,
t["due-date"],
t["recur-length"],
t["NEXT DATE QUERY"],
t["project"],
])
)
- Remaining tasks in converting to JS
-
WHERE command -==> ==Solved==done = null
(where done field is empty).where(t => !t.done)
- WHERE command -
WHERE do-date < date(tomorrow)
(where do-date is today or before) - FROM command - Exclude files from
System Folder
- Next Date table query -
do-date + default(recur-length, "") as "Next_Date",
- JS script - Make the ButtonMaker add the current date - possibly using this script
await update('completed-date', DateTime.local().toISODate(), file);
-
Wonderful! Thank you!
itâs lot to ask for, but could you explain a little what happens in the code for us noobs in js?
I want to change the headers, both the names and how many. But little success so far.
Hi again @dryice !
Please note that I am NOT an expert in JS. These are what have worked for me, but I would not be surprised if there was a more optimal way to do this. However, I am here to help how I can. ALSO, one note about my DataviewJS style - I prefer to create a DV DataArray outside of a dv.table()
call - I find that it keeps it a tad cleaner, and I am able to add more functionality that way.
- Comparing dates can be a little tricky from my experience. If you are using the standard ISO format in your DV fields (e.g. 2021-11-15) as I am:
// Below line of code is outside of the dv.pages() call
let today = DateTime.local().toISODate();
// I like to create my DV DataArrays outside of the dv.table call
let taskList = dv.pages("#tasks")
.filter(t => t["due-date"] !== null)
.filter(t => dv.date(t["due-date"]).toISODate() <= today);
- To my knowledge, I donât think you can have two different DV sources in the
FROM
command. To solve this use case, I just add another filter statement to remove in the pages query:
let projectList = dv.pages("#projects")
.filter(p => !p.file.folder.contains("INSERT FOLDER NAME HERE]"));
- For Next Date, I donât have anything with this specific use case in my vault, but would probably involve using Luxon Duration. Please note I havenât actually tested the implementation below, this is just a quick idea I cobbled together.
// All of your code above
.map(t =>
// More Code - rough NEXT DATE QUERY below
t["recur-length"] ? dv.date(t["do-date"]).plus({ days: t["recur-length"] }).toISODate() : " ",
// Rest of your code
- My full button code (should have probs included it in other thread):
const {update} = this.app.plugins.plugins["metaedit"].api;
const buttonMaker = (pn, pv, fpath) => {
const btn = this.container.createEl('button', {"text": "Finished!"});
const file = this.app.vault.getAbstractFileByPath(fpath)
btn.addEventListener('click', async (evt) => {
evt.preventDefault();
await update(pn, pv, file);
await update('completed-date',DateTime.local().toISODate(),file);
});
return btn;
}
My friend @asauceda94 - noted⊠youâre not an expert, but youâve definitely got skills and generosity - many thanks for your reply!
My progress: I have played around with your JS snippets, I have started a coding course to help, and I have tried really, really hard haha ⊠But I feel like a toddler playing with a 1000 piece jigsaw puzzle.
Maybe Iâll have the skills someday in the future. It might be the instructional to see your code as a complete block, rather than walking me through step by step. What do you think?
But specifically I am having trouble with
- Insert Date Button code - I donât know what goes into
.map(t =>
atbuttonMaker('FIELD', 'INSERTED TEXT', t.file.path)
- despite your explanations about DataviewJS style and arrays being outside of
dv.table()
call, I canât find the right place for the âletâ functions and filters. When I put them
Thanks!
Considering that daily notes name has the format 2021-11-23
, the following snippet lists tasks from previous daily notes and future 3 days, excluding the current day.
let pages = dv.pages('"Daily note"');
dv.taskList(
pages
.where(p => (dv.date(p.file.name) <= dv.date(dv.current().file.name) + dv.duration('3 days')) && !dv.equal(dv.date(p.file.name),dv.date(dv.current().file.name)))
.sort(p => p.file.name, 'asc')
.file
.tasks
.where(t => !t.completed))
I created three sets of task queries for using in Daily/Weekly/Monthly Notes. In the following examples, Daily Note, Weekly Note and Month Note have the formats 2021-01-01
, 2021-W1
and 2021-11
, respectively, and they can be managed by using the Periodic Notes plugin: liamcain/obsidian-periodic-notes: Create/manage your daily, weekly, and monthly notes in Obsidian.
For instance, the three sets of queries can be inserted into template Daily/Weekly/Monthly Notes in the following way:
- In the Daily Note template, one can use the query for Daily, Weekly and Monthly Note tasks.
- In the Weekly Note template, one can use the query for Weekly and Monthly Note tasks.
- In the Monthly Note template, one can use the query for Monthly Note tasks.
Daily Note tasks:
==Due before and on = date({{date:YYYY-MM-DD}})
, and Due in 3 days:==
let pages = dv.pages('"Daily note"');
dv.taskList(
pages
.where(p => (dv.date(p.file.name) <= dv.date(dv.current().file.name) + dv.duration('3 days')) && !dv.equal(dv.date(p.file.name),dv.date(dv.current().file.name)))
.sort(p => p.file.name, 'desc')
.file
.tasks
.where(t => !t.completed))
Weekly Note tasks:
==Due before and in Week = date({{date:YYYY-MM-DD}}).weekyear
, and Due in Week = date({{date:YYYY-MM-DD}}).weekyear + 1
:==
let pages = dv.pages('"Weekly note"');
let dateToday = dv.date('{{date:YYYY-MM-DD}}');
dv.taskList(
pages
.where(p => (p.file.name <= dateToday.year.toString() + '-W' + (dateToday.weekNumber + 1).toString()))
.sort(p => p.file.name, 'desc')
.file
.tasks
.where(t => !t.completed))
Monthly Note tasks:
==Due before and in Month = date({{date:YYYY-MM-DD}}).month
, and Due in Month = date({{date:YYYY-MM-DD}}).month + 1
:==
let pages = dv.pages('"Monthly note"');
let dateToday = dv.date('{{date:YYYY-MM-DD}}');
dv.taskList(
pages
.where(p => (p.file.name <= dateToday.year.toString() + '-' + (dateToday.month + 1).toString()))
.sort(p => p.file.name, 'desc')
.file
.tasks
.where(t => !t.completed))
Sure !
The first line is the header, like Paper, Title, Year etcâŠ
All my files are in the folder Zettelkasten.
The line "[[" + b.file.name + "|" + (b.file.aliases[1] || b.file.aliases[0]) + "]]",
means to create a link for the first column, you can skip it.
The lines
b.file.aliases[0],
b.Year,
b.completion_reading,
b.completion_note,
are the informations I have added in the header of the files, like this
Here is what my header looks like.
Howdy again @dryice ! Sorry for the delay - holidays and all.
Letâs see if I can help! Iâll paste the first code block below, but first, a disclaimer - you should never, EVER, blindly paste code into dataviewjs if you donât understand it. Bad actors could utilize it to wreak havoc on your system. This isnât that but good practice moving forward
Now, the full code block:
const {update} = this.app.plugins.plugins["metaedit"].api;
const buttonMaker = (pn, pv, fpath) => {
const btn = this.container.createEl('button', {"text": "Finished!"});
const file = this.app.vault.getAbstractFileByPath(fpath)
btn.addEventListener('click', async (evt) => {
evt.preventDefault();
await update(pn, pv, file);
await update('completed-date',DateTime.local().toISODate(),file);
});
return btn;
}
let taskList = dv.pages("[[" + dv.current().file.name + "]]")
.filter(p => p.tags == "tasks")
.filter(p => p.status == "In Progress");
dv.table(["Task","Priority","Due Date","Pending Subtasks","Completed Subtasks"],
taskList.sort(t => t.priority)
.map(t => [t.file.link,
t.priority,
t["due-date"],
t.file.tasks.filter(t => !t.fullyCompleted).length,
t.file.tasks.filter(t => t.fullyCompleted).length,
buttonMaker('status','Finished',t.file.path)
])
)
To answer your questions directly:
-
You might want to read the MetaEdit docs to understand a little more thoroughly, but the âFIELDâ is the DataView/YAML field that you want to update (for instance, âcompletion-dateâ is a YAML field on my Task notes.
-
Hopefully the code block above helps, but if it doesnât, please let me know! Iâm not sure I quite understand the issue you might be having here.
Does anyone have an example of the YAML frontmatter for the âpeopleâ entries? I am struggling to get the code to work so this would be very helpful.