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>
);
7 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;
2 Likes

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();
2 Likes
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>

);

Thank you! Keep up the good work.

1 Like

Have you been able to use text input fields?

The moment i put input fields in my modal, I’ve found that obsidian wants to keep stealing focus. So no typign allowed. T_T

https://imgur.com/OtgCjKE

I’m trying to come up with a set of components so i ca compose some common workflows:

useInput

```jsx:component:useFormInput
const [value, setValue] = useState(props?.initialValue);

return {
  onChange: (event) => setValue(() => event?.target?.value),
  value
};

RelatedRecordSelect

```jsx:component:RelatedRecordSelect

const { query, ...inputProps } = props || {};

const dataview = app.plugins.plugins.dataview.api;

const items = useMemo(() => {
    if (typeof query === 'function')
        return query(dataview);
  
    if (typeof query === 'string') 
        return dataview.pages(query).file.name;

    return [];
}, [query]);


return (
	<select { ...(inputProps || {})}>
		{items.map((content) => (
			<option value={content}>{content}</option>
		))}
	</select>
);

Modal

```jsx:component:Modal

  return (
    !!props.isOpen && (
      <div
        style={{
          position: "fixed",
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          zIndex: 1000,
          background: "#00000099",
          backdropFilter: "blur(4px)",
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          pointerEvents: "none",
        }}
      >
        <div
          style={{
            display: "flex",
            position: "relative",
            flexDirection: "column",
            background: "#333",
            color: "#999",
            borderRadius: 4,
            border: "1px solid #363636",
            gap: 4,
            maxWidth: 512,
            width: "100%",
            boxShadow: "0 10p 10px 20px black",
            pointerEvents: "auto",
          }}
        >
          <button
            style={{
              position: "absolute",
              right: 10,
            }}
            onClick={props.onCloseClick}
          >
            close
          </button>

          {props.title && (
            <header
              style={{
                height: 48,
                padding: 5,
                borderBottom: "1px solid #363636",
              }}
            >
              {props.title}
            </header>
          )}
          <div
            style={{
              padding: 5,
            }}
          >
            {props.children}
          </div>

          {props.actions && (
            <div
              style={{
                borderTop: "1px solid #363636",
                display: "flex",
                flexDirection: "row",
                padding: 5,
                gap: 4,
              }}
            >
              {props.actions}
            </div>
          )}
        </div>
      </div>
    ) || null
  );

And the first use case:

CreateProjectModal

```jsx:component:CreateProjectModal

const name = Forms.useFormInput("")
const project = Forms.useFormInput("")

const handleSubmit = () => {
  props.onCloseClick()
}

return (
	<Components.Modal
		title="Create Project"
		isOpen={!!props.isOpen}
		onCloseClick={props?.onClose}
		actions={(
		  <>
			 <button onClick={handleSubmit}>save</button>
		  </>
		)}
    >
		<form onSubmit={handleSubmit}>
			<input type="text" {...name} />
			<RelatedRecordSelect query={"#project"} {...project} />
		</form>
	</Components.Modal>
)

Using the new canvas feature will throw up an error if the react-components plugin is enabled. A rough fix has already been submitted to the react-components github repo by MeepTech, but there’s no sign of activity. I love being able to create custom components to embed inside of my notes so I threw together a temporary fix to get rid of the error inside a canvas.

This is a crude sed command to replace the single line in the main.js file of the react-components plugin to get rid of the rendering error in the canvas. (Does not enable component rendering in the canvas, only tested on linux)

Run this from a terminal opened to the plugin folder: ~/Vault/.obsidian/plugins/obsidian-react-components/

sed -i "/hasClass('markdown-preview-section')/ s/^\( *\).*/\1if (\!ctx.sourcePath || (\!((_a = ctx.containerEl) === null || _a === void 0 \? void 0 \: _a.hasClass('markdown-preview-section')))) {/" ./main.js 

The command finds and updates the line from this:

if (!((_a = ctx.containerEl) === null || _a === void 0 ? void 0 : _a.hasClass('markdown-preview-section'))) {

To this:

if (!ctx.sourcePath || (!((_a = ctx.containerEl) === null || _a === void 0 ? void 0 : _a.hasClass('markdown-preview-section')))) {
1 Like