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 = () => { /*...*/ }
Related
I am using Vite + Electron to make a desktop app demo.
But it cannot load png resource, but js, css resource is fine. So it worked like this
png canoot be loaded
when I check the console, it shows: Failed to load resource: net::ERR_FILE_NOT_FOUND
Also, the url of the png is weird:
it is not even a common file path
So what happened really ???
Here is my code:
vite.config.js
import { defineConfig } from 'vite'
import vue from '#vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
base: path.resolve(__dirname, './dist/'),
plugins: [vue()]
})
index.js
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
webSecurity: false,
}
})
win.loadFile('dist/index.html')
win.webContents.openDevTools()
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
App.vue
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<img alt="Vue logo" src="/src/assets/logo.png" />
<HelloWorld msg="Hello Vue 3 + Vite" />
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
I know perhaps this problem sounds silly, but I just start it and Vue,JS... are really not my comfort zone. So if anyone could help me, I will really appreciate it.
how do I save assigned values from inside onMounted hook in Vue 3? My intention of saving the width and height values is so that can use to manipulate the values inside a custom-directive outside of the setup function later on.
I realised that it is only possible manipulating inside the onMounted and using watch see if there is a change to the value. But even so, after assigning the values, it is still undefined.
Is using Vuex the way to go for my current solution?
Because I can only access DOM properties inside onMounted hook and not anywhere else.
<template>
<div class="outer">
<div class="container">
<div>
<div class="border">
<img
id="image"
ref="image"
src="#/assets/1.jpg"
class="image"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, onMounted, watch } from "vue";
const widthVal = ref<number>()
const heightVal = ref<number>()
export default defineComponent({
setup() {
const image = ref<HTMLElement | null>(null)
onMounted(() => {
if (image.value) {
widthVal.value = image.value.offsetWidth;
heightVal.value = image.value.offsetHeight;
console.log('width: ', widthVal.value)
console.log('height: ', heightVal.value)
}
})
watch([widthVal, heightVal], (newVal, oldVal) => {
widthVal.value = newVal[0];
heightVal.value = newVal[1];
console.log(widthVal.value)
console.log(heightVal.value)
})
// becomes undedefined
console.log('width: ', widthVal.value)
return { image }
}
});
</script>
<style>
p {
color: yellow;
}
.outer {
margin: 1em;
display: flex;
justify-content: center;
height: 100vh;
}
.container {
background: rgb(98, 98, 98);
border-radius: 5px;
width: 950px;
height: 650px;
padding: 1em;
overflow: hidden;
font-family: "Trebuchet Ms", helvetica, sans-serif;
}
img {
width: 950px;
height: 650px;
/* remove margins */
margin-left: -18px;
margin-top: -18px;
}
</style>
If you inspect widthVal inside setup() and not inside the watch or onMounted function it gets called BEFORE the values are assigned cause assignments inside setup happen even before the beforeCreate hook.
See: lifecycle hooks
EDIT:
If you really want to use widthVal/heightVal inside setup I'd recommend using it within a function (or a watcher, whatever you need) and calling that inside onMounted after you initialized widthVal/heightVal. E.g.:
const doSomethingElse = () => {
// Use widthVal and heightVal here...
}
onMounted(() => {
widthVal.value = newVal[0];
heightVal.value = newVal[1];
doSomethingElse();
})
...
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')
})
})
I'm developing a mobile application using React Native. This project needs a custom button to provides a boolean type of input. But I have no idea how to create this kind of custom component for that. I did a research and I try to create this custom button with a react-native switch (import { Switch } from 'react-native';). But seems like It is difficult to style.
I'm not sure what would be the best way to achieve that? Using the switch component? Please help me to find a better solution or new approach for this.
Thank you.
I have made a custom switch in react native and You can do the styling in it a source code is given below -
import React from 'react'
import { Text, TouchableOpacity } from 'react-native'
import styled from 'styled-components/native'
class App extends React.Component {
state = {
active: false
}
handleOFF = () => {
this.setState({
active: false
});
}
handleOn = () => {
this.setState({
active: true
});
}
render() {
return (
<MainView>
<Label>
<LabelOff onPress={this.handleOFF} active={this.state.active} activeOpacity={0.8}>
<Off>OFF</Off>
</LabelOff>
<LabelOn onPress={this.handleOn} active={this.state.active} activeOpacity={0.8}>
<On>ON</On>
</LabelOn>
</Label>
</MainView>
)
}
}
const MainView = styled.View`
margin:50px;
`
const Label = styled.View`
height:60px;
width:240px;
flex-direction:row;
justify-content:space-around;
align-items:center;
background-color:transparent;
`
const LabelOff = styled.TouchableOpacity`
height:60px;
width:120px;
background-color:${props => props.active ? 'transparent' : '#cb6161'};
border:2px solid #cb6161;
border-right-width:0px;
align-items:center;
justify-content:space-around;
`
const LabelOn = styled.TouchableOpacity`
height:60px;
width:120px;
background-color:${props => props.active ? '#55acee' : 'transparent'};
border:2px solid #55acee;
border-left-width:0px;
align-items:center;
justify-content:space-around;
`
const Off = styled.Text`
font-size:22px;
`
const On = styled.Text`
font-size:22px;
`
export default App
Custom switch is done !
I'm starting with polymer 3 and i'm working on this tutorial https://www.polymer-project.org/1.0/start/first-element/step-5, so basically i have the component js file as follows
icon-toggle.js
import { PolymerElement, html } from '#polymer/polymer/polymer-element.js';
import '#polymer/iron-icon/iron-icon.js';
class IconToggle extends PolymerElement {
static get template() {
return html`
<style>
/* shadow DOM styles go here */
:host {
display: inline-block;
--icon-toggle-color: lightgrey;
--icon-toggle-outline-color: black;
--icon-toggle-pressed-color: red;
}
iron-icon {
fill: var(--icon-toggle-color, rgba(0,0,0,0));
stroke: var(--icon-toggle-outline-color, currentcolor);
cursor: pointer;
}
:host([pressed]) iron-icon {
fill: var(--icon-toggle-pressed-color, currentcolor);
}
</style>
<!-- shadow DOM goes here -->
<iron-icon icon="[[toggleIcon]]"></iron-icon>
`;
}
static get properties() {
return {
pressed: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true
},
toggleIcon: {
type: String
}
};
}
constructor() {
super();
this.addEventListener('click', this.toggle.bind(this));
}
toggle() {
this.pressed = !this.pressed;
}
}
customElements.define('icon-toggle', IconToggle);
Now I'm wondering how to import this and use it in an angular 5 app.
Generate a new Angular app.
ng new with-polymer
From within with-polymer create a directory to store the web components in.
mkdir src/app/components
Copy your polymer component code to src/app/components/icon-toggle.js
Install the polymer dependencies.
npm install #polymer/iron-icon #polymer/polymer
Update src/app/app.module.ts to import CUSTOM_ELEMENTS_SCHEMA and tell NgModule that custom elements will be in use.
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '#angular/core';
#NgModule({
...
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
Import icon-toggle in src/app/app.module.ts.
import './components/icon-toggle';
Add an icon-toggle to src/app/app.component.html.
<icon-toggle toggle-icon="star"></icon-toggle>
Start up the dev server.
npm start
Note that you will probably want to include some web component polyfills.