DEV Community

Cleo Buenaventura
Cleo Buenaventura

Posted on

A Final Challenge: The Progress

It's been a week since I started working on this issue in the onlook repo.

GitHub logo onlook-dev / onlook

The open source, local-first Figma for React. Design directly in your live React app and publish your changes to code.

Figma for your React App

Onlook

The first browser-powered visual editor
Explore the docs »

View Demo · Report Bug · Request Feature

Discord LinkedIn Twitter

Table of Contents
  1. Installation
  2. Usage
  3. Roadmap
  4. Contributing
  5. Contact
  6. Acknowledgments
  7. License

The open-source visual editor for your React Apps

Seamlessly integrate with any website or web app running on React + TailwindCSS, and make live edits directly in the browser DOM. Customize your design, control your codebase, and push changes your changes without compromise.

Onlook.Studio.Component.Demo.for.GitHub.mp4

Export-1724891449817

Built With

  • React
  • Electron
  • Tailwind
  • Vite

Stay up-to-date

Onlook officially launched our first version of Onlook on July 08, 2024 and we've shipped a ton since then. Watch releases of this repository to be notified of future updates, and you can follow along with us on LinkedIn or Substack where we write a weekly newsletter.

Getting Started

image

Installation

  1. Visit onlook.dev to download the app.
  2. Run locally following this guide

Usage

Onlook will run on any React project, bring your own React project…

The Issue

The main issue is that when a className is using template literals, the textarea that should show the Tailwind Classes used in the div shows nothing. The textarea displays the Tailwind Classes used in the currently selected div in the application.

In these examples, I selected a div that uses a static string, and then a div that uses template literals to show what it looks like:

Using static string

Static String Sample

Using template literals

Template Literal Sample

The Approach

As I have mentioned from my previous post, I proceeded with understanding the code and the logic behind the parsing of these classNames from the project file. One of the maintainers of the repo also mentioned the function and which file it can be located as a good place to start, which helped a lot.

getTemplateNodeClass

export async function getTemplateNodeClass(templateNode: TemplateNode): Promise<string[]> {
    const codeBlock = await readCodeBlock(templateNode);
    const ast = parseJsxCodeBlock(codeBlock);
    if (!ast) {
        return [];
    }

    const classes = getNodeClasses(ast);
    return classes || [];
}
Enter fullscreen mode Exit fullscreen mode

This is the main function that orchestrates the parsing process.

  1. Calls readCodeBlock to retrieve the relevant section of the JSX/HTML code for the selected TemplateNode.
  2. Converts the retrieved code into an AST using parseJsxCodeBlock.
  3. Passes the AST to getNodeClasses to extract the class names.
  4. Outputs an array of class names or an empty array if nothing is found.

readCodeBlock

export async function readCodeBlock(templateNode: TemplateNode): Promise<string> {
    try {
        const filePath = templateNode.path;

        const startTag = templateNode.startTag;
        const startRow = startTag.start.line;
        const startColumn = startTag.start.column;

        const endTag = templateNode.endTag || startTag;
        const endRow = endTag.end.line;
        const endColumn = endTag.end.column;

        const fileContent = await readFile(filePath);
        const lines = fileContent.split('\n');

        const selectedText = lines
            .slice(startRow - 1, endRow)
            .map((line: string, index: number, array: string[]) => {
                if (index === 0 && array.length === 1) {
                    // Only one line
                    return line.substring(startColumn - 1, endColumn);
                } else if (index === 0) {
                    // First line of multiple
                    return line.substring(startColumn - 1);
                } else if (index === array.length - 1) {
                    // Last line
                    return line.substring(0, endColumn);
                }
                // Full lines in between
                return line;
            })
            .join('\n');

        return selectedText;
    } catch (error: any) {
        console.error('Error reading range from file:', error);
        throw error;
    }
}
Enter fullscreen mode Exit fullscreen mode

This function takes an AST node and attempts to extract the className attribute.

  1. If the className is a plain string, it splits the string into individual classes.
  2. If the className is wrapped in a JSX expression but is still a static string, it extracts the value similarly.
  3. For anything else, it currently returns an empty array, which leads to the issue.

getNodeClasses

function getNodeClasses(node: t.JSXElement): string[] {
    const openingElement = node.openingElement;
    const classNameAttr = openingElement.attributes.find(
        (attr): attr is t.JSXAttribute => t.isJSXAttribute(attr) && attr.name.name === 'className',
    );

    if (!classNameAttr) {
        return [];
    }

    if (t.isStringLiteral(classNameAttr.value)) {
        return classNameAttr.value.value.split(/\s+/).filter(Boolean);
    }

    if (
        t.isJSXExpressionContainer(classNameAttr.value) &&
        t.isStringLiteral(classNameAttr.value.expression)
    ) {
        return classNameAttr.value.expression.value.split(/\s+/).filter(Boolean);
    }

    return [];
}
Enter fullscreen mode Exit fullscreen mode

This function is responsible for:

  1. Reading the source file where the selected div resides.
  2. Extracts the specific range of code based on the start and end positions of the TemplateNode.
  3. Returns the extracted code block as a string.

Understanding these methods are crucial as this will be the foundation for this issue.

Progress

So within a week, I can say that I was able to make some progress with the issue. I now understand what I am working with, and where the issue stems from.

Based on my plans from last week, it was actually more simpler than I thought. I mentioned that I will need to figure out how to determine whether a className is using template literals or static strings. To my surprise, the code uses a method called isStringLiteral, which is a function from the @babel/types library. So I figured, searching through the methods, and I found that a method called isTemplateLiteral exists. This method easily allows me to determine whether a className uses a template literal or not, and then build the logic from there.

With that said, I have made my modifications with the code to achieve something like this:

Warning Sample

Currently, I made it so that when a div has a className that uses template literals, a readOnly warning is shown in the textarea instead of the Tailwind Classes.

Problems Encountered

There was only one problem I encountered so far, which is not really concerned with the code I was working on (which I initially thought it was).

After working on the code, I realized that I have not pulled the latest main from the repo. So I have been working on an older version of the app. After pulling main and updating my branch, the feature I am working on suddenly stopped working.

Broken Textarea

The textarea suddenly stopped getting displayed. I had to reach out to the maintainers and had to confirm whether this was a new bug in the newer version or it was just an issue on my end.

Thankfully, there was just a new feature implemented that requires running the project through the built-in terminal in the app rather than running the project in another IDE separately.

New Version

The next step

So I have now reached out to the maintainers of the repo and showed what I have so far, and they have made a few suggestions.
So for finalizing this issue, the important changes I will have to implement are:

  1. The warning should only appear when a user cannot edit the text. Meaning, this should only happen when dynamic variables are involved. This means, that other than determining whether a className uses template literals or not, I should also make a logic whether that template literal contains a dynamic variable or not. If there are no dynamic variables, then it should still be able to display the Tailwind Classes as if it is a static string.

  2. A button should be displayed for the user along with the warning. This button when clicked, should bring the user to that specific templateNode. Basically bringing the user to the specific className in the source code the warning is referring to.

Next week, will be the final update of this issue hopfully having it done and merged.

Top comments (0)