How to use Mapbox geocoder with Quasar select? - mapbox

I'm trying to create an Autocomplete component using the Mapbox Geocode API and Quasar's <q-select /> component. It appears though that Mapbox requires using their input (could be wrong about this), so I'm having trouble hooking it up to the select.
I've tried using the #mapbox/mapbox-gl-geocoder, vue-mapbox-ts and v-mapbox-geocoder libraries now. The two third-party libraries had some issues with them, so I'd prefer to use the one direct from Mapbox if possible.
<template>
<q-select
v-model="state.location"
:options="state.locations?.features"
:option-value="(result: MapboxGeocoder.Result) => result.place_name"
:option_label="(result: MapboxGeocoder.Result) => result.place_name"
:loading="state.loadingResults"
clear-icon="clear"
dropdown-icon="expand_more"
clearable
outlined
use-input
dense
label="Location">
<template #prepend>
<q-icon name="place " />
</template>
</q-select>
</template>
<script lang='ts' setup>
import { reactive, ref, onMounted } from 'vue';
import MapboxGeocoder from '#mapbox/mapbox-gl-geocoder';
const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN as string;
const state = reactive({
first_name: auth.currentUser?.first_name || undefined,
last_name: auth.currentUser?.last_name || undefined,
location: undefined,
locations: undefined as undefined | MapboxGeocoder.Results,
loadingResults: false,
geocoder: null as null | MapboxGeocoder,
});
onMounted(() => {
state.geocoder = new MapboxGeocoder({
accessToken,
types: 'country,region,place,postcode,locality,neighborhood',
});
state.geocoder?.on('result', (e) => {
console.log('on result: ', e);
});
state.geocoder?.on('results', (e) => {
console.log('results: ', e);
state.locations = e.features;
});
state.geocoder?.on('loading', (e) => {
console.log('loading');
state.loadingResults = true;
});
});
</script>
In the code sample above, none of the console logs are being run. If I add an empty <div id="geocoder" /> and then use the state.geocoder.addTo('#geocoder') function, it renders the Mapbox input and hits the console logs, but then I am unable to use the Quasar select like I'm hoping to.
How can I go about accomplishing this?

I never tracked down the reason why your seemingly correct syntax failed, but if I used this alternative:
const function results(e) {
console.log('results: ', e);
state.locations = e.features;
}
state.geocoder?.on('results', results);
everything magically worked.

MapboxGeocoder is a UI control, it's not meant to be used in a "headless" mode.
As you create your own control, you could just use the Mapbox Geocoder API, see https://docs.mapbox.com/api/search/geocoding/ for more information on how this works.

Related

How to pass DOM elements for libraries (eg. ChartJS, Hightcharts) in Virtual DOMs (such as Qwik)?

Background
I have personally used React, Vue and Angular extensively in the past. And a lot of times I need to create applications with charts generated within them from selective data. I'm recently trying out Qwik due to its promise of speed and attempted to create charts within it using ChartJs. But while ChartJs has separate libraries available for React, Vue, Angular, Svelte, etc. it does not have one for Qwik understandably.
Issue
Many plugins such as Highcharts and ChartJs often require a DOM element to be sent to its functions to identify where to render their output. But when we are dealing with virtual DOMs, I can't run JS selector scripts to fetch DOM elements and pass them into a function within a component. Therefore, as of now, I have not been able to use ChartJs in my Qwik project.
Attempts
I have only looked for solutions for this issue and not found any workable approaches. From ChartJs docs the following code is their raw JS way of implementing charts:
new Chart(
document.getElementById('acquisitions'),
{
type: 'bar',
data: {
labels: data.map(row => row.year),
datasets: [
{
label: 'Acquisitions by year',
data: data.map(row => row.count)
}
]
}
}
);
As expected document.getElementById does not work inside a component and that is where I'm stuck. I've only created the useMount$() function where I expect to place the logic for generating my chart and also looked around for React solutions by perhaps using references and what not. But, other than that, I have been unable to find anything more.
I understand that looking at the source code of the React library for ChartJs would provide me clues but while I investigate a library (which I find difficult at my current level) I was hoping for a pointer to the solution from the Stack Overflow community.
Searching "ref" on the Qwik docs does not return any search results but I had found the git project from another developer online and tried to replicate the use of references from his approach:
Child component code:
import { component$, useMount$, Ref, useStylesScoped$ } from "#builder.io/qwik";
import { Chart } from 'chart.js/auto';
interface GraphProps {
data: object[];
reference: Ref<Element>;
}
export default component$((props: GraphProps) => {
useStylesScoped$(styles);
useMount$(() => {
new Chart(
props.reference.value,
{
<... options here ...>
}
);
});
return (
<div id="chartContent">
</div>
);
});
Parent component code:
import { component$, useRef } from "#builder.io/qwik";
import ContentCard from "../components/contentCard/contentCard";
import ChartJSGraph from "../components/chartJSGraph/chartJSGraph";
...
export default component$(() => {
const leftChartContainer = useRef();
return (
<div>
<div className="row">
<ContentCard>
<div className="graph-container">
<ChartJSGraph
data={[
{ year: 2010, count: 10 },
...
]}
reference={leftChartContainer}
/>
</div>
</ContentCard>
</div>
</div>
)
});
As these are just findings from a YouTuber's code it could be outdated so is certainly not necessarily a reliable source. But so far searching the official docs have not led me to any official approach for references.
The DOM element that is passed to the charting library can only be accessed once it has been mounted to the page. Qwik/Vue/React all provide component mounted hooks.
https://qwik.builder.io/docs/components/lifecycle/#usemount
https://vuejs.org/api/composition-api-lifecycle.html#onmounted
https://reactjs.org/docs/react-component.html#componentdidmount
Inside these mounted hooks you can reference your DOM element via id or querySelector or using the internal DOM reference feature of Qwuik/Vue/React and then use that when initialising the chart. The latter is the cleaner approach.
For example, in Vue:
<template>
<div id="acquisitions" ref="chartEl"></div>
</template>
<script setup>
import Chart from 'chart.js/auto';
import { ref, onMounted } from 'vue';
const chartEl = ref(null)
onMounted(() => {
const chartOpts = {
type: 'bar',
data: {
labels: data.map(row => row.year),
datasets: [
{
label: 'Acquisitions by year',
data: data.map(row => row.count)
}
]
}
}
new Chart(
chartEl.value,
chartOpts
);
})
</script>
Solution
Sadly this was a silly issue of perhaps on my network side or god knows what why the search engine on the Qwik doc never suggested anything for me when I looked up "Ref" in their docs. But my problem has been solved after finding the following link:
https://qwik.builder.io/tutorial/hooks/use-signal/#example
For future reference for myself or any beginners facing the similar issue, I'm writing down my implementation below:
// Root component
import { component$, useSignal } from "#builder.io/qwik";
...
import ChartJSGraph from "../components/chartJSGraph/chartJSGraph";
export default component$(() => {
const chartData1 = useSignal({
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [{
label: 'Inventory Value per Outlet',
data: [65, 59, 80, 81, 56, 55, 40],
fill: false,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
});
return (
<div class="w-100 h-100">
...
<ChartJSGraph
width={'100%'}
height={'25px'}
chartData={chartData1.value}
/>
</div>
);
});
And here's the code for my ChartJSGraph component that uses the data supplied to generate the chart while using the reference of the canvas element to point to ChartJS where to create the chart.
// ChartJSGraph component
import { component$, useClientEffect$, useSignal } from "#builder.io/qwik";
import { Chart } from 'chart.js/auto';
...
interface GraphProps {
height: string;
width: string;
chartData: object;
}
export default component$((props: GraphProps) => {
const outputRef = useSignal<Element>();
useClientEffect$(() => {
new Chart(
outputRef.value,
{
type: 'line',
data: props.chartData
}
);
});
return (
<>
<canvas ref={outputRef} width={props.width} height={props.height}>
</canvas>
</>
);
});

Mapbox GL: How can I handle invalid/expired access tokens?

I've implemented Mapbox GL:
script.src = 'https://api.mapbox.com/mapbox-gl-js/v2.8.2/mapbox-gl.js';
script.onload = function() {
mapboxgl.accessToken = 'invalid_token';
map = new mapboxgl.Map({
container: 'mapsection', // container ID
style: 'mapbox://styles/mapbox/streets-v11' // style URL
});
}
If the access token is invalid or expired then a message is shown in the console, but how can I handle this in my code? I've tried both try .. catch and map.on('error'), but neither acknowledge there is an error. Any operations on the map are performed without errors, but there is just nothing to see on the page.
Alternatively, is there an API to validate a given token?
I don't know for sure, but if you take one of the URLs that are being requested (by looking in developer tools), and using fetch to query that URL, you will probably get back either 200 for a correct token, or 401 or 403 for an invalid token (or other issue).
Looks like I was almost there, but just made a small mistake. It is indeed the map.on('error') event handler I need to use:
script.src = 'https://api.mapbox.com/mapbox-gl-js/v2.8.2/mapbox-gl.js';
script.onload = function() {
mapboxgl.accessToken = 'invalid_token';
map = new mapboxgl.Map({
container: 'mapsection', // container ID
style: 'mapbox://styles/mapbox/streets-v11' // style URL
});
map.on('error', (response) => {
alert(response.error.message)
});
}
Using map.on('error') results in Mapbox GL (v2.12.0) creating the full HTML DIV structure even when a Mapbox access token is invalid.
<div id="map-container" class="mapboxgl-map"><div class="mapboxgl-canary" style="visibility: hidden;"></div><div class="mapboxgl-canvas-container mapboxgl-interactive mapboxgl-touch-drag-pan mapboxgl-touch-zoom-rotate"><canvas class="mapboxgl-canvas" tabindex="0" aria-label="Map" role="region" width="780" height="724" style="width: 519.115px; height: 482.542px;"></canvas></div><div class="mapboxgl-control-container"><div class="mapboxgl-ctrl-top-left"></div><div class="mapboxgl-ctrl-top-right"><div class="mapboxgl-ctrl mapboxgl-ctrl-group"><button class="mapboxgl-ctrl-zoom-in" type="button" aria-label="Zoom in" aria-disabled="false"><span class="mapboxgl-ctrl-icon" aria-hidden="true" title="Zoom in"></span></button><button class="mapboxgl-ctrl-zoom-out" type="button" aria-label="Zoom out" aria-disabled="false"><span class="mapboxgl-ctrl-icon" aria-hidden="true" title="Zoom out"></span></button><button class="mapboxgl-ctrl-compass" type="button" aria-label="Reset bearing to north"><span class="mapboxgl-ctrl-icon" aria-hidden="true" title="Reset bearing to north" style="transform: rotate(0deg);"></span></button></div></div><div class="mapboxgl-ctrl-bottom-left"><div class="mapboxgl-ctrl" style="display: block;"><a class="mapboxgl-ctrl-logo" target="_blank" rel="noopener nofollow" href="https://www.mapbox.com/" aria-label="Mapbox logo"></a></div></div><div class="mapboxgl-ctrl-bottom-right"></div></div></div>
To avoid the unnecessary code execution by mapbox-gl.js, I used #Steve's suggestion of using a fetch query to a Mapbox API. A request to a map styles API URL results in an ~70KB response when the access token is valid. A request to the Mapbox geocoding API (version 5 is older; v6 is the most current version as of Feb 2023), using a non-existent place as the search string results in a 343 byte response.
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/rndstrasdf.json?access_token=${mapboxAccessToken}`;
This all seems unnecessary, however, as it would more efficient if Mapbox provided an access token verification API before executing any mapbox-gl in much the same way they provide a mapbox-gl-supported plugin.
For performance reasons, it is better to check that Mapbox GL JS is
supported before going to the trouble of loading the script and
initializing the map on your page.
document.addEventListener('DOMContentLoaded', function() {
loadMap()
.then(map => console.log("Map loaded successfully into element with ID: " + map._container.id))
.catch(error => console.error("Map load failed with the error: " + error.message));
});
function loadMap() {
return new Promise((resolve, reject) => {
const mapboxAccessToken = "ADD_YOUR_VALID_OR_INVALID_ACCESS_TOKEN";
// Using the following URL in a 'fetch' API results in a ~70KB response.
//const url = `https://api.mapbox.com/styles/v1/mapbox/streets-v11?access_token=${mapboxAccessToken}`;
//const url = `https://api.mapbox.com/styles/v1/mapbox/streets-v11?access_token=invalid`;
// A URL to Mapbox geocoding to validate a Mapbox access token
// results in a 343 byte response using a non-existent place name.
// Place search at https://www.mapbox.com/geocoding
// Temporary Geocoding API pricing https://www.mapbox.com/pricing#search
// A valid place name -> "Los Angeles"
//const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/Los Angeles.json?access_token=${mapboxAccessToken}`;
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/rndstrasdf.json?access_token=${mapboxAccessToken}`;
fetch(url)
.then(response => {
if (!response.ok) {
response.message = "Connected to Mapbox service but with an invalid access token.";
reject(response);
return;
}
// Request succeeded. Response is an empty GeoJSON 'features' collection
// 343 bytes
/*
'{"type":"FeatureCollection","query":["rndstrasdf"],"features":[],
"attribution":"NOTICE: © 2023 Mapbox and its suppliers. All rights reserved.
Use of this data is subject to the Mapbox Terms of Service
(https://www.mapbox.com/about/maps/). This response and the information
it contains may not be retained. POI(s) provided by Foursquare."}'
*/
response.text().then(text => {
console.log(text);
});
mapboxgl.accessToken = mapboxAccessToken;
// stackoverflow.com/questions/72254578/how-to-solve-that-a-valid-mapbox-access-token-is-required-to-use-mapbox-gl-js
// github.com/mapbox/mapbox-gl-js/releases/tag/v2.0.0
// "Beginning with v2.0.0, a billable map load occurs whenever a
// Map object is initialized. Before updating an existing
// implementation from v1.x.x to v2.x.x, please review the
// pricing documentation to estimate expected costs."
const map = new mapboxgl.Map({
container: "map-container",
style: 'mapbox://styles/mapbox/streets-v11',
center: [12.79690, 47.32350], // Longitude, latitude
zoom: 5
});
// Add zoom and rotation controls to the map
// docs.mapbox.com/mapbox-gl-js/example/navigation
map.addControl(new mapboxgl.NavigationControl());
map.on('load', () => resolve(map));
map.on('error', error => reject(error));
})
.catch(error => {
reject(error);
});
});
}
<link href='https://api.mapbox.com/mapbox-gl-js/v2.12.0/mapbox-gl.css' rel='stylesheet' />
<script src='https://api.mapbox.com/mapbox-gl-js/v2.12.0/mapbox-gl.js'></script>
<div id="map-container" style="width: 100%; height: 80vh;"></div>

Mocking authentication when testing MSAL React Apps

Our app is wrapped in the MSAL Authentication Template from #azure/msal-react in a standard way - key code segments are summarized below.
We would like to test app's individual components using react testing library (or something similar). Of course, when a React component such as SampleComponentUnderTest is to be properly rendered by a test as is shown in the simple test below, it must be wrapped in an MSAL component as well.
Is there a proper way to mock the MSAL authentication process for such purposes? Anyway to wrap a component under test in MSAL and directly provide test user's credentials to this component under test? Any references to useful documentation, blog posts, video, etc. to point us in the right direction would be greatly appreciated.
A Simple test
test('first test', () => {
const { getByText } = render(<SampleComponentUnderTest />);
const someText = getByText('A line of text');
expect(someText).toBeInTheDocument();
});
Config
export const msalConfig: Configuration = {
auth: {
clientId: `${process.env.REACT_APP_CLIENT_ID}`,
authority: `https://login.microsoftonline.com/${process.env.REACT_APP_TENANT_ID}`,
redirectUri:
process.env.NODE_ENV === 'development'
? 'http://localhost:3000/'
: process.env.REACT_APP_DEPLOY_URL,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case LogLevel.Error:
console.error(message);
return;
case LogLevel.Info:
console.info(message);
return;
case LogLevel.Verbose:
console.debug(message);
return;
case LogLevel.Warning:
console.warn(message);
return;
default:
console.error(message);
}
},
},
},
};
Main app component
const msalInstance = new PublicClientApplication(msalConfig);
<MsalProvider instance={msalInstance}>
{!isAuthenticated && <UnauthenticatedHomePage />}
{isAuthenticated && <Protected />}
</MsalProvider>
Unauthenticated component
const signInClickHandler = (instance: IPublicClientApplication) => {
instance.loginRedirect(loginRequest).catch((e) => {
console.log(e);
});
};
<UnauthenticatedTemplate>
<Button onClick={() => signInClickHandler(instance)}>Sign in</Button>
</UnauthenticatedTemplate>
Protected component
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
errorComponent={ErrorComponent}
loadingComponent={LoadingComponent}
>
<SampleComponentUnderTest />
</MsalAuthenticationTemplate>
I had the same issue as you regarding component's test under msal-react.
It took me a couple of days to figure out how to implement a correct auth mock.
That's why I've created a package you will find here, that encapsulates all the boilerplate code : https://github.com/Mimetis/msal-react-tester
Basically, you can do multiple scenaris (user is already logged, user is not logged, user must log in etc ...) in a couple of lines, without having to configure anything and of course without having to reach Azure AD in any cases:
describe('Home page', () => {
let msalTester: MsalReactTester;
beforeEach(() => {
// new instance of msal tester for each test
msalTester = new MsalReactTester();
// spy all required msal things
msalTester.spyMsal();
});
afterEach(() => {
msalTester.resetSpyMsal();
});
test('Home page render correctly when user is logged in', async () => {
msalTester.isLogged();
render(
<MsalProvider instance={msalTester.client}>
<MemoryRouter>
<Layout>
<HomePage />
</Layout>
</MemoryRouter>
</MsalProvider>,
);
await msalTester.waitForRedirect();
let allLoggedInButtons = await screen.findAllByRole('button', { name: `${msalTester.activeAccount.name}` });
expect(allLoggedInButtons).toHaveLength(2);
});
test('Home page render correctly when user logs in using redirect', async () => {
msalTester.isNotLogged();
render(
<MsalProvider instance={msalTester.client}>
<MemoryRouter>
<Layout>
<HomePage />
</Layout>
</MemoryRouter>
</MsalProvider>,
);
await msalTester.waitForRedirect();
let signin = screen.getByRole('button', { name: 'Sign In - Redirect' });
userEvent.click(signin);
await msalTester.waitForLogin();
let allLoggedInButtons = await screen.findAllByRole('button', { name: `${msalTester.activeAccount.name}` });
expect(allLoggedInButtons).toHaveLength(2);
});
I am also curious about this, but from a slightly different perspective. I am trying to avoid littering the code base with components directly from msal in case we want to swap out identity providers at some point. The primary way to do this is to use a hook as an abstraction layer such as exposing isAuthenticated through that hook rather than the msal component library itself.
The useAuth hook would use the MSAL package directly. For the wrapper component however, I think we have to just create a separate component that either returns the MsalProvider OR a mocked auth provider of your choice. Since MsalProvider uses useContext beneath the hood I don't think you need to wrap it in another context provider.
Hope these ideas help while you are thinking through ways to do this. Know this isn't a direct answer to your question.

Next js Strapi integration not displaying data

I am trying to build a simple task website to get familiar with full stack development. I am using Next js and Strapi. I have tried all I can think of, but the data from the server just will not display on the frontend. It seems to me that the page loads too soon, before the data has been loaded in. However, I am not a full stack dev and am therefore not sure.
import axios from 'axios';
const Tasks = ({ tasks }) => {
return (
<ul>
{tasks && tasks.map(task => (
<li key={task.id}>{task.name}</li>
))}
</ul>
);
};
export async function getStaticProps() {
const res = await axios.get('http://localhost:1337/tasks');
const data = await res.data;
if (!data) {
return {
notFound: true,
}
} else {
console.log(data)
}
return {
props: { tasks: data },
};
};
export default Tasks;
I had the same issue. You need to call the api from the pages files in the pages folder. I don't know why this is but that's how it works.

Where to implement XSS prevention in symfony-based REST API and Vue.js front-end

I'm building an application that requires html tags to be allowed for user comments in Vue.js.
I wan't to allow users to input a certain selection of HTML tags(p, i, ul, li) and escape/sanitize other like script or div.
Right now I see three ways of dealing with this issue:
On rendering the content with Vue.js
Before sending the response in Symfony(I'm using JMS Serializer)
Upon receiving request to the API
Personally I think that we could save the data to database with tags like script or div, and just sanitize them before sending a response.
Basically my question is where should I implement the prevention and should I allow tags like script into my database?
If you're using v-html to render the comments, then there's always the possibility of XSS. Strict HTML sanitization can mitigate the risk, but you never know.
The only surefire way to prevent XSS is to never use v-html or innerHTML. This means you'll have to parse the HTML (using DOMParser) and render the comments manually.
For something like this it will be easier if you write the render function manually so you have full control over how the comment content will be rendered – only render the HTML tags you choose. Whitelist instead of blacklist.
Don't render user-defined HTML attributes.
HTML sanitization won't be necessary on the server because the HTML will never be rendered as-is in the browser, but you can still sanitize it if you want to trim the fat beforehand.
Here's a basic example:
Vue.component('comment-content', {
functional: true,
props: {
html: {},
allowedElements: {
default: () => ['p', 'i', 'b', 'ul', 'li'],
},
},
render(h, ctx) {
const { html, allowedElements } = ctx.props;
const renderNode = node => {
switch (node.nodeType) {
case Node.TEXT_NODE: return renderTextNode(node);
case Node.ELEMENT_NODE: return renderElementNode(node);
}
};
const renderTextNode = node => {
return node.nodeValue;
};
const renderElementNode = node => {
const tag = node.tagName.toLowerCase();
if (allowedElements.includes(tag)) {
const children = [...node.childNodes].map(node => renderNode(node));
return h(tag, children);
}
};
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return [...doc.body.childNodes].map(node => renderNode(node));
},
});
new Vue({
el: '#app',
data: {
html: `
<p>Paragraph</p>
<ul>
<li>One <script>alert('Hacked')<\/script></li>
<li onmouseover="alert('Hacked')">Two</li>
<li style="color: red">Three <b>bold</b> <i>italic</i></li>
<li>Four <img src="javascript:alert('Hacked')"></li>
</ul>
<section>This element isn't allowed</section>
<p>Last paragraph</p>
`,
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<comment-content :html="html"></comment-content>
</div>