Custom directive to wrap one component into another - vue.js

I create a component whose purpose is to display an arbitrary component within itself, with a changeable padding property. (The example is conditional for simplicity).
<script setup lang="ts">
defineProps<{
innerComponent: any;
settings: { padding: number; backgroundColor: string };
}>();
</script>
<template>
<div
:style="`
padding: ${settings.padding}px;
background-color: ${settings.backgroundColor}`"
>
<component :is="innerComponent" />
</div>
</template>
Let's create a simple component for the example.
<template>
<p style="background-color: beige">it's just a test.</p>
</template>
That's how we use it, and it works great.
<script setup lang="ts">
import ExternalComponent from "./components/ExternalComponent.vue";
import InnerComponent from "./components/InnerComponent.vue";
</script>
<template>
<ExternalComponent
:inner-component="InnerComponent"
:settings="{ padding: 200, backgroundColor: 'yellow' }"
/>
</template>
I wish it all looked even more aesthetically pleasing. For example, like this.
<InnerComponent v-inner="{ padding: 200, backgroundColor: 'yellow' }" />
Let's try using custom directives.
import { createApp, h } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("inner", (el, binding, vnode) => {
//Here is supposedly expected to be something like
el.outerHTML = h("ExternalComponent", {
innerComponent: vnode,
settings: binding.value,
});
//or
vnode = h("ExternalComponent", {
innerComponent: vnode,
settings: binding.value,
});
//But, of course, something completely different :((
});
app.mount("#app");
Unfortunately, my knowledge is not enough to solve this problem. I would be glad to get any advice on what direction to dig.

Related

d3 graph rendering with vue

Because of non compatible dependencies, I have to downgrade from vue3 to vue2.
I have created a force directed graph with D3 library. Everything worked find with vue3 using composition api. I am not familiar with vue 2 and adapting my graph to vue2 has not been working out for me.
In vue3 it was very straitforward and ref() made it pretty easy to accomplish.
As for vue2, I have tried making good use of lifecycle hooks such as computed and watch
Any help is more than welcome
Here is a minimalistic representation of my working component in vue3. This component creates the graph in a svg and then renders it in the template.
<template>
<div class="col" style="position: absolute; width:100%; height:100%" >
<div class="main-map-container" style="overflow: hidden; width: 100%;">
<div ref="graph" class="canvas">
</div>
</div>
</div>
</template>
<script >
import {onMounted, onBeforeMount, ref} from 'vue'
export default {
setup(){
const graph = ref()
const links = [{src:"Amazon",target:"Aurora"},{src:"Amazon",target:"Aurora"},{src:"Amazon",target:"Zoox"},{src:"Amazon",target:"Rivian"}]
const nodes = [{id:"Amazon"},{id:"Aurora"},{id:"Zoox"},{id:"Rivian"}]
onBeforeMount( async ()=>{
const svgobj = ForceGraph(nodes, links)
graph.value.appendChild(svgobj);
})
function ForceGraph(
nodes,
links
){
// The code for the graph has been removed since it is much too long
return Object.assign( svg.node() );
}
return { graph }
}
}
</script>
<style></style>
This is the vue2 component that i have emptied for this post
<template>
<div class="col" style="position: absolute; width:100%; height:100%" >
<div class="main-map-container" style="overflow: hidden; width: 100%;">
<div ref="graph" class="canvas">
</div>
</div>
</div>
</template>
<script>
export default {
setup(){
},
methods: {
},
watch: {
},
props: {
},
computed: {
},
created() {
},
mounted() {
},
updated(){
},
data() {
return {
}}
}
</script>
<style>
</style>
You can use Vue3 composition API in vue 2. Install the composition api and then just keep your code the same, with the setup method exactly as it was.
The setup method, lifecycle hooks, and all the reactivity (refs and reactive objects) are made available to you, with very few incompatibilities.
We use d3 with Vue2 in this fashion all the time. 100% compatible.
https://github.com/vuejs/composition-api

Can't get the vue router to work properly

I have struggling to get the vue router to work properly, despite the fact that I have tried multiples different ways and read through the documentation, there seems to be something that I don't understand
I know app component is a bit messy, with router view covering router link and there is the component. its because I have tried it all, included the router-view first then it showed everything from my firstUser component, then tried the link, it worked, but it did not go to that page seperately,
I need to make it so that when clicked it should show the firstUser page only, Please help, much appreciated
//This is my app component
<template>
<div id="app">
<h1>Welcome</h1>
<router-view>
<router-link to="firstUser"> <first-user/> Register </router-link>
</router-view>
<div>
<h2>Returning user</h2>
<returning-user />
</div>
<div>
<h2>Registered User</h2>
<registered-user />
</div>
</div>
</template>
<script>
import firstUser from "#/components/firstUser.vue";
import returningUser from "#/components/returningUser.vue";
import registeredUser from "#/components/registeredUser.vue";
export default {
components: { returningUser, registeredUser, firstUser },
data() {
return {};
},
};
</script>
<style>
#app {
padding: 20px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
input:focus {
outline: none;
}
</style>
//this is my fisrtUser component
<template>
<div>
<form class="form" #submit.prevent="registerUser">
<h4 class="">Register</h4>
<my-input
v-model="this.firstName"
type="text"
class="input"
placeholder="First Name"
/>
<my-input
v-model="this.lastName"
type="text"
class="input"
placeholder="Last Name"
/>
<myButton #click="registerUser"> Create </myButton>
</form>
</div>
</template>
<script>
import myInput from "./UI/myInput.vue";
import myButton from "./UI/myButton.vue";
export default {
name: "firstUser",
components: { myInput, myButton },
data() {
return {
user: [
{
firstName: "",
lastName: "",
},
],
users: [],
};
},
methods: {
registerUser() {
const newUser = {
firstName: this.firstName,
lastName: this.lastName,
};
this.users.push(newUser);
console.log(newUser);
},
},
};
</script>
<style>
</style>
///then this is the index.js from router
import { createRouter, createWebHistory } from 'vue-router';
import firstUser from '#/views/firstUser';
const routes = [{
path: '/firstUser',
name: 'firstUser',
component: firstUser
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
and
It is not entirely clear what the firstUsers are. I will assume what you did is intentional and there are two firstUsers (one view and one component).
Specify the to prop of the router-link component as you would the href attribute of a link/anchor <a> element. That is, the route needs to begin with a slash.
<router-link to="/firstUser">
Alternatively, you might want to reduce the coupling by declaring the link by route name instead. This way, you can change the route to whatever you want without having to update your link. This can be done by using v-bind to tell Vue to parse the prop contents, and by using different syntax.
<router-link :to="{ name: 'firstUser' }">
See the Vue Router documentation.

Is it possible to create a v-class Vue directive that can be overridden by parent just like regular :class prop?

Please see this minimum example
I have a Child.vue and Parent.vue which look like this
<!-- Child.vue -->
<template>
<div :class="'bg-gray text-white'">I'm Child</div>
</template>
<template>
<Child class="p-4" />
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
};
</script>
<style>
.p-4 {
padding: 16px;
}
.bg-gray {
background: gray;
}
.text-white {
color: white;
}
</style>
The final class name for Child.vue wrapper element will be bg-gray text-white p-4, class names are all merged.
However, I would like to create a v-class directive and make my API looks like this
<!-- Child.vue -->
<template>
<div v-class="'bg-gray text-white'">I'm Child</div>
</template>
<script>
export default {
directives: {
class: {
inserted(el, binding) {
const css = String.raw; // Not doing anything right now.
el.className = css`
${binding.value}
`;
},
},
},
};
</script>
And Parent.vue stays the same.
Now the final merged class name is bg-gray text-white, p-4 is missing!
How can I preserve p-4 just like I'm using regular :class prop?
My use case is I want to use the tagged template literal function in my Vue template.
However, Vue doesn't support template literal function in the template, the compiler will yell!
For example
<div :class="css`text`"></div>
Try out to push the value to the class list :
el.classList.add( css`
${binding.value}
`);

OpenLayers with VueJS - empty div in place of map

I'm trying to implement a simple world map in a Vue application. I have created a MapContainer component that is then imported into my main app. Below is the code for MapContainer.vue:
<template>
<div
ref="map-root"
style="width: 100%, height: 100%">
</div>
</template>
<script>
import View from 'ol/View';
import Map from 'ol/Map';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import 'ol/ol.css';
export default {
name: 'MapContainer',
components: {},
props: {},
mounted() {
new Map({
target: this.$refs['map-root'],
layers: [
new TileLayer({
source: new OSM()
})
],
view: new View({
zoom: 0,
center: [0, 0],
constrainResolution: true
})
});
}
}
</script>
<style>
</style>
I am registering the MapContainer component and then simply placing it inside a div in the parent component. When I import this component and try to use it, I get an empty div in place of the map. Does anyone know what I'm doing wrong?
Here is the parent component's code:
<template>
<div>
<map-container></map-container>
</div>
</template>
<script>
import MapContainer from '../mapping/MapContainter.vue';
export default {
components: {
'map-container': MapContainer
}
}
</script>
I fixed this by adding the following class to the div with the ref:
.map {
width: 100% !important;
height: 600px !important;
}
(The !important's might not be strictly necessary).

Changing styles only when a Vue component is rendered

I'm currently using vue-router to send us to an about page like so:
# #/views/About
<template>
<p>About Vue...</p>
</template>
<script>
[...]
</script>
<style>
</style>
The way routes are rendered are like so:
# #/App.vue
<template>
<Header />
<router-view>
</template>
<script>
import ...
</script>
<style>
header {
background: black;
}
</style>
Question:
How would I go about changing the header background color ONLY when I am on the About.vue (if i go to other routes like / or /contact header should stay the same)?
Thank you in advance for the help
Use a watcher or a computed property to watch your this.$route then conditionally apply a class or style based on if you're currently on the /about route. For example ..
# #/App.vue
<template>
<Header :class="[altBackground ? 'header-red' : 'header-black']" />
<router-view>
</template>
<script>
import ...
computed: {
altBackground() {
const path = this.$route.path.split('/')
return path[path.length-1].toLowerCase() === 'about'
}
}
</script>
<style>
.header-black {
color: black;
}
.header-red {
color: red;
}
</style>