[Panel UI] Alternatives to writing HTML/CSS as a string in main.js

Hi, I am just getting started with trying to create my first plugin and I want to create it as a panel. However all the tutorial docs and sample plugin projects seem to use a pattern of defining the HTML and CSS as a string, and inserted to an element using innerHTML. This feels clunky to me as my text editor can’t do syntax highlighting or autocomplete within a string, and doesn’t seem like good separation of concerns.

So my question is, how can I break out the HTML or at least the CSS out of main.js and reference it from another file? In the plugin docs it says “You can build panels using any method that creates an HTML DOM structure. This means you can use document.createElement, innerHTML, jQuery, React and other frameworks”. Are there example plugins using these other methods? Thanks!

4 Likes

Hi, @qmulus. First of all: Welcome to the forums and the community :wave: :slightly_smiling_face:.

The first place that’s great to find samples is the official plugin-samples repository inside the Adobe XD GitHub organization: https://github.com/AdobeXD/plugin-samples

Among other examples, it features the following ones that might be interesting for you:

It could also be possible to “import” an HTML file either by using some transpilation step (e.g., webpack) or using the storage APIs from the uxp module to import the HTML (or CSS) file’s content as a string and use it using something like

myPanelWrapper.innerHTML = importedString;

All in all, however, it is, to my knowledge, not possible to easily use HTML or CSS files for XD plugins…

3 Likes

I’ve been able to import an HTML file, though I will caveat this by saying that I’m not aware of it being a wide practice among XD plugin developers.

There could be good reasons why (for example, it requires more code than you might expect, and it puts you in async land because you’re loading a file), but so far, it has worked for me.

Here’s an example of getting an HTML file (in this case, markup.html):

const getMarkup = async () => {
  const pluginFolder = await fs.getPluginFolder();
  const pluginFolderEntries = await pluginFolder.getEntries();

  const markupFile = pluginFolderEntries.filter(
    entry => entry.name === "markup.html"
  )[0];
  const markupFileContents = await markupFile.read();

  return markupFileContents;
};

Then, elsewhere in your plugin, you could do this within an async function:

  const markup = await getMarkup();
  panel = document.createElement("div");
  panel.id = "panel";
  panel.innerHTML = markup;

Here’s an example in a plugin I recently put together:

2 Likes

Thanks @pklaschka I’m glad to participate in the community! I had already seen that repo but I was just wondering if there were examples that were closer to what I wanted. Maybe I’m just old fashioned :slight_smile: I’ll check out the other methods you mentioned!

2 Likes

Thanks so much @ashryan this looks really promising!!

2 Likes

Couldn’t getMarkup just read the file directly without scanning the folder, by constructing the right file path?

1 Like

Oh, @cpryland you’re totally right! I blanked on the existence of folder.getEntry(path) (docs link).

@qmulus, this could get you what you want with fewer lines of code than I previously shared:

const getMarkup = async () => {
  const pluginFolder = await fs.getPluginFolder();
  const markupFile = await pluginFolder.getEntry("ui/markup.html");
  const markupFileContents = await markupFile.read();

  return markupFileContents;
}

I’m probably going to go with this because it’s simpler and one less JavaScript loop.

Note that I’ve hard-coded "markup.html" (in the code I shared above a few days ago) or "ui/markup.html" (in the revised code based on Chris’s tip above) because I only have one file to load.

But if you refactor that string to be a function parameter (say, path), you’ve got yourself a nice helper function that can be reused.

Perhaps something like:

const getFileContent = async (path) => {
  const pluginFolder = await fs.getPluginFolder();
  const file = await pluginFolder.getEntry(path);
  const fileContent = await file.read();

  return fileContent;
};

Elsewhere, within an async function, you’d do:

const markup = await getFileContent("ui/markup.html");
panel = document.createElement("div");
panel.innerHTML = markup;

Ultimately appending panel to event.node:

event.node.appendChild(panel);
3 Likes

Looks like you’ve got some good responses already, but as far as examples of other options go, yes! Check the plugin samples repo – we have react, jquery, vue, and vanilla samples: https://github.com/AdobeXD/plugin-samples

Also, we’re adding capabilities to the manifest to allow it to align across all apps taking UXP, and so in the future you’ll be able to use an HTML file as your entry point, instead of just a JS file. No date on that yet, but just thought you’d like to know.

4 Likes

Interesting! Can you expand on that just a bit? No holding you to any details, etc.

In version 1.0 of UXP in XD you would specify the entry point in your manifest using:

{
    "name": "My XD plugin",
    "version": "1.1.0",
    "main": "index.js"
}

IIUC she’s saying that you would specify a HTML page there instead of a javascript script.

Yes, I understood that much, but wondered about further details.