Mount view component only on a single Rails View - ruby-on-rails

I create a view component to handle some selects...
I need to load this component on a single Rails View.
I init my component with:
import Vue from 'vue'
import Product from '../components/product.vue'
import axios from 'axios';
Vue.prototype.$http = axios
document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(document.createElement('app'))
console.log('caricato Vue');
const app = new Vue({
render: h => h(Product)
}).$mount('#product_search')
})
And in my Rails page I have the #product_search div
Rails try to load the component on every page and give me the error:
vue.runtime.esm.js:619 [Vue warn]: Cannot find element: #product_search
Why?

Because Vue tries to render the component (on #product_search) when document is ready, meaning on every page of your application.
You can add a condition to prevent the null error:
document.addEventListener('DOMContentLoaded', () => {
let element = document.querySelector('#product_search')
if (element) {
document.body.appendChild(document.createElement('app'))
console.log('caricato Vue');
const app = new Vue({
render: h => h(Product)
}).$mount('#product_search')
}
})

Related

Vue3 doesn't seem to be loaded in Rails 7 + shakapacker app

I created a new project for a shopify app with rails 7 and shakapacker. I want to use Vue components in my .slim files. The problem is that Vue doesn't seem to be loaded in my app, although I don't get any errors.
Here is what I did:
// config/webpack/rules/vue.js
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
],
resolve: {
extensions: [
'.vue'
]
}
}
// config/webpack/webpack.config.js
const { webpackConfig, merge } = require('shakapacker')
const vueConfig = require('./rules/vue')
module.exports = merge(vueConfig, webpackConfig)
// app/javascript/packs/application.js
import HelloWorld from '../components/HelloWorld'
import { createApp } from 'vue'
const app = createApp({
el: '#app'
})
app.component('helloworld', HelloWorld)
document.addEventListener('DOMContentLoaded', () => {
app
})
// app/javascript/components/HelloWorld.vue
<template>
<h1>Hello world</h1>
</template>
<script>
export default {
name: 'HelloWorld'
}
</script>
/ app/views/layouts/embedded_app.slim
doctype html
html[lang="en"]
head
meta[charset="utf-8"]
- application_name = ShopifyApp.configuration.application_name
title
= application_name
= stylesheet_link_tag "application", "data-turbo-track": "reload"
= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
= csrf_meta_tags
body
#app
.wrapper
main[role="main"]
= yield
= content_tag(:div, nil, id: 'shopify-app-init', data: { api_key: ShopifyApp.configuration.api_key,
shop_origin: #shop_origin || (#current_shopify_session.shop if #current_shopify_session),
host: #host,
debug: Rails.env.development? })
And finally, the view where I just want to display the HelloWorld.vue component:
/ app/views/home/index.slim
helloworld
However, nothing is displayed and I have no errors. I tried to modify the creation of the app in this way, to see if the log appears:
// app/javascript/packs/application.js
import HelloWorld from '../components/HelloWorld'
import { createApp } from 'vue'
const app = createApp({
el: '#app',
created () {
console.log('ok')
}
})
app.component('helloworld', HelloWorld)
document.addEventListener('DOMContentLoaded', () => {
app
})
but then again, I have nothing in console, so I'm not even sure that the app is well rendered. On the other hand, I checked that the DOMContentLoaded event is indeed triggered and it is.
I'm not very comfortable with webpack so I don't know if something is wrong with my configuration, I followed shakapacker's README.
I don't think this is related, but the app is rendered in a Shopify test store via an Ngrok tunnel.
I don't know where to look anymore... Does anyone have an idea?
Thanks in advance
I haven't written any VueJS in a long time, but this is usually what I do in my application.js using React & Shopify Polaris components.
function initialize() {
const rootElement = document.getElementById('app')
const root = createRoot(rootElement);
/* some app bridge code I removed here */
/* react 18 */
root.render(
<BrowserRouter>
/* ... */
</BrowserRouter>
)
}
document.readyState !== 'loading' ? initialize() : document.addEventListener('DOMContentLoaded', () => initialize())
If your <div id="app"> is EMPTY when inspected with browser tools, my first guess would be you're creating an instance, but not actually rendering it in the end.
An application instance won't render anything until its .mount() method is called.
https://vuejs.org/guide/essentials/application.html#mounting-the-app
I would've commented to ask first, but I don't have enough reputation points to do so

React Router, change path alternative of link element

Is it possible to change route in react by react-router by other way than <Link></Link> ?
(e.g change route when onClick, or onKeydown event runs some function)
There is another way. You can use history.push in your code:
import { useHistory } from 'react-router-dom';
const YourComponent = () => {
const history = useHistory();
return <button onClick={() => history.push('/profile')}>Profile</button>;
};

vuejs component inside legacy rails modal

I want to implement a vuejs component inside an existing bootstrap slim modal.
In the modal I reference the container of the component like usually:
form.slim
= modal do
= javascript_pack_tag 'product_asset', 'data-turbolinks-track': 'reload'
#product_assets[
data-product-asset-routes=JsRoutes.generate(include: /product_asset/)
]
end
Outside of a modal it works properly. But in this action it doesn't.
The console output shows this:
Source map error: Error: request failed with status 404
Resource URL: null
Source Map URL: product_asset-ed5ee8937047520ba766.js.map
Anyone of you handled with this kind of problems?
I expected an event to fire. Something like turbolinks:load. But this is an XHR-Request where this kind of stuff doesnt happen.
From:
document.addEventListener(turbolinks:load => ({
const productAsset = new Vue({
el: '#product_asset_form',
store,
railsI18n,
productAssetRoutes,
render: h => h(ProductAsset, { props: { ...root_element.dataset } }),
}).$mount()
)}
To:
const productAsset = new Vue({
el: '#product_asset_form',
store,
// this is vue-i18n magic...
i18n: railsI18n,
productAssetRoutes,
render: h => h(ProductAsset, { props: { ...root_element.dataset } }),
}).$mount()
And now it fires immediatly

Old component retains state on back button

I have a link in a modal that goes to a new page, and I can't seem to reset the state of the component with the modal when directing to that page.
The component with the modal seems to be keeping its state after directing to the new page, because when I hit the back button, it automatically opens the modal.
The modal is either opened or closed based on the state of modalIsOpen.
So I have my simplified Listings component:
import React from 'react'
import ListingModalContent from '../ListingModalContent'
import Modal from '../Modal'
export default class Listings extends React.Component {
constructor(props) {
super(props)
this.state = {
modalIsOpen: false,
modalContent: null
}
}
modalClick = (e, listing) => {
e.preventDefault()
this.setState({
modalContent: <ListingModalContent listing={listing}/>
}, () => {
this.setState({modalIsOpen: true})
})
}
modalClose = () => {
this.setState({modalIsOpen: false})
}
componentWillMount() {
this.setState({modalIsOpen: false})
console.log('mounting...')
console.log(this.state.modalIsOpen)
}
componentWillUnmount() {
console.log('unmounting...')
this.setState({
modalIsOpen: false
}, () => {
console.log('got here...')
console.log(this.state.modalIsOpen)
})
console.log(this.state.modalIsOpen)
}
render() {
const listings = this.props.listings.map(listing => (<div className="listing">
<a href="#" onClick={e => this.modalClick(e, listing)}>More Details</a>
</div>))
return (<div id="listings">
<section className="listings">
{listings}
<Modal visible={this.state.modalIsOpen} onClose={this.modalClose}>
{this.state.modalContent}
</Modal>
</section>
</div>)
}
}
And my ListingsModalContent component:
import React from 'react'
export default class ListingModalContent extends React.Component {
constructor(props) {
super(props)
}
render() {
const {listing} = this.props
return (<div className="listing-modal">
<div className="details">
<h2 className="address">{listing.address}</h2>
<p className="description">{listing.description}</p>
</div>
<div className="btn-container">
<a href={`/listings/${listing.slug}`} onClick={this.props.modalClose}>View Full Listing</a>
</div>
</div>)
}
}
The console output is...
// after initially mounting:
mounting...
false
// after clicking the listing link:
unmounting...
true
// after hitting the back button:
mounting...
false
I'm pretty sure I need to fix this by using componentWillUnmount to set the state of modalIsOpen to false before the component unmounts, but it never seems to finish setting the state before unmounting.
I'm using react on rails, which seems to use some hybrid routing rails/react routing system, but I'm not too familiar with it, and don't want to go down that rabbit hole at the moment if I don't have to.
So my question is, if this is expected behavior of the react component lifecycle, is there a way I can ensure the state of modalIsOpen is reset before unmounting? Or is there a way I can make sure my state is reset to its initial state when going back? Or is this more likely a consequence of the routing system I'm using?
This is strange, unexpected bahaviour in react and for sure is not caused by react (as #azium stated) but some 'things around', probably react_on_rails issue (or 'feature'). Report a bug/create an issue on github.
As you see in log state has proper value on mounting and there is no reason to render modal. 'Normal' react would work as expected.
There is no sense to set state on unmount - instance of component will be destroyed, its state, too.
HINTS
You shouldn't store modal content in state. It's possible, it works for simple cases, it can be used a kind of cache for parts of content, but you can have issues when conditional rerendering needed (using prop/state changes).
After setting state this.setState({modalIsOpen: true, modalContent:listing}) in click handler you can use conditional rendering (in render):
{this.state.modalIsOpen && <ListingModalContent listing={this.state.modalContent}/>}
To be true even this.setState({modalIsOpen: true}) can be removed (by save only listing idx in state, '-1' for closing) but then code can be less readable (storing additional pointer is cheap).

You have included the Google Maps JavaScript API multiple times on this page

how can I avoid “You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.” if I am using google-map-react to display the map and react-places-autocomplete in another component to get the address and coordinates ?
//LocationMapPage component that displays the map box and pass the props to it
class LocationMapPage extends Component {
render() {
let {latLng,name,address} = this.props.location;
return (
<MapBox lat={latLng.lat} lng={latLng.lng} name={name} address={address}/>
)
}
}
//MapBox component
import React from "react";
import GoogleMapReact from 'google-map-react';
import apiKey from "../../configureMap";
const Marker = () => <i className="fa fa-map-marker fa-2x text-danger" />
const MapBox = ({lat,lng, name, address}) => {
const center = [lat,lng];
const zoom = 14;
return (
<div style={{ height: '300px', width: '100%' }}>
<GoogleMapReact
bootstrapURLKeys={{ key: apiKey }}
defaultCenter={center}
defaultZoom={zoom}
>
<Marker
lat={lat}
lng={lng}
text={`${name}, ${address}`}
/>
</GoogleMapReact>
</div>
);
}
export default MapBox;
Map is blank:
The Error in the console:You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.
How to solve?
I am using google-map-react, react-places-autocomplete in the project.
AS temporary solution to my specific use case where I use the google map API's in two different components I have just added the script in the index.html:
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>
I did it in order to avoid that particular error as per of the documentation on the react-places-autocomplete GitHub page.
Unfortunately the link in the head of the index.html caused the same error. I found another workaround. Not the best solution, but works for now:
import React, { useEffect, useState } from 'react';
import GoogleMapReact from 'google-map-react';
export default () => {
const [mapActive, setMapActive] = useState(false);
useEffect(() => {
const t = setTimeout(() => {
setMapActive(true)
}, 100);
return () => {
window.clearTimeout(t);
};
}, [])
return (
<>
{ mapActive && <GoogleMapReact
bootstrapURLKeys={ {
key: ...,
language: ...
} }
defaultCenter={ ... }
defaultZoom={ ... }
>
</GoogleMapReact> }
</>
);
};
You could set a global variable and load the Google JavaScript only if the global variable is not set:
<script type="text/javascript">
if(document.isLoadingGoogleMapsApi===undefined) {
document.isLoadingGoogleMapsApi=true;
var script = document.createElement('script');
script.src='https://maps.googleapis.com/maps/api/js?key=[your-key]&callback=[yourInitMethodName]&v=weekly';
script.type='text/javascript';
script.defer=true;
document.getElementsByTagName('head')[0].appendChild(script);
}else{
[yourInitMethodName]();
}
</script>
In my case there is an arbitrary number of maps in a web application (starting at 0) and the user can add additional maps at runtime.
Most of the users do not use any map so loading it by default would cost unnecessarily loading time.

Resources