I'm trying to make a Vue app where one component has to load a 3D model that I want to modify using a user interface later on. I've been doing some research on how to do this but anything I find is either from older versions of Vue or just does not work.
I tried loading a three scene in my component, there are no errors, but there is also nothing showing up here is my component's code:
const viewPort = Vue.ref(null);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
mounted(){
const renderer = new THREE.WebGLRenderer({
//render in canvas
canvas: viewPort.value
});
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
//import donut glb
let donut;
const loader = new THREE.GLTFLoader();
loader.load('/bignut.glb', (gltf) => {
donut = gltf.scene;
donut.scale.set(40,40,40);
scene.add(donut);
});
}
body{
background-color: red;
}
<template>
<canvas ref="viewPort">
</canvas>
<h1>test</h1>
</template>
<script src="https://cdn.jsdelivr.net/npm/three#0.122.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.122.0/examples/js/controls/OrbitControls.min.js"></script>
<script src="https://unpkg.com/three#0.122.0/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://unpkg.com/vue#3"></script>
Related
I have a simple, sample project at https://github.com/ericg-vue-questions/leaflet-test/tree/feature/simple-marker
(note the feature/simple-marker branch)
The relevant code is in the LeafletTest.vue file
<template>
<div id="container">
<div id="mapContainer"></div>
</div>
</template>
<script>
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import Vue from 'vue';
export default {
name: 'Map',
methods: {
async setupLeafletMap() {
const center = [37, -122];
const mapDiv = L.map('mapContainer').setView(center, 13);
var tiles = new L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
minZoom: 3,
maxZoom: 8
}).addTo(mapDiv);
L.marker(center).addTo(mapDiv);
}
},
async mounted() {
await Vue.nextTick();
await this.setupLeafletMap();
}
};
</script>
<style scoped>
#mapContainer {
width: 500px;
height: 500px;
}
</style>
When the code run, the default marker shows up as:
The URL in the img tag that L.marker(center).addTo(mapDiv); adds to the map is
<img src="marker-icon.png" class="leaflet-marker-icon leaflet-zoom-animated leaflet-interactive" alt="Marker" tabindex="0" role="button" style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; transform: translate3d(250px, 250px, 0px); z-index: 250;">
I figure there is something extra I need to do in configuring the vue app so it and leaflet can work together in this case.
What do I need to change so the default marker-icon.png will show up by default?
One answer I found was to modify the mounted() method to be:
async mounted() {
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
});
await Vue.nextTick();
await this.setupLeafletMap();
}
The part that was added was:
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
});
If I understand correctly what is going on here is that webpack will see the require's and assist in making sure the right thing happens. If anyone has a more detailed explanation of why this works, I would be interested.
I have to change the default icon on the Locate widget on arcGIS 4.18. The default icon class is, esri-icon-locate how can I change it to the class, 'esri-icon-navigation'?
I am going through the documentation,
https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Locate.html#iconClass
I have tried to use the property, 'iconClass'. But not reflecting in the map icon. Please find the code below,
var locateBtn = new Locate({
view: view,
// iconClass: '\ue666'
iconClass: 'esri-icon-navigation'
});
view.ui.add(locateBtn, {
position: "manual",
});
KER,
You actually right, does not work as expected. Setting iconClass should be the solution.
Funny fact if you check the default iconClass is actually esri-icon-north-navigation, which obviously in not.
Anyway, I am gonna give a dirty solution, just overlap the class you want,
view.when(_ => {
const n = document.getElementsByClassName("esri-icon-locate");
if (n && n.length === 1) {
n[0].classList += " esri-icon-navigation"
}
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
<title>Locate button | Sample | ArcGIS API for JavaScript 4.18</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.18/esri/themes/light/main.css" />
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
</style>
<script src="https://js.arcgis.com/4.18/"></script>
<script>
require([
"esri/Map",
"esri/views/MapView",
"esri/widgets/Locate"
], function (Map, MapView, Locate) {
var map = new Map({
basemap: "topo-vector"
});
var view = new MapView({
container: "viewDiv",
map: map,
center: [-56.049, 38.485, 78],
zoom: 3
});
var locateBtn = new Locate({
view: view
});
// Add the locate widget to the top left corner of the view
view.ui.add(locateBtn, {
position: "top-left"
});
view.when(_ => {
const n = document.getElementsByClassName("esri-icon-locate");
if (n && n.length === 1) {
n[0].classList += " esri-icon-navigation"
}
});
});
</script>
</head>
<body>
<div id="viewDiv"></div>
</body>
</html>
I have a component that displays an image if the screen is desktop and it hides the image if the screen is for mobile devices:
<script>
export default {
name: 'MyComponentApp',
}
</script>
<template>
<div class="my-component">
<div class="my-component__image-container">
<img class="my-component__image-container--img" />
</div>
</div>
</template>
<style lang="scss" scoped>
.my-component {
&__image-container {
overflow: hidden;
width: 50%;
&--img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
#media (max-width: 600px) {
.my-component {
&__image-container {
&--img {
display: none;
}
}
}
}
</style>
When I try to do the unit test case and test if the image is hidden when the window.width is below 600px, it doesn't update the DOM and the image is still visible:
import MyComponentApp from './MyComponentApp.vue';
import { shallowMount } from '#vue/test-utils';
const factory = () => {
return shallowMount(MyComponentApp, {});
};
describe('DownloadApp.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = factory();
});
describe('Check Items on Mobile Devices', () => {
it('Img on div.my-component__image-container shouldn\'t be displayed', async () => {
jest.spyOn(screen, 'height', 'get').mockReturnValue(500);
jest.spyOn(screen, 'width', 'get').mockReturnValue(500);
await wrapper.vm.$nextTick();
const image = wrapper.find('div.my-component__image-container > img');
expect(image.isVisible()).toBe(false);
});
});
});
However, the test fails:
DownloadApp.vue › Check Items on Mobile Devices › Img on div.my-component__image-container shouldn\'t be displayed
expect(received).toBe(expected) // Object.is equality
Expected: false
Received: true
Does anybody know how to update the DOM or make the test case realized that the screen width has changed and the image should be displayed?
CSS #media queries depend on the viewport size, which currently cannot be manipulated from Jest alone, and setting window.innerWidth in Jest won't have an effect in this case.
Alternatively, you could resize the viewport in Cypress tests. In a Vue CLI project, you could add the Cypress plugin by running this command from the root of your project:
vue add e2e-cypress
Then in <root>/tests/e2e/test.js, insert the following tests that use cy.viewport() to set the viewport size before checking the img element's visibility:
describe('My img component', () => {
it('should show image for wide viewport', () => {
cy.visit('/') // navigate to page where test component exists
cy.viewport(800, 600)
cy.get('.my-component__image-container--img').should('be.visible')
})
it('should hide image for narrow viewport', () => {
cy.visit('/') // navigate to page where test component exists
cy.viewport(500, 600)
cy.get('.my-component__image-container--img').should('not.be.visible')
})
})
There is a case that we need a component in global to use it like this.$toast('some words') or this.$dialog({title:'title words',contentText:'some words').
In Vue 2.x, we can add Toast.vue's methods to Vue.prototype, and call Toast.vue's methods everywhere. But how do we do this in Vue 3.x?
I read the document of i18n plugin demo in vue-next. But it needs to inject the i18n plugin into every component that needs to use it. It's not convenient.
A way showing the component anywhere in vue3 app without injection
mechanism
mountting the component into dom each time.
implementation
use 'Toast' for example:
step 1: create a SFC (Toast.vue)
<template>
<transition name="fade">
<div class="toast" v-html="msg" :style="style" #click="closeHandle"></div>
</transition>
</template>
<script>
import {ref,computed,onMounted,onUnmounted} from 'vue'
export default {
name: "Toast",
props:{
msg:{type:String,required:true},
backgroundColor:{type:String},
color:{type:String},
// closing the Toast when timed out. 0:not closed until to call this.$closeToast()
timeout:{type:Number,default:2000, validate:function (val){return val >= 0}},
// closing the Toast immediately by click it, not wait the timed out.
clickToClose:{type:Boolean, default: true},
// a function provied by ToastPlugin.js, to unmout the toast.
close:{type:Function,required: true}
},
setup(props){
let innerTimeout = ref();
const style = computed(
()=>{return{backgroundColor:props.backgroundColor ? props.backgroundColor : '#696969', color:props.color ? props.color : '#FFFFFF'}}
);
onMounted(()=>{
toClearTimeout();
if(props.timeout > 0)
innerTimeout.value = setTimeout(()=>{ props.close(); },props.timeout);
});
/**
* when toast be unmounted, clear the 'innerTimeout'
*/
onUnmounted(()=>{toClearTimeout()})
/**
* unmount the toast
*/
const closeHandle = () => {
if(props.clickToClose)
props.close();
}
/**
* to clear the 'innerTimeout' if it exists.
*/
const toClearTimeout = ()=>{
if(innerTimeout.value){
try{
clearTimeout(innerTimeout.value);
}catch (e){
console.error(e);
}
}
}
return {style,closeHandle};
},
}
</script>
<style scoped>
.toast{position: fixed; top: 50%; left: 50%; padding: .3rem .8rem .3rem .8rem; transform: translate(-50%,-50%); z-index: 99999;
border-radius: 2px; text-align: center; font-size: .8rem; letter-spacing: .1rem;}
.fade-enter-active{transition: opacity .1s;}
.fade-leave-active {transition: opacity .3s;}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {opacity: 0;}
</style>
step 2: create a plugin (ToastPlugin.js)
import Toast from "./Toast.vue";
import {createApp} from 'vue'
const install = (app) => {
// dom container for mount the Toast.vue
let container;
// like 'app' just for Toast.vue
let toastApp;
// 'props' that Toast.vue required.
const baseProps = {
// define a function to close(unmount) the toast used for
// case 1: in Toast.vue "click toast appeared and close it"
// case 2: call 'this.$closeToast()' to close the toast in anywhere outside Toast.vue
close:()=> {
if (toastApp)
toastApp.unmount(container);
container = document.querySelector('#ToastPlug');
if(container)
document.body.removeChild(container);
}
};
// show Toast
const toast = (msg)=>{
if(typeof msg === 'string')
msg = {msg};
const props = {...baseProps,...msg}
console.log('props:',JSON.stringify(props));
// assume the toast(previous) was not closed, and try to close it.
props.close();
// create a dom container and mount th Toast.vue
container = document.createElement('div');
container.setAttribute('id','ToastPlug');
document.body.appendChild(container);
toastApp = createApp(Toast, props);
toastApp.mount(container);
}
// set 'toast()' and 'close()' globally
app.config.globalProperties.$toast = toast;
app.config.globalProperties.$closeToast = baseProps.close;
}
export default install;
step 3: usage
in main.js
import ToastPlugin from 'xxx/ToastPlugin'
import { createApp } from 'vue'
const app = createApp({})
app.use(ToastPlugin)
// then the toast can be used in anywhere like this:
this.$toast('some words')
this.$toast({msg:'some words',timeout:3000})
Vue 3 provides an API for attaching global properties:
import { createApp } from 'vue'
const app = createApp({})
app.config.globalProperties.$toast = () => { /*...*/ }
We use Vue.js and OpenLayers (4.6.5) in our web project. We have a lot of features on the map and some of them are polygons. When I select some particular polygon, its style turns to another color, which means it's highlighted (selected). Of course, I can get the coordinates of selected polygon. But, how can I get the coordinates of point inside that polygon where I clicked?
The code look as following:
markObject (mark) {
if (!mark) {
this.map.un('select', this.onMarkObject)
if (this.markSelection) {
this.markSelection.getFeatures().remove(this.lastSelectedFeature)
this.map.removeInteraction(this.markSelection)
}
return
}
if (!this.markSelection) {
this.markSelection = new Select({
condition: condition.click,
layers: [this.vectorLayer]
})
this.markSelection.on('select', this.onMarkObject)
}
this.map.addInteraction(this.markSelection)
},
onMarkObject (event) {
if (event.selected && event.selected.length > 0) {
const coordinates = event.selected[0].getGeometry().getCoordinates()
}
}
Actually, I've found the solution:
onMarkObject (event) {
const clickCoordinates = event.mapBrowserEvent.coordinate
...
}
Thank you anyway.
What you need is to capture the click event on the map, and then transform pixel to map coordinates, take a look at this example I made for you,
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.1.1/css/ol.css" type="text/css">
<style>
.map {
height: 400px;
width: 100%;
}
#a { display: none; }
</style>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.1.1/build/ol.js"></script>
<title>Click Pixel Coord</title>
</head>
<body>
<h2>Click on Map to get pixel and coord values</h2>
<p id="p"></p>
<div id="map" class="map"></div>
<script type="text/javascript">
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
map.on('click', function(evt) {
const coord = map.getCoordinateFromPixel(evt.pixel);
document.getElementById('p').innerHTML =
`Pixel:${evt.pixel[0]} ${evt.pixel[0]}` + '<br>' +
`Coord:${coord[0].toFixed(2)} ${coord[1].toFixed(2)}`;
});
</script>
</body>
</html>