How do I pass a server variable to client side JS in astro? - astrojs

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.

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>
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 🧪.

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

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>

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!

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?

Related

Google App Scripts Function to Open URL [duplicate]

Is there a way to write a google apps script so when ran, a second browser window opens to www.google.com (or another site of my choice)?
I am trying to come up with a work-around to my previous question here:
Can I add a hyperlink inside a message box of a Google Apps spreadsheet
This function opens a URL without requiring additional user interaction.
/**
* Open a URL in a new tab.
*/
function openUrl( url ){
var html = HtmlService.createHtmlOutput('<html><script>'
+'window.close = function(){window.setTimeout(function(){google.script.host.close()},9)};'
+'var a = document.createElement("a"); a.href="'+url+'"; a.target="_blank";'
+'if(document.createEvent){'
+' var event=document.createEvent("MouseEvents");'
+' if(navigator.userAgent.toLowerCase().indexOf("firefox")>-1){window.document.body.append(a)}'
+' event.initEvent("click",true,true); a.dispatchEvent(event);'
+'}else{ a.click() }'
+'close();'
+'</script>'
// Offer URL as clickable link in case above code fails.
+'<body style="word-break:break-word;font-family:sans-serif;">Failed to open automatically. Click here to proceed.</body>'
+'<script>google.script.host.setHeight(40);google.script.host.setWidth(410)</script>'
+'</html>')
.setWidth( 90 ).setHeight( 1 );
SpreadsheetApp.getUi().showModalDialog( html, "Opening ..." );
}
This method works by creating a temporary dialog box, so it will not work in contexts where the UI service is not accessible, such as the script editor or a custom G Sheets formula.
You can build a small UI that does the job like this :
function test(){
showURL("http://www.google.com")
}
//
function showURL(href){
var app = UiApp.createApplication().setHeight(50).setWidth(200);
app.setTitle("Show URL");
var link = app.createAnchor('open ', href).setId("link");
app.add(link);
var doc = SpreadsheetApp.getActive();
doc.show(app);
}
If you want to 'show' the URL, just change this line like this :
var link = app.createAnchor(href, href).setId("link");
EDIT : link to a demo spreadsheet in read only because too many people keep writing unwanted things on it (just make a copy to use instead).
EDIT : UiApp was deprecated by Google on 11th Dec 2014, this method could break at any time and needs updating to use HTML service instead!
EDIT :
below is an implementation using html service.
function testNew(){
showAnchor('Stackoverflow','http://stackoverflow.com/questions/tagged/google-apps-script');
}
function showAnchor(name,url) {
var html = '<html><body>'+name+'</body></html>';
var ui = HtmlService.createHtmlOutput(html)
SpreadsheetApp.getUi().showModelessDialog(ui,"demo");
}
There really isn't a need to create a custom click event as suggested in the bountied answer or to show the url as suggested in the accepted answer.
window.open(url)1 does open web pages automatically without user interaction, provided pop- up blockers are disabled(as is the case with Stephen's answer)
openUrl.html
<!DOCTYPE html>
<html>
<head>
<base target="_blank">
<script>
const url1 ='https://stackoverflow.com/a/54675103';
const winRef = window.open(url1);
winRef ? google.script.host.close() : window.alert('Allow popup to redirect you to '+url1) ;
window.onload=function(){document.getElementById('url').href = url1;}
</script>
</head>
<body>
Kindly allow pop ups</br>
Or <a id='url'>Click here </a>to continue!!!
</body>
</html>
code.gs:
function modalUrl(){
SpreadsheetApp.getUi()
.showModalDialog(
HtmlService.createHtmlOutputFromFile('openUrl').setHeight(50),
'Opening StackOverflow'
)
}
Google Apps Script will not open automatically web pages, but it could be used to display a message with links, buttons that the user could click on them to open the desired web pages or even to use the Window object and methods like addEventListener() to open URLs.
It's worth to note that UiApp is now deprecated. From Class UiApp - Google Apps Script - Google Developers
Deprecated. The UI service was deprecated on December 11, 2014. To
create user interfaces, use the HTML service instead.
The example in the HTML Service linked page is pretty simple,
Code.gs
// Use this code for Google Docs, Forms, or new Sheets.
function onOpen() {
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.createMenu('Dialog')
.addItem('Open', 'openDialog')
.addToUi();
}
function openDialog() {
var html = HtmlService.createHtmlOutputFromFile('index')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.showModalDialog(html, 'Dialog title');
}
A customized version of index.html to show two hyperlinks
<a href='http://stackoverflow.com' target='_blank'>Stack Overflow</a>
<br/>
<a href='http://meta.stackoverflow.com/' target='_blank'>Meta Stack Overflow</a>
Building of off an earlier example, I think there is a cleaner way of doing this. Create an index.html file in your project and using Stephen's code from above, just convert it into an HTML doc.
<!DOCTYPE html>
<html>
<base target="_top">
<script>
function onSuccess(url) {
var a = document.createElement("a");
a.href = url;
a.target = "_blank";
window.close = function () {
window.setTimeout(function() {
google.script.host.close();
}, 9);
};
if (document.createEvent) {
var event = document.createEvent("MouseEvents");
if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1) {
window.document.body.append(a);
}
event.initEvent("click", true, true);
a.dispatchEvent(event);
} else {
a.click();
}
close();
}
function onFailure(url) {
var div = document.getElementById('failureContent');
var link = 'Process';
div.innerHtml = "Failure to open automatically: " + link;
}
google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onFailure).getUrl();
</script>
<body>
<div id="failureContent"></div>
</body>
<script>
google.script.host.setHeight(40);
google.script.host.setWidth(410);
</script>
</html>
Then, in your Code.gs script, you can have something like the following,
function getUrl() {
return 'http://whatever.com';
}
function openUrl() {
var html = HtmlService.createHtmlOutputFromFile("index");
html.setWidth(90).setHeight(1);
var ui = SpreadsheetApp.getUi().showModalDialog(html, "Opening ..." );
}
I liked #Stephen M. Harris's answer, and it worked for me until recently. I'm not sure why it stopped working.
What works for me now on 2021-09-01:
function openUrl( url ){
Logger.log('openUrl. url: ' + url);
const html = `<html>
<a id='url' href="${url}">Click here</a>
<script>
var winRef = window.open("${url}");
winRef ? google.script.host.close() : window.alert('Configure browser to allow popup to redirect you to ${url}') ;
</script>
</html>`;
Logger.log('openUrl. html: ' + html);
var htmlOutput = HtmlService.createHtmlOutput(html).setWidth( 250 ).setHeight( 300 );
Logger.log('openUrl. htmlOutput: ' + htmlOutput);
SpreadsheetApp.getUi().showModalDialog( htmlOutput, `openUrl function in generic.gs is now opening a URL...` ); // https://developers.google.com/apps-script/reference/base/ui#showModalDialog(Object,String) Requires authorization with this scope: https://www.googleapis.com/auth/script.container.ui See https://developers.google.com/apps-script/concepts/scopes#setting_explicit_scopes
}
https://developers.google.com/apps-script/reference/base/ui#showModalDialog(Object,String) Requires authorization with this scope: https://www.googleapis.com/auth/script.container.ui See https://developers.google.com/apps-script/concepts/scopes#setting_explicit_scopes

Google Analytics - Embed API - Getting an OAuth Error

I am trying to follow the example here to embed some Google Analytics statistics in my web app.
My HTML looks as follows:
<div class="page-header">
<h3 class="text-center">WAND Sessions</h3>
</div>
<br/>
<br/>
<div id="embed-api-auth-container"></div>
<div id="chart-container"></div>
<div id="view-selector-container"></div>
#section Scripts {
<script>
(function(w, d, s, g, js, fs) {
g = w.gapi || (w.gapi = {});
g.analytics = { q: [], ready: function(f) { this.q.push(f); } };
js = d.createElement(s);
fs = d.getElementsByTagName(s)[0];
js.src = 'https://apis.google.com/js/platform.js';
fs.parentNode.insertBefore(js, fs);
js.onload = function() { g.load('analytics'); };
}(window, document, 'script'));
</script>
<script src="~/Scripts/GoogleAnalytics/viewSessions.js"></script>
}
My viewSessions.js file looks like this:
$(document).ready(function () {
gapi.analytics.ready(function () {
/**
* Authorize the user immediately if the user has already granted access.
* If no access has been created, render an authorize button inside the
* element with the ID "embed-api-auth-container".
*/
gapi.analytics.auth.authorize({
container: 'embed-api-auth-container',
clientid: 'xxxxxxxxx'
});
/**
* Create a new ViewSelector instance to be rendered inside of an
* element with the id "view-selector-container".
*/
var viewSelector = new gapi.analytics.ViewSelector({
container: 'view-selector-container'
});
// Render the view selector to the page.
viewSelector.execute();
/**
* Create a new DataChart instance with the given query parameters
* and Google chart options. It will be rendered inside an element
* with the id "chart-container".
*/
var dataChart = new gapi.analytics.googleCharts.DataChart({
query: {
metrics: 'ga:sessions',
dimensions: 'ga:date',
'start-date': '30daysAgo',
'end-date': 'yesterday'
},
chart: {
container: 'chart-container',
type: 'LINE',
options: {
width: '100%'
}
}
});
/**
* Render the dataChart on the page whenever a new view is selected.
*/
viewSelector.on('change', function (ids) {
dataChart.set({ query: { ids: ids } }).execute();
});
});
});
When I load my page, all I see is this:
If I click on this, I get a Google error dialog saying
401 Error: invalid_client
The OAuth client was not found.
Any idea what's wrong?
The documentation on that page is not very good. I have mentioned this to Google in the past.
You need to go to Google Developers console and create your own client id using an OAuth credentials.
The Google Analytics Embeded API getting started page tells you how to do that.
Create a New Client ID
The Embed API handles almost all of the authorization process for you
by providing a one-click sign-in component that uses the familiar
OAuth 2.0 flow. In order to get this button working on your page
you'll need a client ID.
If you've never created a client ID, you can do so by following these
instructions.
As you go through the instructions, it's important that you not
overlook these two critical steps: • Enable the Analytics API • Set the
correct origins
The origins control what domains are allowed to use this code to
authorize users. This prevents other people from copying your code and
running it on their site.
For example, if your website is hosted on the domain example.com, make
sure to set the following origin for your client ID
http://example.com. If you want to test your code locally, you'll need
to add the origin for your local server as well, for example:
http://localhost:8080.

Firefox Addon SDK(jetpack): Passing page-script form data to index.js?

I want to pass the page script form data to index.js in my extension. What is the way to do it? I am trying to send it through content-script.js. To do this I am including my content-script.js file into the page-script. The content-script.js contains these lines of code-
function getInput(){
var url = document.getElementById('addr').value;
self.port.emit("addr",url);
}
Now from the page-script submit button I am calling getInput() function. But self.port.emit does not work here.
I have found out the solution. This can be done by creating DOM events.
In the page-script I have created a custom DOM event like this-
add.html->
<html>
<head>
<script>
function sendMessage() {
var url = document.getElementById('addr').value;
//console.log(url);
var event = document.createEvent('CustomEvent');
event.initCustomEvent("msg", true, true, url);
document.documentElement.dispatchEvent(event);
}
</script>
</head>
<body>
<form>
<input type="text" id="addr" name="addr">
<button onclick="sendMessage()">Add</button>
</form>
Next the helper.js listens for the new event and retrieves the message.
helper.js-
window.addEventListener("msg", function(event) {
var url = JSON.stringify(event.detail);
self.postMessage(url);
}, false);
Finally the index.js "panel" code looks like this-
var panels = require("sdk/panel");
var panel = panels.Panel({
width: 200,
height: 200,
contentURL: "./page.html",
contentScriptFile: "./helper.js",
onHide: handleHide,
onMessage: function(url) {
console.log(url); // displays the user input
}
});
Working fine. Is there other way to do this? Is this efficient one?
Also working fine with self.port.emit() and panel.port.on().

Google + HTML Sign in not working on IE or Firefox

I'm working on a web app that uses client side authentication through the Google + API. Im using a modified example from Google that uses an HTML element with the app information and some Javascript to handle the response. In Chrome everything works great but in IE 9+ and Fierfox I cant get the authentication to kick in.
HTML:
<div id="signin-button" class="hide">
<div class="g-signin"
data-callback="loginFinishedCallback"
data-clientid="{my client id}"
data-scope="https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/plus.profile.emails.read"
data-cookiepolicy="single_host_origin"
>
</div>
Javascript:
<script src="https://apis.google.com/js/client:plusone.js" type="text/javascript"></script>
<script type="text/javascript">
var profile, email;
function loginFinishedCallback(authResult) {
if (authResult) {
if (authResult['error'] == undefined){
gapi.client.load('plus','v1', loadProfile); // Trigger request to get the email address.
} else {
console.log('An error occurred');
$( "#signin-button" ).removeClass( "hide" ).addClass( "show" );
}
} else {
console.log('Empty authResult'); // Something went wrong
$( "#signin-button" ).removeClass( "hide" ).addClass( "show" );
}
}
/**
* Uses the JavaScript API to request the user's profile, which includes
* their basic information.
*/
function loadProfile(){
var request = gapi.client.plus.people.get( {'userId' : 'me'} );
request.execute(loadProfileCallback);
}
function loadProfileCallback(obj) {
profile = obj;
email = obj['emails'].filter(function(v) {
return v.type === 'account'; // Filter out the primary email
})[0].value; // get the email from the filtered results, should always be defined.
displayProfile(profile);
}
/**
* Display the user's basic profile information from the profile object.
*/
function displayProfile(profile){...}
</script>
I should mention that I do not get any error on the console when the page loads in Firefox, IE, or Chrome.

Ajax requests are not made on page load when using KnockoutJS

I am new to the whole front-end client scripting scene and have encountered a few difficulties when working on my most recent project. I have looked around the website and could not find anything that answered my question. There may be something here and I have just not found it because of my inexperience and if there is it would be nice if you can provide a link to those resources.
I am currently working on building a client that makes ajax calls to a cross-domain asp.net web api that I have built. I know that the web api works as it has been tested in fiddler. I have also managed to successfully make calls on a click event.
The problem is that I cannot seem to get this working on page load and with knockoutjs. I have tried to do a simple list that is populated with data when the page loads but when I load the page and check fiddler I can see that the ajax calls are not being made. This possibly explains why when I load the page the content isn't there. I have tried inserting some static data to view model and the binding worked so it seems it may be the case that there is something blocking the ajax calls.
I have looked at examples and have knocked up some code. I cannot see any problems with the code but as I am inexperienced there is certainly a possibility that I am missing something. There may also be more efficient ways to do model binding, if so, I would appreciate any advice from someone more experienced.
The code is:
#{
ViewBag.Title = "KnockoutTesting";
}
<!-- MAIN -->
<div id="main">
<!-- wrapper-main -->
<div class="wrapper">
<ul data-bind="foreach: places">
<li>
<span data-bind="text: title"></span>
</li>
</ul>
</div>
</div>
#section scripts {
<script type="text/javascript" src="../../Scripts/jquery-1.7.2.js"></script>
<script type="text/javascript" src="../../Scripts/knockout-2.1.0.js"></script>
<script type="text/javascript">
function PlacesViewModel() {
var self = this;
function Place(root, id, title, description, url, pub) {
var self = this;
self.id = id;
self.title = ko.observable(title);
self.description = ko.observable(description);
self.url = ko.observable(url);
self.pub = ko.observable(pub);
self.remove = function () {
root.sendDelete(self);
};
self.update = function (title, description, url, pub) {
self.title(title);
self.description(description);
self.url(url);
self.pub(pub);
};
};
self.places = ko.observableArray();
self.add = function (id, title, description, url, pub) {
self.places.push(new Place(self, id, title, description, url, pub));
};
self.remove = function (id) {
self.places.remove(function (place) { return place.id === id; });
};
self.update = function (id, title, description, url, pub) {
var oldItem = ko.utils.arrayFirst(self.places(), function (i) { return i.id === id; });
if (oldItem) {
oldItem.update(title, description, url, pub);
}
};
self.sendDelete = function (place) {
$.ajax({
url: "http://localhost:1357/api/places" + place.id,
type: "DELETE"
});
}
};
$(function () {
var viewModel = new PlacesViewModel();
ko.applyBindings(viewModel);
$JQuery.support.cors = true;
$.get("http://localhost:1357/api/places", function (places) {
$.each(places, function (idx, place) {
viewModel.add(place.PlaceID, place.Title, place.Description, place.URL, place.Public);
});
}, "json");
});
</script>
}
It has been simplified for the sake of getting it to work before I add more functionality.
Thanks for your time.
I believe your problem may lie in your Web API implementation. Both the client and the server must support CORS. According to Carlos' post, Web API does not natively support CORS. His post includes a code sample.

Resources