It's been a week since I started working on this issue in the onlook repo.
onlook-dev / onlook
The open source, local-first Figma for React. Design directly in your live React app and publish your changes to code.
Onlook
The first browser-powered visual editor
Explore the docs »
View Demo
·
Report Bug
·
Request Feature
Table of Contents
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
Built With
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
Installation
- Visit onlook.dev to download the app.
- 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
Using template literals
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 || [];
}
This is the main function that orchestrates the parsing process.
- Calls
readCodeBlock
to retrieve the relevant section of the JSX/HTML code for the selectedTemplateNode
. - Converts the retrieved code into an
AST
usingparseJsxCodeBlock
. - Passes the
AST
togetNodeClasses
to extract the class names. - 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;
}
}
This function takes an AST node and attempts to extract the className
attribute.
- If the
className
is a plain string, it splits the string into individual classes. - If the
className
is wrapped in a JSX expression but is still a static string, it extracts the value similarly. - 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 [];
}
This function is responsible for:
- Reading the source file where the selected
div
resides. - Extracts the specific range of code based on the
start
andend
positions of theTemplateNode
. - 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:
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.
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.
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:
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.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 specificclassName
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)