| | |
| | | |
| | | This piece of HTML represents an article with a leading header that says "An article header" and a paragraph that contains the text "Some content". This is normally combined with CSS to style the page and JavaScript to add interactivity. |
| | | |
| | | However, HTML doesn't let you create reusable templates. If you wanted to create a new page, you would need to copy and paste the above snippet and edit the header and content yourself. This isn't great if we have a lot of content on our site that shares a lot of similar layout. The smart people who created React also had similar thoughts, inventing the concept of JSX Components to solve the code duplication problem. |
| | | However, HTML doesn't let you create reusable templates. If you wanted to create a new page, you would need to copy and paste the above snippet and edit the header and content yourself. This isn't great if we have a lot of content on our site that shares a lot of similar layout. The smart people who created React also had similar thoughts, inventing the concept of JSX Components to solve the code duplication problem. |
| | | |
| | | In effect, components allow you to write a JavaScript function that takes some data and produces HTML as an output. **While Quartz doesn't use React, it uses the same component concept to allow you to easily express layout templates in your Quartz site.** |
| | | |
| | | > [!hint] |
| | | > For those coming from React, Quartz components are different from React components in that it only uses JSX for templating and layout. Hooks like `useEffect`, `useState`, etc. are not rendered. |
| | | ## An Example Component |
| | | |
| | | ### Constructor |
| | | |
| | | Component files are written in `.tsx` files that live in the `quartz/components` folder. These are re-exported in `quartz/components/index.ts` so you can use them in layouts and other components more easily. |
| | | |
| | | Each component file should have a default export that satisfies the `QuartzComponentConstructor` function signature. It is a function that takes in a single optional parameter `opts` and returns a Quartz Component. The type of the parameters `ops` is defined by the interface `Options` which you as the component creator also decide. |
| | |
| | | } |
| | | |
| | | const defaultOptions: Options = { |
| | | favouriteNumber: 42 |
| | | favouriteNumber: 42, |
| | | } |
| | | |
| | | export default ((userOpts?: Options) => { |
| | | const opts = { ...userOpts, ...defaultOpts } |
| | | function YourComponent(props: QuartzComponentProps) { |
| | | if (opts.favouriteNumber < 0) { |
| | | return null |
| | | } |
| | | |
| | | if (opts.favouriteNumber < 0) { |
| | | return null |
| | | } |
| | | |
| | | return <p>My favourite number is {opts.favouriteNumber}</p> |
| | | } |
| | | |
| | |
| | | ``` |
| | | |
| | | ### Props |
| | | |
| | | The Quartz component itself (lines 11-17 highlighted above) looks like a React component. It takes in properties (sometimes called [props](https://react.dev/learn/passing-props-to-a-component)) and returns JSX. |
| | | |
| | | All Quartz components accept the same set of props which are defined in `QuartzComponentProps`: |
| | |
| | | ``` |
| | | |
| | | - `fileData`: Any metadata [[making plugins|plugins]] may have added to the current page. |
| | | - `fileData.slug`: slug of the current page. |
| | | - `fileData.frontmatter`: any frontmatter parsed. |
| | | - `fileData.slug`: slug of the current page. |
| | | - `fileData.frontmatter`: any frontmatter parsed. |
| | | - `cfg`: The `configuration` field in `quartz.config.ts`. |
| | | - `tree`: the resulting [HTML AST](https://github.com/syntax-tree/hast) after processing and transforming the file. This is useful if you'd like to render the content using [hast-util-to-jsx-runtime](https://github.com/syntax-tree/hast-util-to-jsx-runtime) (you can find an example of this in `quartz/components/pages/Content.tsx`). |
| | | - `allFiles`: Metadata for all files that have been parsed. Useful for doing page listings or figuring out the overall site structure. |
| | | - `displayClass`: a utility class that indicates a preference from the user about how to render it in a mobile or desktop setting. Helpful if you want to conditionally hide a component on mobile or desktop. |
| | | |
| | | ### Styling |
| | | |
| | | Quartz components can also define a `.css` property on the actual function component which will get picked up by Quartz. This is expected to be a CSS string which can either be inlined or imported from a `.scss` file. |
| | | |
| | | Note that inlined styles **must** be plain vanilla CSS. |
| | |
| | | ```tsx {6-10} title="quartz/components/YourComponent.tsx" |
| | | export default (() => { |
| | | function YourComponent() { |
| | | return <p>Example Component</p> |
| | | return <p class="red-text">Example Component</p> |
| | | } |
| | | |
| | | YourComponent.css = ` |
| | | p { |
| | | p.red-text { |
| | | color: red; |
| | | } |
| | | ` |
| | |
| | | ``` |
| | | |
| | | > [!warning] |
| | | > Quartz does not use CSS modules so any styles you declare here apply *globally*. If you only want it to apply to your component, make sure you use specific class names and selectors. |
| | | > Quartz does not use CSS modules so any styles you declare here apply _globally_. If you only want it to apply to your component, make sure you use specific class names and selectors. |
| | | |
| | | ### Scripts and Interactivity |
| | | - listening for the nav event |
| | | - best practice: anything here should unmount any existing event handlers to prevent memory leaks |
| | | |
| | | What about interactivity? Suppose you want to add an-click handler for example. Like the `.css` property on the component, you can also declare `.beforeDOMLoaded` and `.afterDOMLoaded` properties that are strings that contain the script. |
| | | |
| | | ```tsx title="quartz/components/YourComponent.tsx" |
| | | export default (() => { |
| | | function YourComponent() { |
| | | return <button id="btn">Click me</button> |
| | | } |
| | | |
| | | YourComponent.beforeDOM = ` |
| | | console.log("hello from before the page loads!") |
| | | ` |
| | | |
| | | YourComponent.afterDOM = ` |
| | | document.getElementById('btn').onclick = () => { |
| | | alert('button clicked!') |
| | | } |
| | | ` |
| | | return YourComponent |
| | | }) satisfies QuartzComponentConstructor |
| | | ``` |
| | | |
| | | > [!hint] |
| | | > For those coming from React, Quartz components are different from React components in that it only uses JSX for templating and layout. Hooks like `useEffect`, `useState`, etc. are not rendered and other properties that accept functions like `onClick` handlers will not work. Instead, do it using a regular JS script that modifies the DOM element directly. |
| | | |
| | | As the names suggest, the `.beforeDOMLoaded` scripts are executed _before_ the page is done loading so it doesn't have access to any elements on the page. This is mostly used to prefetch any critical data. |
| | | |
| | | The `.afterDOMLoaded` script executes once the page has been completely loaded. This is a good place to setup anything that should last for the duration of a site visit (e.g. getting something saved from local storage). |
| | | |
| | | If you need to create an `afterDOMLoaded` script that depends on _page specific_ elements that may change when navigating to a new page, you can listen for the `"nav"` event that gets fired whenever a page loads (which may happen on navigation if [[SPA Routing]] is enabled). |
| | | |
| | | ```ts |
| | | document.addEventListener("nav", () => { |
| | | // do page specific logic here |
| | | // e.g. attach event listeners |
| | | const toggleSwitch = document.querySelector("#switch") as HTMLInputElement |
| | | toggleSwitch.removeEventListener("change", switchTheme) |
| | | toggleSwitch.addEventListener("change", switchTheme) |
| | | }) |
| | | ``` |
| | | |
| | | It is best practice to also unmount any existing event handlers to prevent memory leaks. |
| | | |
| | | #### Importing Code |
| | | |
| | | Of course, it isn't always practical (nor desired!) to write your code as a string literal in the component. |
| | | |
| | | Quartz supports importing component code through `.inline.ts` files. |
| | | |
| | | ```tsx title="quartz/components/YourComponent.tsx" |
| | | // @ts-ignore: typescript doesn't know about our inline bundling system |
| | | // so we need to silence the error |
| | | import script from "./scripts/graph.inline" |
| | | |
| | | export default (() => { |
| | | function YourComponent() { |
| | | return <button id="btn">Click me</button> |
| | | } |
| | | |
| | | YourComponent.afterDOM = script |
| | | return YourComponent |
| | | }) satisfies QuartzComponentConstructor |
| | | ``` |
| | | |
| | | ```ts title="quartz/components/scripts/graph.inline.ts" |
| | | // any imports here are bundled for the browser |
| | | import * as d3 from "d3" |
| | | |
| | | document.getElementById("btn").onclick = () => { |
| | | alert("button clicked!") |
| | | } |
| | | ``` |
| | | |
| | | Additionally, like what is shown in the example above, you can import packages in `.inline.ts` files. This will be bundled by Quartz and included in the actual script. |
| | | |
| | | ### Using a Component |
| | | #### In a layout |
| | | #### In the configuration |
| | | |
| | | After creating your custom component, re-export it in `quartz/components/index.ts`: |
| | | |
| | | ```ts title="quartz/components/index.ts" {4,10} |
| | | import ArticleTitle from "./ArticleTitle" |
| | | import Content from "./pages/Content" |
| | | import Darkmode from "./Darkmode" |
| | | import YourComponent from "./YourComponent" |
| | | |
| | | export { ArticleTitle, Content, Darkmode, YourComponent } |
| | | ``` |
| | | |
| | | Then, you can use it like any other component in `quartz.layout.ts` via `Component.YourComponent()`. See the [[configuration#Layout|layout]] section for more details. |
| | | |
| | | As Quartz components are just functions that return React components, you can compositionally use them in other Quartz components. |
| | | |
| | | ```tsx title="quartz/components/AnotherComponent.tsx" |
| | | import YourComponent from "./YourComponent" |
| | | |
| | | export default (() => { |
| | | function AnotherComponent(props: QuartzComponentProps) { |
| | | return ( |
| | | <div> |
| | | <p>It's nested!</p> |
| | | <YourComponent {...props} /> |
| | | </div> |
| | | ) |
| | | } |
| | | |
| | | return AnotherComponent |
| | | }) satisfies QuartzComponentConstructor |
| | | ``` |
| | | |
| | | > [!hint] |
| | | > Look in `quartz/components` for more examples of components in Quartz as reference for your own components! |