12

I found this (github) html starter page for google auth, and I wanted to make it into a astro component. Im wanted to convert this to a .astro file and be able to pass in the variables for CLIENT_ID and API_KEY from the backend. I

Here's the HTML code from google:

<!DOCTYPE html>
<html>
  <head>
    <title>Google Calendar API Quickstart</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <p>Google Calendar API Quickstart</p>

    <!--Add buttons to initiate auth sequence and sign out-->
    <button id="authorize_button" onclick="handleAuthClick()">Authorize</button>
    <button id="signout_button" onclick="handleSignoutClick()">Sign Out</button>

    <pre id="content" style="white-space: pre-wrap;"></pre>

    <script type="text/javascript">
      /* exported gapiLoaded */
      /* exported gisLoaded */
      /* exported handleAuthClick */
      /* exported handleSignoutClick */

      // TODO(developer): Set to client ID and API key from the Developer Console
      const CLIENT_ID = '<YOUR_CLIENT_ID>';
      const API_KEY = '<YOUR_API_KEY>';

      // Discovery doc URL for APIs used by the quickstart
      const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest';

      // Authorization scopes required by the API; multiple scopes can be
      // included, separated by spaces.
      const SCOPES = 'https://www.googleapis.com/auth/calendar.readonly';

      let tokenClient;
      let gapiInited = false;
      let gisInited = false;

      document.getElementById('authorize_button').style.visibility = 'hidden';
      document.getElementById('signout_button').style.visibility = 'hidden';

      /**
       * Callback after api.js is loaded.
       */
      function gapiLoaded() {
        gapi.load('client', intializeGapiClient);
      }

      /**
       * Callback after the API client is loaded. Loads the
       * discovery doc to initialize the API.
       */
      async function intializeGapiClient() {
        await gapi.client.init({
          apiKey: API_KEY,
          discoveryDocs: [DISCOVERY_DOC],
        });
        gapiInited = true;
        maybeEnableButtons();
      }

      /**
       * Callback after Google Identity Services are loaded.
       */
      function gisLoaded() {
        tokenClient = google.accounts.oauth2.initTokenClient({
          client_id: CLIENT_ID,
          scope: SCOPES,
          callback: '', // defined later
        });
        gisInited = true;
        maybeEnableButtons();
      }

      /**
       * Enables user interaction after all libraries are loaded.
       */
      function maybeEnableButtons() {
        if (gapiInited && gisInited) {
          document.getElementById('authorize_button').style.visibility = 'visible';
        }
      }

      /**
       *  Sign in the user upon button click.
       */
      function handleAuthClick() {
        tokenClient.callback = async (resp) => {
          if (resp.error !== undefined) {
            throw (resp);
          }
          document.getElementById('signout_button').style.visibility = 'visible';
          document.getElementById('authorize_button').innerText = 'Refresh';
          await listUpcomingEvents();
        };

        if (gapi.client.getToken() === null) {
          // Prompt the user to select a Google Account and ask for consent to share their data
          // when establishing a new session.
          tokenClient.requestAccessToken({prompt: 'consent'});
        } else {
          // Skip display of account chooser and consent dialog for an existing session.
          tokenClient.requestAccessToken({prompt: ''});
        }
      }

      /**
       *  Sign out the user upon button click.
       */
      function handleSignoutClick() {
        const token = gapi.client.getToken();
        if (token !== null) {
          google.accounts.oauth2.revoke(token.access_token);
          gapi.client.setToken('');
          document.getElementById('content').innerText = '';
          document.getElementById('authorize_button').innerText = 'Authorize';
          document.getElementById('signout_button').style.visibility = 'hidden';
        }
      }

      /**
       * Print the summary and start datetime/date of the next ten events in
       * the authorized user's calendar. If no events are found an
       * appropriate message is printed.
       */
      async function listUpcomingEvents() {
        let response;
        try {
          const request = {
            'calendarId': 'primary',
            'timeMin': (new Date()).toISOString(),
            'showDeleted': false,
            'singleEvents': true,
            'maxResults': 10,
            'orderBy': 'startTime',
          };
          response = await gapi.client.calendar.events.list(request);
        } catch (err) {
          document.getElementById('content').innerText = err.message;
          return;
        }

        const events = response.result.items;
        if (!events || events.length == 0) {
          document.getElementById('content').innerText = 'No events found.';
          return;
        }
        // Flatten to string to display
        const output = events.reduce(
            (str, event) => `${str}${event.summary} (${event.start.dateTime || event.start.date})\n`,
            'Events:\n');
        document.getElementById('content').innerText = output;
      }
    </script>
    <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
    <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
  </body>
</html>

I quickly found there's no way to template these variables into the <script> tag. I tried attaching the variables to window and other sneaky things nothing worked.

ThomasReggi
  • 55,053
  • 85
  • 237
  • 424

7 Answers7

28

Sharing server variables → clientside

You can share the server variables to client side by defining define:vars attribute on <script /> or <style /> tag in the .astro template.

Read the astro documentation here for more information.

Example as below ( source : Astro documentation )

---
const foregroundColor = "rgb(221 243 228)"; // CSS variable shared
const backgroundColor = "rgb(24 121 78)"; // CSS variable shared
const message = "Astro is awesome!"; // Javascript variable shared
---

/* +++++ CSS variables to share as below +++++ */

<style define:vars={{ textColor: foregroundColor, backgroundColor }}>
  h1 {
    background-color: var(--backgroundColor);
    color: var(--textColor);
  }
</style>


/* ++++ Javascript variables to share as below ++++ */

<script define:vars={{ message }}>
  alert(message);
</script>

Clientside sharing states ( component ↔ frameworks )

There is also a concept of sharing states using nanostore stated in documentation . It allows sharing states between components at framework level on client-side. Not between client and server.

Theoretically sharing states from server to client can be done using hydration technique by combining define:vars and nanostore library map api during the onLoad event may be .


CAUTION

Using define:vars on a <script> or <style> tag implies the is:inline directive, which means your scripts or styles won’t be bundled and will be inlined directly into the HTML.

This is because when Astro bundles a script, it includes and runs the script once even if you include the component containing the script multiple times on one page. define:vars requires a script to rerun with each set of values, so Astro creates an inline script instead.

For scripts, try passing variables to scripts manually instead.

Source: Astro documentation

NiRUS
  • 3,901
  • 2
  • 24
  • 50
  • 1
    What if you need to do the opposite action? from client to server? – Lordan Sep 27 '22 at 15:11
  • @Lordan by writing a `POST` rest API and sending `JSON` object to the server and process it. – NiRUS Sep 27 '22 at 20:00
  • 2
    looks like a headache, i just want to use the window object to get the clientheight, and it can only be done in client script, wonder if there's more easy and less code approach for it . – Lordan Sep 28 '22 at 09:22
1

I came up with this sneaky way of doing it but

---

interface Props {
  clientId: string,
  apiKey: string
}


const { clientId, apiKey } = Astro.props as Props;

---
    
   <!-- begining code -->
    <div id="clientId" style="display:none">{clientId}</div>
    <div id="apiKey" style="display:none">{apiKey}</div>
    <script type="text/javascript">
      /* exported gapiLoaded */
      /* exported gisLoaded */
      /* exported handleAuthClick */
      /* exported handleSignoutClick */

      // TODO(developer): Set to client ID and API key from the Developer Console
      const CLIENT_ID = document.getElementById('clientId').innerText;
      const API_KEY = document.getElementById('apiKey').innerText;
      console.log({ CLIENT_ID, API_KEY })
      // ... rest of the code
ThomasReggi
  • 55,053
  • 85
  • 237
  • 424
0

You can access a file with import.meta.glob(), and from the file you can get the variable you want.

/index.astro:

---
//Node stuff
---

<html>
//template here
</html>
<script>
 const modules = import.meta.glob('./blog/*.{md,mdx}')

 for (const path in modules) {
   modules[path]().then((mod) => {
     console.log(path, mod) //Access fronttmatter, content, path, etc
 })
</script>
0

I need the environment variable in a function that is binded to an onclick so I came up with following hacky quite ugly workaround today. Using vars too but by assiging the value to the window object.

---
const code = import.meta.env.PUBLIC_CODE;
---

<script is:inline define:vars={{ code }}>
  window.code = code;
</script>

<script is:inline>
  const copy = async () => await navigator.clipboard.writeText(window.code);
</script>

<button onclick="copy()">
  Copy
</button>

I welcome any better solution!

David Dal Busco
  • 7,975
  • 15
  • 55
  • 96
0

Server to Client with Astro

  • frontmatter variable in html attribute or content
  • define:vars but not always recommended
  • cookies
  • Server Sent Events
  • Websockets
  • Database

I only posted points, because this question only adresses one path, server->client, and not the whole scenario server<->client. For a detailed answer about State sharing including server to client see How to share state among components in Astro?

wassfila
  • 1,173
  • 8
  • 18
0

If you need to pass a variable and also avoid the is:inline attribute, Astro explains in its docs that you need to create a custom HTML element (more info on Astro docs).

Here an example where I'm using the typewriter-effect package (installed through npm) that needs to access to document HTML attribute:

---
// Astro code
const my_messages = ["Hello", "World"];
---

<!-- HTML code -->

<astro-typewriter data-messages={my_messages} >
    <div id="#writer" />
</astro-typewriter>

<script>
  import Typewriter from "typewriter-effect/dist/core";
  
  // Create your custom class
  class MyTypeWriter extends HTMLElement {
    constructor() {
      super();

      // Read the message from the data attribute.
      const messages = this.dataset.messages !== undefined ?
         JSON.parse(this.dataset.messages) :
         [];

      const tw = new Typewriter('#writer', {
        loop: true,
      });
      messages.forEach(m => {
        tw
          .typeString(m)
          .pauseFor(1000)
          .deleteAll()
      });
      tw.start();
    }
  }

  customElements.define('astro-typewriter', MyTypeWriter);
</script>
mune
  • 11
  • 2
0

I published a library to send primitive & complex values from the server to your client script with types preserved.

See: https://www.npmjs.com/package/@ayco/astro-resume

For example, if you need to make all the component props to the client script:

---
import Serialize from "@ayco/astro-resume";
export interface Props {
    hello: string;
    isOkay: boolean;
}
---

<Serialize id="preferences" data={{...Astro.props}} />

<script>
    import {deserialize} from '@ayco/astro-resume';
    import type {Props} from './ThisComponent.astro';
    const {hello, isOkay} = deserialize<Props>('preferences');
    console.log(hello, isOkay);
</script>
Ayo Ayco
  • 1
  • 1