Using reusable components in Vue 2 in comination with vue router - components

I seem to do something wrong when I'm trying to target child components in nested router-views with click events.
Current situation:
I have a component one and component two. Both have a child component called dialog.
Component one and two are being loaded through a router-view in parent component dashboard. Each view has a button to show their child component "Modal".
The button seems to work fine on the view that gets loaded on pageload. But as soon as I switch routes the showModal function does not know the dialog element from which view to target.
I thought the components would be destroyed and rebuilt upon switching routes but apparently not.
Here is my code, I hope someone is able to help:
App
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
/**
* First we will load all of this project's JavaScript dependencies which
* include Vue and Vue Resource. This gives a great starting point for
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap')
/**
* Next, we will create a fresh Vue application instance and attach it to
* the body of the page. From here, you may begin adding components to
* the application, or feel free to tweak this setup for your needs.
*/
Vue.component('vuetest', require('./components/vuetest.vue'))
const Dashboard = require('./components/dashboard.vue')
const FirstRoute = require('./components/firstroute.vue')
const Second = require('./components/secondroute.vue')
const routes = [
{
path: '/dashboard',
component: Dashboard,
children: [
{
path: 'firstview',
name: 'firstview',
canReuse: false,
component: FirstRoute
},
{
path: 'secondview',
name: 'secondview',
canReuse: false,
component: Second
}
]
}
]
const router = new VueRouter({
routes // short for routes: routes
})
window.EventHub = new Vue()
const app = new Vue({
el: '#app',
router
});
Vuetest
<template>
<div>
<h1>Vue Test</h1>
<router-view></router-view>
</div>
</template>
<script>
export default {
created() {
},
mounted() {
console.log('Component ready.')
}
}
</script>
Dashboard Route
<template>
<div>
<h1>Dashboard</h1>
<navigation></navigation>
<router-view></router-view>
</div>
</template>
<script>
Vue.component('navigation', require('./navigation.vue'))
</script>
Navigation
<template>
<div>
<router-link :to="{ name: 'firstview' }">first</router-link>
<router-link :to="{ name: 'secondview' }">second</router-link>
</div>
</template>
First Route
<template>
<div class="firstroute">
<h1>First Route</h1>
<button class="showmodal" v-on:click="showModal">Showmodal</button>
<modal></modal>
</div>
</template>
<script>
export default {
methods: {
showModal: function () {
EventHub.$emit('showModal')
}
}
}
Vue.component('modal', require('./modal.vue'));
</script>
Second Route
<template>
<div class="secondroute">
<h1>Second Route</h1>
<button class="showmodal" v-on:click="showModal">Showmodal</button>
<modal></modal>
</div>
</template>
<script>
export default {
methods: {
showModal: function () {
EventHub.$emit('showModal')
}
}
}
Vue.component('modal', require('./modal.vue'));
</script>
Modal
<template>
<div class="dialog hidden">
Dialog
</div>
</template>
<style>
.hidden {
display: none;
}
</style>
<script>
export default{
created() {
EventHub.$on('showModal', this.showModal);
},
methods: {
showModal: function() {
document.querySelector('.dialog').classList.toggle('hidden');
}
}
}
</script>
I really appreciate any help.

tiny recomendations
':class' directive instead of native code:
document.querySelector('.dialog').classList.toggle('hidden');
components:
import Modal from './modal'
export default {
...
components:
Modal
}
...
}
instead of
Vue.component('modal', require('./modal.vue'));
.. also Vuex is a good point for this case
additional:
https://github.com/vuejs/vue-devtools
https://jsfiddle.net/uLaj738k/2/

As it turns out the problem was the moment I called the querySelector method.
Assigning the .dialog element to a const in mounted() solved my problem.

Related

Vue test utils cannot find child component

I am running a Vue 3.0 with Quasar library. In the application there is a component that has a child component in it. With vue test utils v 2.2.6, I cannot seem to locate the child components in order to generate events. Why?
Simplified Parent Component
<template>
<q-dialog square persistent transition-show="scale" transition-hide="scale" >
<q-card-section class="q-px-xs q-pt-none">
<ChildComponent/>
</q-card-section>
</q-dialog>
</template>
<script setup>
//imports, props, etc
</script>
<style> ... </style>
Simplified Child Component
<template>
<q-card square bordered id="new-recipient-root-element" class="new-recipient">
<q-form>...</q-form>
</q-card>
</template>
<script setup>
//imports, props, etc
</script>
<style> ... </style>
Using Jest / vue test utils like so:
wrapper = mount(ParentComponent, {
props: { ...},
global: {
plugins: [i18n],
provide: {
store: ...
},
mocks: { ... }
}
});
wrapper.vm.show();
await nextTick();
domWrapper = new DOMWrapper(document.body);
...
await domWrapper.findComponent(ChildComponent).vm.$emit('myEvent');
domWrapper.findComponent(ChildComponent) always returns nothing. If I try domWrapper.findAllComponents(), it returns an empty array.
Also noticed that I can locate all the elements, but if I click on a button, no event seems to be generated since the handlers are not called and wrapper.emitted() contains nothing.
What am I missing?

Angular RouterLinks broken URL navigation unaffected

My angular router is failing to load the latest component after a router change is made within the application. If I call the URL manually it will load the correct content however any use of routerLink or a call to router.navigate has no affect on the router-outlet content.
I have tried binding to router events and recalling the getContent function when there is a change and this fixes the issue when calling programmatically.
The project is pretty bare but the router:
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import { ContentpaneComponent } from './contentpane/contentpane.component';
const routes: Routes = [
{ path: '', component: ContentpaneComponent },
{ path: 'post/:app', component: ContentpaneComponent }
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
And the container in app.component.html
<div class="above">
<app-sidebar></app-sidebar>
<div class="contentpane">
<router-outlet></router-outlet>
</div>
</div>
<app-terminal></app-terminal>
If there is any other code segments that would benefit please request in comments.
Here is the code containing the routerLink directives:
<div class="main">
<div class="logo">
O
</div>
<ul class="navbar">
<li *ngFor="let nav of navs">
<a routerLink="/{{nav.href}}" class="navitem">{{nav.title}}</a>
</li>
</ul>
</div>
EDIT: I am also getting a websocket error in the console:
WebSocket connection to 'ws://localhost:4200/sockjs-node/748/g0a4bxsw/websocket' failed: WebSocket is closed before the connection is established.
Not sure if it is a related issue.
So I managed to find the solution. The problem arose because routerLink does not force the reloading of a component when the URL changes, so ngOnInit does not get called. The fix I implemented was to subscribe my content update function to the route.params and queryParams events:
this.route.queryParams.subscribe(queryParams => {this.getContent()});
this.route.params.subscribe(queryParams => {this.getContent()});
This causes these functions to be called whenever the route updates.

Laravel jetstream inertia persistent layout

In a fresh laravel installation i'm trying to make layout persistent following the inertia doc https://inertiajs.com/pages
app.js
require('./bootstrap');
// Import modules...
import { createApp, h } from 'vue';
import { App as InertiaApp, plugin as InertiaPlugin } from '#inertiajs/inertia-vue3';
import { InertiaProgress } from '#inertiajs/progress';
import AppLayout from '#/Layouts/AppLayout';
const el = document.getElementById('app');
createApp({
render: () =>
h(InertiaApp, {
initialPage: JSON.parse(el.dataset.page),
resolveComponent: name => import(`./Pages/${name}`)
.then(({ default: page }) => {
if (page.layout === undefined) {
page.layout = AppLayout
}
return page
}),
}),
})
.mixin({ methods: { route } })
.use(InertiaPlugin)
.mount(el);
InertiaProgress.init({ color: '#4B5563' });
Dashboard.vue (here i replace the default app-layout wrapper by div)
<template>
<div>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Dashboard
</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<welcome />
</div>
</div>
</div>
</div>
</template>
<script>
import Welcome from '#/Jetstream/Welcome'
export default {
components: {
Welcome,
},
}
</script>
While compiling i get this error :
Error: Codegen node is missing for element/if/for node. Apply
appropriate transforms first.
I can't figure out what that means. Is there a reason why the default laravel app with jetstream and inertia doesn't use persistent layout?
If the header slot is in AppLayout aka the persistent one, you cannot use this way (because there is no slot yet? I don't know but I know persistent layouts do mount after child components, this can be the culprit). As a solution, create another layout, ie PageLayout, with named slots and use that layout to build your dashboard and other pages:
AppLayout.vue
<template>
<div>Persistent stuff...</div>
<slot />
<div>Persistent stuff...</div>
</template>
PageLayout.vue
<template>
<slot name="header" />
<slot name="content" />
<div>PageLayout stuff...</div>
</template>
Dashboard.vue
<template>
<PageLayout>
<template #header>
<h1>Dashboard</h1>
</template>
<template #content>
<p>Welcome user!</p>
</template>
</PageLayout>
</template>
<script>
import AppLayout from 'AppLayout'
import PageLayout from 'PageLayout'
export default {
layout: AppLayout, // Persistent layout
components: {
PageLayout // Regular layout
}
</script>
There is an ongoing discussion here:
https://github.com/inertiajs/inertia/issues/171
You need the app-layout as you need to extend from it <template #header>

How to send data from parent to child component in Vue.js

I am new to vue.js and currently I am building an app for learning purposes.
What I want to do:
I have a parent component which has a bunch of buttons with different id's.
The child component will wait for those id's to be sent by the parent and it will decide what to display based on the id. Thats all.
I wont post the full code because it's too large but I have tried a bunch of stuff like props and state but honestly it is so confusing.
I come from React background and I am still confused.
Parent component
<template>
<button id="btn1">Show banana</button>
<button id="btn2">Show orange</button>
</template>
<script>
export default {
name: 'Parent',
data: function {
//something
},
props: {
// ?????
}
};
</script>
**Child component**
<template>
<p v-html="something.text">
</template>
<script>
export default {
name: 'Child',
data: function() {
something: ''
if(id from parent === id I want) {
something = object.withpropIneed
}
},
};
</script>
You need to map the data from parent and pass it to child, thats it!
In example i make passing a html string and binding that html received through 'fromParentHtml' prop mapped on child, so inside child component 'this.fromParentHtml' pass to exists because it is defined in props and every time you click in parent button executes the 'show' function and change the value from passed prop to child through parent 'html' data .. =)
<template>
<div>
Current html sent to child '{{html}}'
<br>
<button #click="show('banana')">Banana</button>
<button #click="show('orange')">Orange</button>
<button #click="show('apple')">Apple</button>
<!-- Create child component -->
<child-component :fromParentHtml="html"></child-component>
</div>
</template>
<script>
export default {
name: "test3",
components: {
'child-component': {
template: "<div>Child component... <br> <span v-html='fromParentHtml'></span> </div>",
//Child component map a prop to receive the sent html from parent through the attribute :fromParentHtml ...
props: {
fromParentHtml: {
required: true,
type: String
}
}
}
},
data(){
return {
html: ''
}
},
methods: {
show(fruit){
this.html = '<span>The fruit is ' + fruit + ' !</span>';
}
}
}
</script>
<style scoped>
</style>
If helped you please mark as correct answer! Hope it helps.
Edit 1:
Assuming you have webpack to work with single file components, to import another component just do:
<template>
<div>
<my-child-component></my-child-component>
</div>
</template>
<script>
//Import some component from a .vue file
import ChildComponent from "./ChildComponent.vue";
export default {
components: {
//And pass it to your component components data, identified by 'my-child-component' in the template tag, just it.
'my-child-component': ChildComponent
},
data(){
},
methods: {
}
}
</script>
Just for the sake of it, I think you were looking for this:
<template>
<button id="btn1" #click = "id = 1">Show banana</button>
<button id="btn2" #click = "id = 2">Show orange</button>
<child-component :childid = "id"></child-component>
</template>
<script>
import childComponent from 'childComponent'
export default {
name: 'Parent',
data () {
return {
id: 0
}
},
components: {
childComponent
}
};
</script>
**Child component**
<template>
<p v-html="something.text">
</template>
<script>
export default {
name: 'Child',
props: {
childid: String
},
data: function() {
something: ''
if(this.childid === whatever) {
something = object.withpropIneed
}
},
};
</script>
Solved my problem by taking a different approach.
I have implemented state and my component behaves exactly as I wanted to.
I found this link to be helpful for me and solved my problem.
Thank you.

rendering different templates in angular2

I have just started using angular2 and I am facing some situation to organize my logic. I am actually refactoring backend theme that consists of the two main components i.e login view that is supposed to appear on page load and main dashboard that will appear on successful login. I have no problem with the main dashboard template because i have refactored all the code its working fine. However the main problem is with the login module because dashboard consist of the sidebar, header and maincontent area. My real problem is that how do I exclude sidebar, header on loading the login page which will going to be the startpoint of my app. To be more precise, can i use the layout for the login module which is independent to the dashboard module? Here is my current code for the dashboard.
I would really appreciate if anyone could help me structure this app in proper way.
P.S I am using node as a backend
index.html
<html>
<head>
<title>Angular 2 QuickStart</title>
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<!-- 2. Configure SystemJS -->
<base href="/">
<script src="systemjs.config.js"></script>
<script>
System.import('app/main').catch(function(err){ console.error(err); });
</script>
</head>
<!-- 3. Display the application -->
<body>
<parent>
Loading...
</parent>
<!-- START SCRIPTS -->
<!-- START PLUGINS -->
</body>
</html>
app.component.ts
import { Component, NgZone } from '#angular/core';
import { HeaderComponent } from './header/header.component';
import { SidebarComponent } from './sidebar/sidebar.component';
import { AuthComponent } from './auth/auth.component';
import { ListComponent } from './blogs/list.component';
import {RouteConfig, ROUTER_DIRECTIVES, Router, AuxRoute} from '#angular/router-deprecated';
#Component({
selector: 'parent',
templateUrl:'app/main.html',
directives: [ROUTER_DIRECTIVES, HeaderComponent, SidebarComponent, AuthComponent]
})
#RouteConfig([
{ path: '/list', name: 'BlogList', component: ListComponent}
])
export class AppComponent {
constructor(private _router: Router , private _zone:NgZone){}
}
main.ts
import { bootstrap } from '#angular/platform-browser-dynamic';
import { AppComponent } from './app.component';
import {ROUTER_PROVIDERS} from '#angular/router-deprecated';
bootstrap(AppComponent,[ROUTER_PROVIDERS]);
import {NgZone, enableProdMode} from '#angular/core'
main.html
<div class="page-container">
<div class="page-sidebar">
<mysidebar>
</mysidebar>
</div>
<div class="page-content">
<myheader>
</myheader>
<!-- PAGE CONTENT WRAPPER -->
<div class="page-content-wrap">
<router-outlet></router-outlet>
</div>
</div>
</div>
You can accomplish this by using a Service
export class AppComponentService {
public showSidebar : boolean = true;
public showHeader : boolean = true;
}
If you add this service during bootstrap:
bootstrap(AppComponent,[ROUTER_PROVIDERS, AppComponentService ]);
You can inject this into your AppComponent:
export class AppComponent {
constructor(
private _router: Router,
private _zone:NgZone,
private _appService: AppComponentService
){}
}
And change your template to:
<div class="page-container">
<div class="page-sidebar">
<mysidebar *ngIf="_appService.showSidebar">
</mysidebar>
</div>
<div class="page-content">
<myheader *ngIf="_appService.showHeader">
</myheader>
<!-- PAGE CONTENT WRAPPER -->
<div class="page-content-wrap">
<router-outlet></router-outlet>
</div>
</div>
</div>
Within your LoginComponent you can inject the same singleton service, and play with it on routerOnActivate and routerOnDeactivate:
export class LoginComponent {
constructor(private _appService: AppComponentService){}
routerOnActivate() : void {
this._appService.showSidebar = false;
this._appService.showHeader = false;
}
routerOnDeactivate() : void {
this._appService.showSidebar = true;
this._appService.showHeader = true;
}
}

Resources