sejori / peko

Featherweight apps on the edge 🐣⚡ Node, Deno, Bun & Cloudflare Workers.

Home Page:https://peko.deno.dev/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

General-Purpose HTML Rendering

sejori opened this issue · comments

As well as using the SSR handler for complete HTML page rendering it could also be used to serve chunks of HTML that can be loaded into a variety of web applications. Therefore it makes sense to restructure some of the code so that this usecase is more intuitive and we don't have to hack it into the existing system.

Renaming the "addPageRoute" function to "addHTMLRoute" and redefining the "PekoPageRouteData" type to be more general-purpose seem like the right course of action to me.

When I run the example, I do a:

GET http://localhost:7777/exampleSrc/htmlTemplate.js HTTP/1.1
content-type: application/json

And get the following response that contains everything:

export default (_request, customTags, HTML) => `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="icon" href="/exampleSrc/assets/favicon.ico">
        
        ${customTags.title}
        <meta name="description" content="The Featherweight Deno SSR Library">
        
        ${customTags.style}
        ${customTags.modulepreloads}
    </head>
    <body>
        <div id="root">
            ${HTML}
        </div>

        ${customTags.hydrationScript}
    </body>
    </html>
`

So I created a htmlChunk.js file, and removed everything but the html I want to send to the client.

export default (HTML) => `
        <nav id="nav">
            <span>example of a nav bar sent to the client</span>
            ${HTML}
        </nav>
`

Question:

Why/in what case, would we send the "export default (param) =>" to the client? Will they get this as a string client side? Is this to construct a function that returns the string within to the client? Is this basically sending over a function call that returns the string to the client?

Also, if I wanted to send over styles and javascript, would I just append the code to the bottom of the string?

For example, if I wanted to send over a JS file at the end, that handles the onClick="toggleDropDown(this,0)"

<nav>
  <div class="burger">
    <div class="line1"></div>
    <div class="line2"></div>
    <div class="line3"></div>
  </div>
  <ul class="nav-links">
    <li onClick="toggleDropDown(this,0)">
      <a href="#" class="nav-item" id="navItem">Stream</a>
      <span class="down-arrow"
        ><svg
          class="MuiSvgIcon-root-3894"
          focusable="false"
          viewBox="0 0 24 24"
          aria-hidden="true"
        >
          <path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></svg
      ></span>
      </ul> 
     ...
</nav>
<script>
  const navSlide = () => {
      const burger = document.querySelector('.burger');
      const nav = document.querySelector('.nav-links');

      const navLinks = document.querySelectorAll('.nav-links li');


      burger.addEventListener('click', () => {
          nav.classList.toggle('nav-active');


          burger.classList.toggle('toggle');
      });

  }

  const closeAllDropdowns = (currentIndex) => {
      const dropDowns = document.querySelectorAll('.sub-menu');

      dropDowns?.forEach((item, index) => {
          if (currentIndex !== index) {
              item.classList.remove('show');
          }
      })
  }

  window.onclick = function (event) {
      console.log('window')
      console.log(event.target)
      if (!event.target.matches('#navItem')) {
          closeAllDropdowns()
      }
  }

  function toggleDropDown(a, index) {
      console.log('myfun',a.parentNode);
      closeAllDropdowns(index);
      a.parentNode.getElementsByClassName("sub-menu")[index].classList.toggle("show");

      a.parentNode.getElementsByClassName('up-arrow')[index].classList.toggle("show");
      a.parentNode.getElementsByClassName('down-arrow')[index].classList.toggle("show");
  }

  navSlide();
</script>
<style>
    .up-arrow {
        right: 10px;
    }
    .down-arrow {
        right: 10px;
    }
</style>
`

This code has to be Injected into another project, and used as a component, like in a React app for example. Would the app have to parse out the style/js from the html, inject the html into a react component, then add the style and js along side ti?

Sorry for all the edits, just stream of thought here... but I think I'm finally done with the question! Thank you!

Hey there!

So you are requesting the htmlTemplate.js source file directly which is not intended to be used by the client. This template file is imported in example.js and passed into lib/handlers/ssr.ts to be used as the template that the rendered HTML and other provided metadata will be injected into. This all happens server-side and the resultant HTML is then sent to the client.

If you run $ deno run --allow-env --allow-read --allow-net example.js you will see that all of the source files are made public in the example set-up. This is so that the preact component modules can be loaded into the browser. It unecessarily makes the htmlTemplate.js file public just because it's within the src folder but it is never intended to be used in the client.

Instead if you make a GET request to the root / you will see the template with the injected app HTML is returned.

Regarding adding custom styles and JS, take a look at line 91 in example.js as this is where a CSS style tag (defined as a string above in the file) is passed into the template as a "customTag".

The customTags must be strings that start with "<" and end with ">", this is just a basic way of ensuring valid html is injected in. But you are free to add as many customTags as you like. If you look in htmlTemplate.js you will see ${customTags.style} is where the this style tag is added to the HTML.

For custom JS you can simply define the script as a string in example.js (or your equivalent file) and insert it as an attribute to the customTags property for that HTMLRoute. Then in your htmlTemplate put ${customTags["Insert Script Property Name Here"]} where you would like the script to be added.

And finally as a heads up, I am working on a PR that changes index.ts to mod.ts and restructures the example code into a directory called examples/preact/.... Just letting you know ahead of time as this will break your implementation if you're importing from the repo. All you'll need to do is change your import file to mod.ts - I won't make the change till later tomorrow :)

Hi, added in a new Nav component and removed the DOC/Header/HTML/Body:

image

How would another application actually use just the "navigation" string shown here in the Network tab? Also, there are a few other files, can these be removed as well?

There are a number of ways, you could use an <iframe> and set the src property to the Peko route that serves the HTML you want embedded. This is probably not the best method though as iframes are a bit gross and would require sending a full HTML doc instead of just a snippet.

In Vanilla JavaScript you could do something along the lines of:
const htmlString = await fetch("https://peko-service/navComponent");
document.querySelector("#target").innerHTML = htmlString;

Other methods would depend on your frontend tooling but here is a promising option I found: https://htmx.org/

And yes, you can simply remove the hydrationScript in the template file or edit it in the customTags data as I believe those additional JS files are coming as a result of the import statements in the script.

This is the response from the fetch call:

Response {
  size: 0,
  timeout: 0,
  [Symbol(Body internals)]: {
    body: Gunzip {
      _writeState: [Uint32Array],
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 5,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      bytesWritten: 0,
      _handle: [Zlib],
      _outBuffer: <Buffer 0a 20 20 0a 20 20 20 20 3c 73 74 79 6c 65 3e 0a 20 20 20 20 20 20 20 20 68 74 6d 6c 2c 20 62 6f 64 79 20 7b 0a 20 20 20 20 20 20 20 20 20 20 20 20 68 ... 16334 more bytes>,
      _outOffset: 0,
      _chunkSize: 16384,
      _defaultFlushFlag: 2,
      _finishFlushFlag: 2,
      _defaultFullFlushFlag: 3,
      _info: undefined,
      _maxOutputLength: 4294967296,
      _level: -1,
      _strategy: 0,
      [Symbol(kCapture)]: false,
      [Symbol(kCallback)]: null,
      [Symbol(kError)]: null
    },
    disturbed: false,
    error: null
  },
  [Symbol(Response internals)]: {
    url: 'http://localhost:7777/navigation',
    status: 200,
    statusText: 'OK',
    headers: Headers { [Symbol(map)]: [Object: null prototype] },
    counter: 0
  }
}

I'm using Next.js and making the call server side:

export async function getServerSideProps() {
  // Fetch data from external API
  const htmlString = await fetch(`http://localhost:7777/navigation`);
  console.log(htmlString);
  const html = JSON.parse(JSON.stringify(htmlString));

  // Pass data to the page via props
  return { props: { html } };
}

I guess I thought it would return just an html "string".

The HTML string would be in the body of the request - I should have clarified that in my example code my bad.

export async function getServerSideProps() {
  // Fetch data from external API
  const htmlRequest = await fetch(`http://localhost:7777/navigation`);
  const htmlString = htmlRequest.text();

  console.log(htmlString);
  const html = JSON.parse(JSON.stringify(htmlString));

  // Pass data to the page via props
  return { props: { html } };
}