Code buttons - simple way to execute JavaScript inside Markdown codeblock

The topic about interactive programming and mini apps inside Logseq seems super exciting nowadays.
Here is a quick way to execute JavaScript code, which is located inside Markdown codeblocks.

Thanks to @mentaloid for work and inspiration in Edit and run javascript code inside Logseq itself. My requirements are a bit simpler - just execute that code block with a button. If you are seeking for Jupyter-like notebook experience and/or more programming languages support, above topic might be better suited! Go credit him as well, if you like the overall topic.

Specific goals:

  • vanilla Logseq - no plugin
  • performance - no need to watch DOM with MutationObserver
  • convention-driven: Place code block as first child block under code button
  • encapsulation: based on official Web components standard, mini app in Logseq

Getting started

1. Put code for the CodeButton web component (Attachment 1) in custom.js. Then create a macro for ease of use:

 :macros {
          :button "<div is='code-button' name=$1 class=$2></div>"
          }

1a. Restart.
2. Write in a block content {{button Click}}.
3. Create child block with content

```js
logseq.api.show_msg("Hello world", "info");
```

4. Get greeting like in above picture.

More examples

// Insert childblock (current block ID of codeblock available via this.uuid)
// this.target_uuid has ID of block containing the button.
// this.ev is the fired click event.
logseq.api.insert_block(this.uuid, "new kid on the block")

// simple query
const results = logseq.api.q("[[MyPage]]");
console.log(results);

// advanced query
const results = logseq.api.datascript_query(`[
:find (pull ?b[*]) 
:where 
  [?p :block/name "mypage"]
  [?b :block/refs ?p]]`);
console.log(results);

Invoking the macro with quiet

{{button "Simple query",quiet}}

will suppress code execution popup.

Notes

  • To save you an app crash with logseq.api.show_msg :slight_smile: : Don’t feed it with complex objects. Use console.log or write blocks instead. In other words: e.g. no logseq.api.show_msg(this.ev, "info");.
  • TODO: Post about web components in Logseq as mini app, current limitations in Logseq

Attachment 1

class CodeButton extends HTMLDivElement {
  constructor() {
    super();
    const style = document.createElement("style");
    style.textContent = `
      button.button-style {
          display: inline-block;
          outline: none;
          cursor: pointer;
          padding: 0 10px;
          background-color: #fff;
          border-radius: 0.25rem;
          border: 1px solid #0070d2;
          color: #0070d2;
          font-size: 13px;
          line-height: 30px;
          font-weight: 400;
          text-align: center;
	  user-select: none;
      }
      
      button.button-style::before {
          content: "➤ ";
      }
      
      button.button-style:hover {
          background-color: #f4f6f9; 
      }`;

    this.appendChild(style);
    this.insertAdjacentHTML(
      "beforeend",
      `<button class="button-style">${this.getAttribute("name") || "Click"}</button>`,
    );
  }

  code_from_childblock(ele) {
    const ele_block_uuid = ele
      .closest("[id^='block-content-'][blockid]")
      ?.getAttribute("blockid");
    if (!ele_block_uuid) return;

    // Convention: Codeblock is first child of button block.
    const first_child_block_uuid =
      logseq.api.get_block(ele_block_uuid)?.children?.[0]?.[1];
    if (!first_child_block_uuid) return;

    const content = logseq.api.get_block(first_child_block_uuid)?.content;
    if (!content) return;

    const regex =
      /(?<![\r\n])^```(?:javascript|js)\n(.*)\n```(?=[\r\n]?$(?![\r\n]))/msu;
    const code = content.match(regex)?.[1];
    if (!code) return;

    return {
      target_uuid: ele_block_uuid,
      uuid: first_child_block_uuid,
      code,
    };
  }

  handleClick(ev) {
    const AsyncFunction = async function () {}.constructor;
    const { code, ...rest } = this.code_from_childblock(ev.target) ?? {};
    if (!code) {
      logseq.api.show_msg("No code block found", "error", { timeout: 5000 });
      return;
    }
    try {
      AsyncFunction('"use strict";' + code).call({ ev, ...rest });
      if (!this.classList.contains("quiet"))
        logseq.api.show_msg("Executed code", "success", { timeout: 2000 });
    } catch (er) {
      logseq.api.show_msg(`Code error: ${er.message}`, "error", {
        timeout: 5000,
      });
      throw er;
    }
  }

  connectedCallback() {
    this.querySelector("button").addEventListener(
      "click",
      this.handleClick.bind(this),
    );
  }
}

customElements.define("code-button", CodeButton, { extends: "div" });

Looks very nice, thanks for sharing. Going to experiment with it while searching for a good setup and workflow. (Just starting out with logseq)

Thanks. This is awesome.

I use this every day as a core component of my workflow.

Could you describe what your buttons do in your workflow?