React Components showcase

Hi all

I have recently discovered that you can add React components to Obsidian through that plugin.
This is quite powerful and i have already added a live clock to my Obsidian dashboard (code below)! I am quite new with React and would love to see what the community has been implementing for React components!!

Please share your snippets, animations, embedded music players and much more!

Many thanks in advance


Code for the live clock is below:

const [date, setDate] = useState(new Date());
useEffect(() => {
 var timerID = setInterval( () => setDate(new Date()), 1000 );
 return function cleanup() {
  clearInterval(timerID);
 };
});
return (
 <div>
  <p>Current date: <b>{date.toLocaleDateString()}</b></p>
   <p>Current time: <b>{date.toLocaleTimeString()}</b></p>
 </div>
);
6 Likes

Greetings. I am about 2 months deep into Obsidian and have not really touched React before. I saw the Obsidian React Components plugin and thought it would be a good way to get some more Notion functionality in my templates. In particular I wanted a dropdown select list to update front matter on a note. I can read the front matter data but I haven’t found out how to save it. Below is an example of what I have so far.

SelectIceCreamComponent.md:

import Select from 'https://cdn.skypack.dev/react-select';

const options = [
  { value: 'chocolate', label: 'Chocolate' },
  { value: 'strawberry', label: 'Strawberry' },
  { value: 'vanilla', label: 'Vanilla' },
];

const [selectedOption, setSelectedOption] = useState();

const ctx = useContext(ReactComponentContext);
const frontmatter = ctx.markdownPostProcessorContext.frontmatter;

const handleChange = (e) => {
  setSelectedOption(e);
  //frontmatter['ice-cream'] = e;
  //console.log('frontmatter:', frontmatter);
};

if (frontmatter) {
  console.log('frontmatter', frontmatter);
  const label = frontmatter['ice-cream'];
  console.log('initial setting', label);
  const found = options.find(x => x.label === label);
  console.log('found', found);
  //if (found) handleChange(found);
}

return (
  <Select
    isClearable
    value={selectedOption}
    onChange={handleChange}
    options={options}
  />
);

Test React Select.md:

---

ice-cream: Vanilla

---

# 🍦 Favorite

🍦 Favorite

name:: Johnny

occupation:: Note Taker

\`\`\`jsx:

<SelectIceCreamComponent />

\`\`\`

Any help figuring this out will be appreciated.

1 Like

Back again. I figured it out. React is still new to me. The Obsidian React Components plugin has access to Obsidian’s app: App. Below is what I used to create a generic React select dropdown. For brevity I did not include helper methods (ObsidianApp()), but I can include them upon request.

Obsidian View
React SelectComponent View2

SelectIceCreamComponent.md:

  const DEFAULT_VALUE = 'default';
  const DEFAULT_LABEL = 'Select...';

  const [init, setInit] = useState(false);

  const [options] = useState(props.options
    .split(',')
    .reduce( (previous, current, index) => {
      if (index === 0)
        previous.push({ value: DEFAULT_VALUE, label: DEFAULT_LABEL});

      previous.push({
        value: current.toLocaleLowerCase(),
        label: current,
      });
      return previous;
    }, [])
  );
  const [selectedOption, setSelectedOption] = useState(DEFAULT_VALUE);

  const initialize = async () => {
    setInit(true);

    const file = ObsidianApp().getFile();
    const fileContents = await ObsidianApp().getFileContent(file);

    const { value } = ObsidianApp().getFieldValueAndPosition(props.fieldName, fileContents);
      
    const fileOption = options.find(x => x.label === value)?.value || DEFAULT_VALUE;

    if (selectedOption !== fileOption) {
      setSelectedOption(fileOption);
    }
  };

  const updateContent = async () => {
    const fieldValue = options.find(x =>
      selectedOption !== DEFAULT_VALUE
      && x.value === selectedOption)?.label
      || '';

    ObsidianApp().setFileFieldContent(props.fieldName, fieldValue);
  };

  useEffect(() => {
    initialize();
  }, [])

  useEffect(() => {
    if (!init) return;

    updateContent();
  }, [selectedOption])

  const handleChange = (e) => {
    const value = e?.target?.value || DEFAULT_VALUE;
    setSelectedOption(value);
  };

  return (
    <>
        <select
          className="dropdown"
          style={{width: "400px"}}
          onChange={handleChange}
          value={selectedOption}
        >
          { options?.map(option => {
              return option.value === DEFAULT_VALUE ?
              (
                <option value={option.value} disabled hidden>{option.label}</option>
              ):
              (
                <option value={option.value}>{option.label}</option>
              );
            })
          }
        </select>
    </>
  );

Test React Select.md:

# 🍦 Favorite

🍦 Favorite

name:: Johnny
occupation:: Note Taker
ice-cream:: Vanilla

\`\`\`jsx:
  <SelectComponent
    fieldName={"ice-cream"}
    options={"Chocolate,Strawberry,Vanilla,Cheesecake"}
  />
\`\`\`

The above code works on mobile as well (at least I tried it on the iPhone). I hope this helps someone, saves them some time, and/or sparks other ideas. For my next trick I need a Date/Time picker :blush:

4 Likes

Hi! Would you include the helper method, please. Your solution is very interesting, but it does not work in my vault (I have Elias’s react plugin). Naturally it could be my lack of knowledge as I am new to react as well.

Hi again,

I got the next message: “ReferenceError: ObsidianApp is not defined”. Is it because I don’t have the helper function you mentioned or something else?

Thank you for your answer in advance,
codexmonk

@CodeXmonk, sorry for taking so long to reply. The solution I posted does require a helper function to interact with Obsidian. Below is the source code for the helper function. With the code below I removed the need to get the file prior to getting the file contents. So in the original code remove const file = Obsidian().getFile(); and modify getting the file contents to be const fileContents = await ObsidianApp().getFileContent();.

Helper function named: ObsidianApp.md

  const ObsidianAppFunction = (function() {
    const FIELD_POST_CHAR = '::'; // frontmatter is :
    
    return {
      getFile: getFile,
      getFileContent: getFileContent,
      getFieldValueAndPosition: getFieldValueAndPosition,
      setFileFieldContent: setFileFieldContent,
    };

    function getFile() {
      const activeView = app.workspace.activeLeaf.view;
      return activeView.file;
    }

    async function getFileContent() {
      const file = getFile();
      
      const fileContents = await app.vault.read(file);
      return fileContents;
    }

    function getArrayFromString(fileContents) {
      return fileContents?.split('\n');
    }

    function getFieldValueAndPosition(fieldName, fileContents) {
      const dataArray = getArrayFromString(fileContents);
      const pos = dataArray
        .findIndex(x => x.includes(`${fieldName}${FIELD_POST_CHAR}`));
      const value = dataArray[pos]?.split(FIELD_POST_CHAR)[1]?.trim();
      
      return ({ value: value, pos: pos });
    }

    async function setFileFieldContent(fieldName, fieldValue) {
      const file = getFile();

      const fileContents = await getFileContent(file);

      const dataArray = getArrayFromString(fileContents);

      const { value, pos } = getFieldValueAndPosition(fieldName, fileContents);

      const fieldNameWithValue = `${fieldName}${FIELD_POST_CHAR} ${fieldValue}`;

      dataArray.splice(pos, 1, fieldNameWithValue);

      if (`${fieldName} ${value}` !== fieldNameWithValue) {
        await app.vault.modify(file, dataArray.join('\n'));
      } 
    }
  })();

  return ObsidianAppFunction;
1 Like

Date/Time picker React component for Obsidian. This uses the ObsidianApp helper function mentioned above. Tested on Mac and iPhone.

Obsidian View

Date Time Picker Component Example.md:

  # Date Time Picker Component Example

  Component example that allows a user to pick a date/time and fills in a markdown field with the selected results

  My Date:: 2022-02-02

  \```jsx:
    <DateTimePickerComponent
      fieldName="My Date"
    />
  \```

DateTimePickerComponent.md

  const DateTimePickerComponentFunction = () => {
    const [dateTime, setDateTime] = useState(null);
    const [isWithTime, setIsWithTime] = useState(false);
    const [init, setInit] = useState(false);

    const isDark = document.querySelector('body.theme-dark') !== null;

    const inputStyle = {
      'color-scheme': isDark ? 'dark' : 'light',
      backgroundColor: 'var(--background-modifier-form-field)',
      border: '1px solid var(--background-modifier-border)',
      color: 'var(--text-normal)',
      fontFamily: "'Inter', sans-serif",
      padding: '5px 14px',
      fontSize: '16px',
      borderRadius: '4px',
      outline: 'none',
      height: '30px',
    };
    
    const initialize = async () => {
      const fileDateTimeValue = await getFileDateTimeValue();
      const fileDateTime = fileDateTimeValue ? fileDateTimeValue : null;

      if (
        fileDateTime !== dateTime
        && fileDateTime !== null
      ) {
        updateDateTime(fileDateTime);
      }

      setInit(true);
    }

    const getFileDateTimeValue = async () => {
      const fileContents = await ObsidianApp().getFileContent();
    
      return ObsidianApp().getFieldValueAndPosition(props.fieldName, fileContents)?.value;
    }

    const updateContent = async () => {
      const fileDateTimeValue = await getFileDateTimeValue();
      const fileDateTime = fileDateTimeValue ? fileDateTimeValue : null;

      const newTime = isWithTime ? ` ${getDateFromString(dateTime)
          .toLocaleString('en-US', { timeStyle: 'short'})}` :
        '';

      const newDate = `${getDateFromString(dateTime)
        .toISOString()
          .split('T')[0]}${newTime}`;

      if (fileDateTime !== newDate) {
        ObsidianApp().setFileFieldContent(props.fieldName, newDate);
      }
    };
    
    const getDateFromString = (dateString) => {
      return new Date(dateString.replace(/-/g, '/'));
    }

    const updateDateTime = (newDateTimeString) => {
      const hasTime = newDateTimeString.includes('M');

      if (isWithTime !== hasTime) {
        setIsWithTime(hasTime);
      }
  
      setDateTime(newDateTimeString);
    };

    useEffect(() => {
      initialize();
    }, []);

    useEffect(() => {
      if (!init) return;

      updateContent();
    }, [dateTime]);

    const toggleDate = (e) => {
      const newValue = !isWithTime;

      setIsWithTime(newValue);
    };

    const handleDateChange = (e) => {
      setDateTime(e.target.value.replace('T', ' '));
    };

    return (
      <>
        <div style={{display: 'flex'}}>
          {isWithTime ? (
            <>
            <label className="input-label" htmlFor="datetime-local-picker">Pick a date </label>
            <input 
              style={inputStyle}
              type="datetime-local"
              name="datetime-local-picker"
              value={dateTime ? getDateFromString(`${dateTime} GMT`).toISOString().slice(0, 16) : null}
              onChange={handleDateChange}
              id="datetime-local-picker" />
            </>
          ) : (
            <>
            <label className="input-label" htmlFor="date-picker">Pick a date </label>
            <input
              style={inputStyle}
              type="date"
              name="date-picker"
              value={dateTime?.split(' ')[0]}
              onChange={handleDateChange}
              id="date-picker" />
            </>
          )}
          <>
            <div style={{display: 'flex'}}>
              <label>&nbsp; Include time? </label>
              <div class="setting-item-control" onClick={toggleDate}>
                <div className={`checkbox-container ${isWithTime ? 'is-enabled' : ''}`}></div>
              </div>
            </div>
          </>
        </div>
      </>
    );
  };

  return DateTimePickerComponentFunction();
1 Like
import Select from 'https://cdn.skypack.dev/react-select';

const [contents, setContents] = useState('');

useEffect(() => {
    // You need to restrict it at some point
    // This is just dummy code and should be replaced by actual
    getContents();
  }, []);

const getContents = async () => {
	const dv = app.plugins.plugins.dataview.api;
	const notes = dv.pages('"booknotes/How to Take Smart Notes"').filter(b => b.created && dv.date('today').ts - b.created.ts < 20 * 60 * 60 * 24 * 1000).sort(b => b.page).values;
	console.log(notes);
	
	const contents = await Promise.all(notes.map(async (note) => {
		const content = (await app.vault.cachedRead(app.vault.getAbstractFileByPath(note.file.path)));
		return content;
	}))
	console.log(contents[0]);
    setContents(contents);
  };

return (

<div contentEditable={true}>
<Select />
{contents.map((content) => (


<Markdown src={content} />
))}

</div>

);