TypeScript Tutorial: Building a Simple Markdown Previewer

In the world of web development, we often encounter the need to display formatted text. While HTML provides a robust way to structure content, writing HTML directly can be cumbersome, especially for lengthy articles or notes. This is where Markdown comes in – a lightweight markup language that allows you to write formatted text using a plain text editor. Wouldn’t it be great to see how your Markdown looks instantly, without having to refresh a page or manually convert it? In this tutorial, we will walk you through building a simple Markdown previewer using TypeScript, a project that is perfect for beginners to intermediate developers to learn more about front-end development, DOM manipulation, and the power of TypeScript.

Understanding Markdown

Markdown is designed to be easy to read and write. It uses simple syntax to format text. Here are a few examples:

  • Headers: Use `#` for `

    `, `##` for `

    `, and so on.

  • Emphasis: Use `*` or `_` for italics, and `**` or `__` for bold.
  • Lists: Use `*`, `-`, or `+` for unordered lists, and numbers for ordered lists.
  • Links: `[Link text](URL)`
  • Images: `![Alt text](URL)`

For a comprehensive guide, check out the Markdown Guide.

Setting Up Your Project

Let’s get started by setting up our project. We will use `npm` (Node Package Manager) to manage our dependencies and `parcel` as a bundler to simplify development.

  1. Create a Project Directory: Create a new directory for your project, for example, `markdown-previewer`.
  2. Initialize npm: Navigate to your project directory in your terminal and run npm init -y. This creates a `package.json` file.
  3. Install Dependencies: Install `typescript`, `parcel-bundler`, and a Markdown parser like `marked` (we’ll use this) by running: npm install typescript parcel-bundler marked --save-dev.
  4. Create TypeScript Configuration: Create a `tsconfig.json` file in your project root with the following content. This file configures how TypeScript compiles your code.
{
  "compilerOptions": {
    "outDir": "dist",
    "module": "esnext",
    "target": "es5",
    "jsx": "preserve",
    "sourceMap": true,
    "moduleResolution": "node",
    "lib": ["dom", "esnext"]
  },
  "include": ["src/**/*"]
}
  1. Project Structure: Create a basic project structure:
    • markdown-previewer/
    • ├── src/
    • │ ├── index.html
    • │ ├── index.ts
    • ├── package.json
    • ├── tsconfig.json
    • └── dist/

Writing the HTML

Let’s create a simple HTML file (`src/index.html`) that has a text area for the Markdown input and a div to display the preview.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Markdown Previewer</title>
</head>
<body>
    <textarea id="markdown-input" rows="10" cols="50" placeholder="Enter Markdown here..."></textarea>
    <div id="preview"></div>
    <script src="index.ts"></script>
</body>
</html>

Writing the TypeScript Code

Now, let’s write the TypeScript code (`src/index.ts`) that will handle the Markdown conversion and display the preview.

import { marked } from 'marked';

// Get references to the textarea and preview div
const markdownInput = document.getElementById('markdown-input') as HTMLTextAreaElement;
const previewDiv = document.getElementById('preview') as HTMLDivElement;

// Function to update the preview
function updatePreview() {
  if (markdownInput && previewDiv) {
    // Get the markdown text from the textarea
    const markdownText = markdownInput.value;

    // Convert markdown to HTML using marked
    const html = marked.parse(markdownText);

    // Set the HTML to the preview div
    previewDiv.innerHTML = html;
  }
}

// Add an event listener to the textarea to update the preview on input
if (markdownInput) {
  markdownInput.addEventListener('input', updatePreview);
}

// Initial preview update (in case there's any initial content)
updatePreview();

Let’s break down the code:

  • Import `marked`: We import the `marked` library to parse Markdown.
  • Get DOM Elements: We get references to the textarea (where the user types the Markdown) and the preview div (where the converted HTML will be displayed). We use type assertions (as HTMLTextAreaElement and as HTMLDivElement) to tell TypeScript the expected types of these elements. This helps with type safety.
  • `updatePreview()` Function: This function is the core of our previewer. It does the following:
    • Gets the Markdown text from the textarea.
    • Uses `marked.parse()` to convert the Markdown text into HTML.
    • Sets the `innerHTML` of the preview div to the generated HTML.
  • Event Listener: We add an event listener to the textarea. Whenever the user types something in the textarea (the `input` event), the `updatePreview()` function is called.
  • Initial Update: We call `updatePreview()` once when the page loads to handle any initial content in the textarea.

Building and Running the Application

Now, let’s build and run our application:

  1. Build with Parcel: In your terminal, run npx parcel src/index.html. Parcel will bundle your code, including the HTML, TypeScript, and dependencies, and create a `dist` directory.
  2. Run the Application: Open the `dist/index.html` file in your browser. You should see a text area and an empty div.
  3. Test it Out: Type some Markdown into the text area. As you type, the preview div should update in real-time.

Styling the Previewer

To make the previewer look better, let’s add some basic CSS. Create a file named `src/style.css` and add the following styles:

body {
  font-family: sans-serif;
  margin: 20px;
}

textarea {
  width: 100%;
  margin-bottom: 10px;
  padding: 10px;
  box-sizing: border-box;
}

#preview {
  border: 1px solid #ccc;
  padding: 10px;
}

Include this CSS file in your `index.html` inside the “ tag:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Markdown Previewer</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <textarea id="markdown-input" rows="10" cols="50" placeholder="Enter Markdown here..."></textarea>
    <div id="preview"></div>
    <script src="index.ts"></script>
</body>
</html>

Rebuild your project with `npx parcel src/index.html` and refresh your browser. The previewer should now have basic styling.

Advanced Features and Improvements

Let’s explore some ways to enhance our Markdown previewer.

1. Adding a Toolbar

A toolbar with buttons for common Markdown formatting options can greatly improve the user experience. Here’s how you can add a simple toolbar:

  1. Add HTML for the Toolbar: Inside your `index.html`, add a `<div>` for the toolbar above the textarea.
<div id="toolbar">
    <button data-command="bold">Bold</button>
    <button data-command="italic">Italic</button>
    <button data-command="link">Link</button>
    <button data-command="heading">Heading</button>
</div>
  1. Add Styles for the Toolbar: In your `style.css`, add some basic styling for the toolbar.
#toolbar {
  margin-bottom: 10px;
}

#toolbar button {
  margin-right: 5px;
  padding: 5px 10px;
  border: 1px solid #ccc;
  background-color: #f0f0f0;
  cursor: pointer;
}
  1. Add Event Listeners to Toolbar Buttons: In your `index.ts`, get references to the toolbar buttons and add event listeners.
const toolbar = document.getElementById('toolbar');

if (toolbar) {
  toolbar.addEventListener('click', (event: MouseEvent) => {
    const target = event.target as HTMLElement;
    if (target.tagName === 'BUTTON') {
      const command = target.dataset.command;
      if (command) {
        switch (command) {
          case 'bold':
            insertText('**', '**');
            break;
          case 'italic':
            insertText('*', '*');
            break;
          case 'link':
            insertText('[', '](url)');
            break;
          case 'heading':
            insertText('# ', '');
            break;
        }
      }
    }
  });
}
  1. Implement the `insertText` Function: This function will insert text at the current cursor position in the textarea.
function insertText(startTag: string, endTag: string) {
  if (!markdownInput) return;

  const start = markdownInput.selectionStart;
  const end = markdownInput.selectionEnd;
  const text = markdownInput.value;

  if (start != null && end != null) {
    const selectedText = text.substring(start, end);
    const newText = startTag + selectedText + endTag;
    markdownInput.value = text.substring(0, start) + newText + text.substring(end);
    markdownInput.focus();
    markdownInput.selectionStart = start + startTag.length;
    markdownInput.selectionEnd = start + startTag.length + selectedText.length;
    updatePreview();
  }
}

This code adds event listeners to the buttons. When a button is clicked, it calls the `insertText` function. The `insertText` function inserts the appropriate Markdown tags at the current cursor position, updating the textarea content. The `updatePreview()` function is then called to reflect the changes.

2. Implementing Live Preview for Code Blocks

By default, `marked` doesn’t provide syntax highlighting. We can add this using a library like `highlight.js`.

  1. Install `highlight.js`: npm install highlight.js --save
  2. Import and Configure: Import `highlight.js` in your `index.ts`.
import { marked } from 'marked';
import hljs from 'highlight.js';

// Configure marked to use highlight.js
marked.setOptions({
  highlight: function(code: string, lang: string) {
    return hljs.highlight(code, { language: lang }).value;
  }
});
  1. Add highlight.js Styles: Include a CSS theme for `highlight.js` in your `index.html`. You can find different themes on the `highlight.js` website. For example:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">

Now, code blocks in your Markdown will be syntax-highlighted in the preview.

3. Adding Error Handling

What happens if the `marked.parse()` function throws an error? It’s good practice to handle potential errors gracefully.

function updatePreview() {
  if (markdownInput && previewDiv) {
    const markdownText = markdownInput.value;
    try {
      const html = marked.parse(markdownText);
      previewDiv.innerHTML = html;
    } catch (error) {
      console.error('Markdown parsing error:', error);
      previewDiv.innerHTML = '<div class="error">Error parsing Markdown.</div>';
    }
  }
}

This adds a `try…catch` block to handle potential errors during the Markdown parsing process. If an error occurs, it logs the error to the console and displays an error message in the preview div.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Incorrect Import Paths: Double-check the import paths for your modules (e.g., `marked`, `highlight.js`). Parcel can sometimes be particular about import paths. Make sure they are correct relative to your `index.ts` file.
  • Type Errors: TypeScript helps prevent errors, but you may still encounter them. Make sure you are using type assertions (e.g., as HTMLTextAreaElement) and that your types are correct. Use the TypeScript compiler to catch type errors before runtime.
  • Missing Dependencies: Ensure that you have installed all necessary dependencies using `npm install`.
  • Incorrect HTML Structure: Make sure your HTML is well-formed. Missing closing tags or other HTML errors can cause unexpected behavior. Use your browser’s developer tools to check for HTML errors.
  • Incorrect CSS Selectors: If your CSS isn’t working, double-check your CSS selectors to make sure they are targeting the correct elements.
  • Browser Caching: If you are making changes to your code and the changes aren’t appearing in your browser, try clearing your browser’s cache or force-refreshing the page (Ctrl+Shift+R or Cmd+Shift+R).

Key Takeaways

  • Markdown is a powerful and easy-to-learn markup language. It’s much simpler to write than raw HTML.
  • TypeScript provides strong typing and helps prevent errors. This makes your code more robust and easier to maintain.
  • Parcel is a convenient bundler for development. It simplifies the build process.
  • You can easily extend your Markdown previewer. Add features like a toolbar, syntax highlighting, and error handling to make it even more useful.
  • Understanding DOM manipulation is fundamental to front-end development. This project gives you hands-on experience with this.

FAQ

  1. Why use TypeScript for a Markdown previewer? TypeScript adds type safety, making your code easier to read, maintain, and debug. It also helps you catch errors early in the development process.
  2. Can I use a different Markdown parser? Yes! There are many Markdown parsers available. `marked` is a popular choice, but you could also use other libraries like `markdown-it`. The core logic of the previewer will remain the same; only the import and parsing method will change.
  3. How can I deploy this previewer? You can deploy this previewer by deploying the contents of your `dist` folder to a web server. Services like Netlify, Vercel, or GitHub Pages make this easy.
  4. How can I add more features? You can add features such as image upload, support for tables, and custom CSS styling. Experiment with different Markdown features and libraries to enhance the functionality.
  5. Why are the toolbar buttons not working? Make sure you have correctly implemented the event listeners for the toolbar buttons and that the `insertText` function is correctly inserting the Markdown tags. Also, check for any JavaScript errors in your browser’s console.

This simple Markdown previewer is a great starting point for learning TypeScript and front-end development. It demonstrates how to combine the simplicity of Markdown with the power of TypeScript and DOM manipulation to create a useful and interactive web application. By adding features like a toolbar, syntax highlighting, and error handling, you can easily customize and extend this project to meet your specific needs. The process of building such a tool allows for a deeper understanding of front-end web development principles, and offers a fun way to explore the capabilities of TypeScript. Continue to experiment, learn, and grow your skills to become a more proficient and capable developer. The best way to solidify your knowledge is through practice and exploration, so take this project and make it your own.