Ionic/Vue - Show permanent Toolbar above InAppBrowser - vue.js

I am seriously struggling to figure out how to layout the InAppBrowser in a small section of the screen.
I want to have a permament IonicToolbar above the InAppBrowser and eventually add my own navigation buttons.
I'm very new to Iconic/Vue and whatever I've tried so far just seems to show the browser on top of the IonToolbar and IonPage
BaseLayout
<template>
<ion-page>
<ion-header>
<ion-header>
<ion-toolbar>
<ion-title>Title</ion-title>
</ion-toolbar>
</ion-header>
</ion-header>
<ion-content>
<slot />
</ion-content>
</ion-page>
</template>
<script>
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
} from "#ionic/vue";
export default {
props: ["pageTitle", "defaultBackLink"],
components: {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
},
};
</script>
BrowserLayout
<template>
<in-app-browser></in-app-browser>
</template>
<script>
import { InAppBrowser } from "#ionic-native/in-app-browser";
function beforeloadCallBack(params, callback) {
console.log(">>> beforeload: " + params.url.toString());
if (params.url.includes("https://www.google.com/")) {
console.log(">>> beforeload: allowed");
callback(params.url);
} else {
console.log(">>> beforeload: restricted");
alert("The URL is restricted!");
}
}
export default {
components: {
InAppBrowser,
},
setup() {
const options = {
location: "no",
zoom: "no",
hardwareback: "yes",
mediaPlaybackRequiresUserAction: "no",
hidenavigationbuttons: "yes",
hideurlbar: "yes",
toolbar: "no",
fullscreen: "no",
beforeload: "yes",
};
let browser = InAppBrowser.create(
"https://www.google.com",
"_blank",
options
);
browser.on("loadstop").subscribe((event) => {
console.log(">>> onLoadStop:" + event.url.toString());
});
browser.on("loadstart").subscribe((event) => {
console.log(">>> onLoadStart:" + event.url.toString());
});
browser.on("beforeload").subscribe((params) =>
beforeloadCallBack(params, () => {
return params.url;
})
);
},
};
</script>
HomePage
<template>
<base-layout>
<browser-layout></browser-layout>
</base-layout>
</template>
<script>
import BrowserLayout from "#/components/browser/BrowserLayout.vue";
import BaseLayout from "#/components/base/BaseLayout.vue";
export default {
components: {
BaseLayout,
BrowserLayout,
},
};
</script>

Related

Attribute patch in pinia store is working from one component, but not from another (Nuxt.js)

I've those two components using Nuxt 3.
The setActive method in component 1 changes the state of activeColor, but the cancelEdit method in component 2 does not.
Any idea why this is the case?
Component 1
Here the setActive method changes activeColor:
<template>
<div class="f-button" #click="addColor">+ Add Color</div>
{{ activeColor }}
<div class="f-colors">
<Color v-for="color in colors" :color="color" :edit="activeColor === color" #click="setActive(color)"/>
</div>
</template>
<script>
import {useProjectStore} from "~/stores/projects";
import {storeToRefs} from "pinia";
import Color from "~/components/Color.vue";
export default defineComponent({
name: "ColorManagement",
components: {Color},
setup() {
const projectStore = useProjectStore()
const { getColors, getActiveColor } = storeToRefs(projectStore);
return {
projectStore,
colors: getColors,
activeColor: getActiveColor
};
},
methods: {
addColor() {
...
},
setActive(color) {
this.projectStore.$patch({ activeColor: color })
}
}
});
</script>
Component 2
Here the cancelEdit method doesn't change activeColor:
<div class="f-color">
<div class="f-color__actions" v-if="edit">
<div class="f-color__action" #click="cancelEdit">
<Cancel /><span>Cancel</span>
</div>
</div>
</div>
</template>
<script>
import Cancel from "~/components/icons/Cancel.vue";
import {useProjectStore} from "~/stores/projects";
import {storeToRefs} from "pinia";
export default defineComponent({
name: "Color",
components: {Cancel},
props: ["color","edit"],
setup() {
const projectStore = useProjectStore()
const { activeColor } = storeToRefs(projectStore);
return {
projectStore,
activeColor
};
},
methods: {
cancelEdit() {
this.projectStore.$patch({ activeColor: false })
}
}
});
</script>
nuxt.config.ts
export default defineNuxtConfig({
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '#use "#/assets/styles/_styles.scss" as *;'
}
}
}
},
modules: ['#pinia/nuxt']
})
Store
import { defineStore } from "pinia";
export const useProjectStore = defineStore({
id: 'project',
state: () => {
return {
project: {
colors: [{ color: false, name: '' }]
},
activeColor: null
}
},
getters: {
getColors(state){
return state.project.colors || [];
},
getActiveColor(state){
return state.activeColor;
}
}
});
Ok, if I got this correctly, the deal is this:
Your so called Component 2 is the <Color ... component being used in Component 1, right?
When you trigger cancelEdit inside Component 2 (aka Color) you are also triggering the logic from setActive due to this <Color ...#click="setActive(color)"...so your activeColor is set to false (from the cancelEdit method) but right after it is set to active again, got it?
To fix this (if you don't want to change your HTML structure) you can use events stopPropagation method inside the cancelEdit:
cancelEdit(e) {
e.stopPropagation()
this.projectStore.$patch({ activeColor: false })
}
Event.stopPropagation() reference

Vue-i18n not translating inside component script tags

Building a language switcher, all works fine but when I use the $t() inside the data object it will not be dynamic when I switch between a language.
Component.vue
<template>
// loop menu here
<div v-for="item in menu">
{{ item.label }}
</div>
</template>
<script>
const mainMenu = [
{
label: $t('dashboard'),
},
{
label: $t('users'),
},
{
label: $t('settings'),
},
}
export default {
data () {
return {
menu = MainMenu
}
}
}
</script>
i18n.js
// https://vue-i18n.intlify.dev/
import { createI18n } from 'vue-i18n'
export function loadLocalMessages () {
const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages;
}
const i18n = createI18n({
locale: 'en',// .env not working
fallbackLocale: 'en',// .env not working
messages: loadLocalMessages(),
});
export default i18n
<template>
<div v-for="item in menu">
{{ item.label }}
</div>
</template>
<script>
export default {
computed: {
menu() {
return [{
label: this.$t('dashboard'),
}, {
label: this.$t('users'),
}, {
label: this.$t('settings'),
}]
}
}
}
</script>
data is only ever called once when creating the component, and it's not intended to be reactive.
To make a property reactive on $t(), it should be computed:
export default {
computed: {
hello() {
return this.$t('hello')
}
}
}
demo

Can't close the modal in Ionic-vue 5.5.2

in this version of #ionic/vue#0.5.5.2 I can't use a component and assign a reference (< Login ref="modal"/>) to it to close it in the Modal component (Login.vue), so I don't know how to close it from Login.vue. I leave my code:
Home.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>
Title
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="auth-form">
<ion-grid>
<ion-row>
<ion-col align-self-center>
<ion-button #click="openModal" expand="block" color="primary">Registrarme</ion-button>
<span class="divider line one-line">o</span>
<span class="already">¿Tienes una cuenta?</span>
<ion-button #click="openModal" expand="block" color="danger">Iniciar sesión</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
</ion-page>
</template>
<script>
import {
IonContent,
modalController,
IonTitle,
IonToolbar,
IonHeader,
IonButton,
IonCol,
IonRow,
IonGrid,
IonPage
} from '#ionic/vue';
import Login from '../views/Login.vue';
export default {
name: 'inicio',
components: {
IonContent,
IonTitle,
IonToolbar,
IonHeader,
IonButton,
IonCol,
IonRow,
IonGrid,
IonPage
},
data() {
return {
modal: '',
isOpen: false,
}
},
methods: {
async createModal() {
this.modal = await modalController.create({
component: Login,
componentProps: {
title: 'Iniciar sesión'
},
})
},
async openModal() {
await this.createModal()
this.isOpen = true
this.modal.present()
},
closeModal() {
this.isOpen = false
this.modal.dismiss().then(() => {
this.modal = null;
});
},
},
}
</script>
And my Login.vue:
<template>
<ion-page>
<ion-header translucent>
<ion-toolbar>
<ion-title>{{ title }}</ion-title>
<ion-buttons slot="end">
<ion-button #click="cerrarmodal">Cerrar</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-label>Email</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Contraseña</ion-label>
<ion-input type="password"></ion-input>
</ion-item>
</ion-list>
<ion-button color="primary" expand="block">Ingresar</ion-button>
</ion-content>
</ion-page>
</template>
<script>
import {
IonButtons,
IonContent,
IonButton,
IonToolbar,
IonHeader,
IonTitle,
IonList,
IonLabel,
IonInput,
IonItem,
IonPage
} from '#ionic/vue';
import {
defineComponent
} from 'vue';
export default defineComponent({
name: 'login',
props: {
title: {
type: String,
default: 'Super Modal'
},
closeMe: {
type: Function,
default: () => {
''
}
},
},
data() {
return {
content: 'Content',
}
},
methods: {
cerrarmodal() {
this.$emit('close', {
foo: 'bar'
})
// Not working
this.$parent.closeModal()
},
},
components: {
IonButton,
IonButtons,
IonToolbar,
IonList,
IonInput,
IonLabel,
IonItem,
IonPage,
IonHeader,
IonTitle,
IonContent
},
});
</script>
I've tried $ emit and $ parent.closeModal () but no luck, thanks for your help in advance.
I confirm that the best and simple way to solve this is calling dismiss() function of modalController.
First we must import modalController in our modal component:
import { modalController } from "#ionic/vue";
Next we can close modal like this:
async function close() {
await modalController.dismiss();
}
Important: I use await because dismiss return a promise so it's important use it.
I resolved it by passing a prop "close" with the function closeModal and defining the prop "close" in the modal component and then calling it with a this.close
this.modal = await modalController.create({
component: Login,
componentProps: {
title: 'Iniciar sesión',
close: () => this.closeModal()
},
})
In modal component:
props: {
title: {
type: String,
default: 'Super Modal'
},
close: { type: Function }
},
cerrarmodal() {
this.$emit('close', {
foo: 'bar'
})
this.close()
},
I think you can call
modalController.dismiss()
to close any open dialog... also the emit should have worked, didn't see that code
<!-- ./modal.vue -->
<template>
<ion-header>
<ion-toolbar>
<ion-button shape="round" fill="clear" #click="modalController.dismiss()">
<ion-icon slot="icon-only" :icon="arrowBackOutline" />
</ion-button>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
{{ content }}
</ion-content>
</template>
<script lang="ts">
import { IonButton, IonHeader, IonToolbar, IonContent } from '#ionic/vue';
import { arrowBackOutline } from 'ionicons/icons';
import { modalController } from "#ionic/vue";
export default {
name: 'Modal',
components: { IonButton, IonHeader, IonToolbar, IonContent },
setup(){
const content = 'Content';
return {
content,
modalController,
arrowBackOutline,
IonHeader,
IonToolbar
}
}
}
</script>

Vue: Register part of package as component

I have got a wrapper around a package called vue-awesome-swiper, as follows:
Slider.vue
<template>
<div class="media-slider">
<slot :sliderSetting="sliderSettings" :name="name">
<swiper :options="sliderSettings[name]"
class="swiper"
v-if="slides"
ref="default-slider">
<slot name="slides" :slides="slides" :code="code">
<swiper-slide v-for="image in slides" :key="image" v-if="endpoint"
:style="{'background-image': `url('${image}')`}">
</swiper-slide>
</slot>
<div class="swiper-button-next swiper-button-white" slot="button-next"></div>
<div class="swiper-button-prev swiper-button-white" slot="button-prev"></div>
<div class="swiper-pagination" slot="pagination"></div>
<div class="swiper-scrollbar" slot="scrollbar"></div>
</swiper>
</slot>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper';
import 'swiper/css/swiper.css';
import Axios from '../../../../axiosConfig';
// https://github.surmon.me/vue-awesome-swiper/
export default {
components: { Swiper, SwiperSlide },
data: function() {
return {
code: null,
images: [],
defaults: {
'default-slider': {
loop: true,
loopedSlides: 5,
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
},
scrollbar: {
el: '.swiper-scrollbar',
hide: true
}
}
},
sliderSettings: {}
}
},
props: {
endpoint: {
type: String,
default: null
},
settings: {
type: Object,
default: {}
},
theme: {},
name: {},
hash: {},
numberOfImages: {},
imageFormat: {},
vehicleId: {}
},
computed: {
slides() {
if (this.images.length) {
return this.images;
}
return [...Array(parseInt(this.numberOfImages))].map(
(_, i) => {
i++;
return this.imageFormat.replace('#', i);
}
);
}
}
}
</script>
As you can see I have got a slot within this component, however it must use an instance of SwiperSlide for it to work. I need to register this as a component to use it.
However this isn't working as expected:
Vue.component('slider', () => import('./components/Media/Slider/Index'));
Vue.component('slide', () => import('vue-awesome-swiper'));
How can I do this?
Edit
<slider class="app">
<template v-slot:slides="props">
<slide :style="{'background-image': `url('img-src/${props.code}/theme/slide-1.jpg')`}"></slide>
</template>
</slider>
An import like this import() is gonna import the default export of the package, what you have done here is that you have destructured the import with import { .. } from ".." that means it has an named export.
However try it like this
Vue.component('slider', () => import('./components/Media/Slider/Index'));
Vue.component('slide', async () => (await import('vue-awesome-swiper')).SwiperSlide);

Vue unit tests failing because component method calls this.$route.query - TypeError: Cannot read property 'query' of undefined

Long time user of the wisdom of StackOverflow on and off the job, but first time I'm posting a question. What a milestone!
I'm writing unit tests for a large Vue application, and one of my components uses a method that references $route in order to determine if a query param is being passed in / pass in the param if it is being used. The method calling this.$route.query.article_id works great, however now that I am in testing, the tests don't recognize this.$route.query
I've tried to mock the $route object when using shallowMount to mount my localVue, as described in the doc, but it doesn't work, and I continue to get the same error.
Here is my component:
<template>
<b-container fluid>
<div class="content-page-header"></div>
<b-row>
<b-col cols="3" class="outer-columns text-center" style="color:grey">
<font-awesome-icon
:icon="['fas', 'newspaper']"
class="fa-9x content-page-photo mb-3 circle-icon"
/>
<br />
<br />Get practical tips and helpful
<br />advice in clear articles written
<br />by our staff's experts.
</b-col>
<b-col cols="6" v-if="articlesExist">
<h1 class="header-text">
<b>Articles</b>
</h1>
<div v-if="!selectedArticle">
<div v-for="article in articles">
<article-card :article="article" #clicked="onClickRead" />
<br />
</div>
</div>
<div v-else>
<router-link to="articles" v-on:click.native="setSelectedArticle(null)">
<font-awesome-icon icon="chevron-circle-left" />&nbsp
<b>Back to All Articles</b>
</router-link>
<article-header :article="selectedArticle" />
<br />
<span v-html="selectedArticle.text"></span>
</div>
</b-col>
<b-col cols="6" v-else>
<h1 class="header-text">
<b>Articles</b>
</h1>
<div class="text-center">Stay tuned for more Articles</div>
</b-col>
<b-col class="outer-columns">
<b class="text-color" style="font-size:14pt">Saved Articles</b>
<div v-for="article in userArticles">
<router-link
:to="{path:'articles', query: {article_id: article.article.id}}"
v-on:click.native="setSelectedArticle(article.article)"
>
<user-article :article="article.article" />
</router-link>
<br />
</div>
</b-col>
</b-row>
</b-container>
</template>
<script>
import ArticleCard from "./ArticleCard";
import UserArticle from "./UserArticle";
import ArticleHeader from "./ArticleHeader";
import { library } from "#fortawesome/fontawesome-svg-core";
import {
faNewspaper,
faChevronCircleLeft
} from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
library.add(faNewspaper, faChevronCircleLeft);
export default {
name: "Articles",
props: [],
components: {
ArticleCard,
ArticleHeader,
UserArticle,
library,
FontAwesomeIcon,
faNewspaper,
faChevronCircleLeft
},
mixins: [],
data() {
return {
selectedArticle: null
};
},
computed: {
articles() {
return this.$store.getters.articles.filter(article => article.text);
},
articlesExist() {
return Array.isArray(this.articles) && this.articles.length;
},
userArticles() {
return this.$store.getters.userArticles;
},
articleParam() {
return parseInt(this.$route.query.article_id);
}
},
methods: {
setSelectedArticle(article) {
this.selectedArticle = article;
},
onClickRead(article) {
this.selectedArticle = article;
}
},
mounted() {
if (this.articleParam) {
this.setSelectedArticle(
this.articles.filter(article => article.id === this.articleParam)[0]
);
}
}
};
</script>
<style lang="stylus" scoped>
.text-color {
color: #549DB0;
}
.header-text {
color: white;
margin-top: -50px;
margin-bottom: 20px;
}
.outer-columns {
background-color: #F2FBFD;
padding-top: 20px;
}
.nav-back {
color: #549DB0;
background-color: #F0FBFD;
padding: 5px;
}
</style>
And here is my test:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import VueRouter from 'vue-router'
import Articles from '../../../app/javascript/components/member-dashboard/Articles.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
localVue.use(BootstrapVue)
describe('Articles', () => {
let store
let getters
let state = {
articles: [
{
title: "Testing Vue Components"
},
{
title: "This One shows",
text: "<p>You can see me!</p>"
},
{
title: "Another One",
text: "<p>See me too!</p>"
}
],
userArticles: [
{article: {
title: "This One shows",
text: "<p>You can see me!</p>"
}},
{article: {
title: "Another One",
text: "<p>See me too!</p>"
}}
]
}
beforeEach(() => {
getters = {
articles: () => {
return state.articles
},
userArticles: () => {
return state.userArticles
}
}
store = new Vuex.Store({ getters })
})
it('only displays article with body text', () => {
const wrapper = shallowMount(Articles, {
store,
localVue
})
expect(wrapper.vm.articles.length).to.deep.equal(2)
})
})
As I mentioned, in the shallow mount, I've tried doing this:
const wrapper = shallowMount(Articles, {
store,
localVue,
mocks: {
$route: {
query: null
}
}
})
But I continue to get this error:
TypeError: Cannot read property 'query' of undefined
at VueComponent.articleParam (webpack-internal:///1:107:35)
When I remove the line return parseInt(this.$route.query.article_id); from the articleParam method, my test passes.
How do I get around this call to this.$route.query in the component? It's not necessary to my test, but is causing my test to fail when mounting the component.
import import VueRouter from 'vue-router'; in your unite test file and create a new object of the router like const router = new VueRouter(); and use it in your test case.
I have updated code here:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import VueRouter from 'vue-router'
import Articles from '../../../app/javascript/components/member-dashboard/Articles.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
localVue.use(BootstrapVue);
const router = new VueRouter();
describe('Articles', () => {
let store
let getters
let state = {
articles: [
{
title: "Testing Vue Components"
},
{
title: "This One shows",
text: "<p>You can see me!</p>"
},
{
title: "Another One",
text: "<p>See me too!</p>"
}
],
userArticles: [
{article: {
title: "This One shows",
text: "<p>You can see me!</p>"
}},
{article: {
title: "Another One",
text: "<p>See me too!</p>"
}}
]
}
beforeEach(() => {
getters = {
articles: () => {
return state.articles
},
userArticles: () => {
return state.userArticles
}
}
store = new Vuex.Store({ getters })
})
it('only displays article with body text', () => {
const wrapper = shallowMount(Articles, {
store,
router,
localVue
})
expect(wrapper.vm.articles.length).to.deep.equal(2)
})
})