Styling component nested in opened new window - vue.js

I'm trying to style component mounted in new window but it seems like it's not readable. However inline styling is working. It is possible to style with style section in component?
Code explain:
When component is mounted i'm opening new window with this component as argument.
<template>
<!-- Inline styling works -->
<div style="background-color: red" class="window-wrapper" v-if="open">
<slot />
</div>
</template>
<script>
export default {
name: 'window-portal',
props: {
open: {
type: Boolean,
default: false,
}
},
data() {
return {
windowRef: null,
}
},
watch: {
open(newOpen) {
if(newOpen) {
this.openPortal();
} else {
this.closePortal();
}
}
},
methods: {
openPortal() {
this.windowRef = window.open("", "", "width=800,height=600,left=200,top=200");
this.windowRef.addEventListener('beforeunload', this.closePortal);
//here i'm opening new window with this compomnent
this.windowRef.document.body.appendChild(this.$el);
},
closePortal() {
if(this.windowRef) {
this.windowRef.close();
this.windowRef = null;
this.$emit('close');
}
}
},
mounted() {
if(this.open) {
this.openPortal();
}
},
beforeUnmounted() {
if (this.windowRef) {
this.closePortal();
}
}
}
</script>
<style scoped>
.body {
box-sizing: border-box;
}
/* here styling is not working */
.window-wrapper {
background-color: red;
}
</style>
Also components passed as slot cannot read styles from styles section.

Related

How to validate only onblur with Vuelidate?

From my parent component I'm calling my custom input child component this way:
<custom-input
v-model="$v.form.userName.$model"
:v="$v.form.userName"
type="text"
/>
And here's my custom input component:
<template>
<input
v-bind="$attrs"
:value="value"
v-on="inputListeners"
:class="{ error: v && v.$error }"
>
</template>
<script>
export default {
inheritAttrs: false,
props: {
value: {
type: String,
default: ''
},
v: {
type: Object,
default: null
}
},
computed: {
inputListeners () {
const vm = this
return Object.assign({},
this.$listeners,
{
input (event) {
vm.$emit('blur', event.target.value)
}
}
)
}
}
}
</script>
This triggers validation errors from the very first character entered in the input field (which is arguably poor UX, so I really don't understand why this is default behavior).
Anyway, how to trigger such errors only on blur event?
This is not default behavior - it's your code!
Vuelidate validates (and raise errors) only after field is marked as dirty by calling $touch method. But when you are using $model property ($v.form.userName.$model) for v-model, it calls $touch automatically - docs
So either do not use $model for binding and call $touch by yourself on blur event (or whenever you want)
Alternatively you can try to use .lazy modifier on v-model but that is supported only on native input elements (support for custom components is long time request)
Example below shows how to implement it yourself....
Vue.use(window.vuelidate.default)
Vue.component('custom-input', {
template: `
<input
v-bind="$attrs"
:value="value"
v-on="inputListeners"
:class="status(v)"
></input>
`,
inheritAttrs: false,
props: {
value: {
type: String,
default: ''
},
v: {
type: Object,
default: null
},
lazy: {
type: Boolean,
default: false
}
},
computed: {
inputListeners() {
const listeners = { ...this.$listeners }
const vm = this
const eventName = this.lazy ? 'change' : 'input'
delete listeners.input
listeners[eventName] = function(event) {
vm.$emit('input', event.target.value)
}
return listeners
}
},
methods: {
status(validation) {
return {
error: validation.$error,
dirty: validation.$dirty
}
}
}
})
const { required, minLength } = window.validators
new Vue({
el: "#app",
data: {
userName: ''
},
validations: {
userName: {
required,
minLength: minLength(5)
}
}
})
input {
border: 1px solid silver;
border-radius: 4px;
background: white;
padding: 5px 10px;
}
.dirty {
border-color: #5A5;
background: #EFE;
}
.dirty:focus {
outline-color: #8E8;
}
.error {
border-color: red;
background: #FDD;
}
.error:focus {
outline-color: #F99;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vuelidate/dist/vuelidate.min.js"></script>
<script src="https://unpkg.com/vuelidate/dist/validators.min.js"></script>
<div id="app">
<custom-input v-model="$v.userName.$model" :v="$v.userName" type="text" lazy></custom-input>
<pre>Model: {{ userName }}</pre>
<pre>{{ $v }}</pre>
</div>
Try to emit the input event from the handler of blur event so :
instead of :
v-on="inputListeners"
set
#blur="$emit('input', $event.target.value)"

Vue dynamic class binding with computed props

I am trying to bind a class from a parent component to a child component via a computed switch case to an slot.
Parent:
<template>
<mcTooltip :elementType="'text'"><p>Test</p></mcTooltip>
</template>
<script>
import mcTooltip from '#/components/mcTooltip/index.vue';
export default {
components: {
mcTooltip
}
};
</script>
Child:
<template>
<div>
<slot :class="[elementClass]" />
</div>
</template>
<script>
export default {
props: {
elementType: {
type: String,
required: true,
// must have one of these elements
validator: (value) => {
return ['text', 'icon', 'button'].includes(value);
}
}
},
data() {
return {};
},
computed: {
elementClass: () => {
// return this.elementType ? 'tooltip--text' : 'tooltip--text';
// calls prop value for verification
switch (this.elementType) {
case 'text':
return 'tooltip--text';
case 'icon':
return 'tooltip--icon';
case 'button':
return 'tooltip--button';
default:
return 'tooltip--text';
}
}
},
};
</script>
<style lang="scss" scoped>
.tooltip--text {
text-decoration: underline dotted;
cursor: pointer;
&:hover {
background: $gray_220;
}
}
</style>
Whatever I try I dont seem to make it work in any way. Thats my latest attempt. The vue devtools say to my computed prop "(error during evaluation)".
I found a solution, the way I did it is as following:
<div
v-show="showTooltip"
ref="mcTooltipChild"
:class="['tooltip__' + elementType]"
></div>
elementType: {
type: String,
default: 'small',
},

How to implement a derived render implementation in Vue.js using ES6 classes

Apologies in advance for the length of this query..
In React I can have a render function on any standard ES6 class that returns a JSX.Element. This is brilliant because it allows me to have derived ES6 classes where each derived class implements its own JSX rendering implementation - see (ts) example below:
export class Colour {
public name: string = "";
constructor(name) {
this.name = name;
}
public render(): JSX.Element {
return <span>Error, no colour specified</span>;
}
}
export class Red extends Colour {
constructor() {
super("red");
}
public render(): JSX.Element {
return <div style={{ color: this.name }}>Hi, I am {this.name}!</div>;
}
}
export class Blue extends Colour {
constructor() {
super("blue");
}
public render(): JSX.Element {
return <h2 style={{ color: this.name }}>Hi, I am {this.name}!</h2>;
}
}
Then in a React component I can create a list of Colour objects that I can render easily like so:
function App() {
const list = [];
list.push(new Red());
list.push(new Blue());
list.push(new Red());
const listItems = list.map(item => <li key={item}>{item.render()}</li>);
return (
<div className="App">
<h1>React derived rendering </h1>
<ul>{listItems}</ul>
</div>
);
}
Resulting in output like this:
and all is well...
Now my question is: Is there any way to do this as easily in Vue.js?
Please NOTE - it is important to me to create a number of derived instances in code and add these to a list to be rendered!
Right now, the closest I can make this work is to create a dummy Vue component and when it renders I actually call the derived implementation handing over the render handle 'h'.
Vue.component("fusion-toolbar", {
props: ["items"],
template: `
<div>
<div v-for="item in items"
v-bind:key="item.code" >
<f-dummy v-bind:item='item'></f-dummy>
</div>
</div>
`
});
Vue.component("f-dummy", {
props: ["item"],
render(h) {
return this.item.render(h);
}
});
export class Colour {
public colour: string = "";
constructor(colour: string) {
this.colour = colour;
}
render(h: any) {
return h("h1", "Hello, I am " + this.colour);
}
}
export class Red extends Colour {
constructor() {
super("red");
}
render(h: any) {
return h("h2", "Hello, I am " + this.colour);
}
}
export class Blue extends Colour {
private _isDisabled = true;
constructor(disabled: boolean) {
super("blue");
this._isDisabled = disabled;
}
render(h: any) {
return h('div',
[
h('h4', {
class: ['example-class', { 'conditional-class': this._isDisabled }],
style: { backgroundColor: this.colour }
}, "Hello, I am " + this.colour)
]
)
}
}
Then in my parent listing component I have:
<template>
<div id="app"><fusion-toolbar v-bind:items="myitems"> </fusion-toolbar></div>
</template>
<script>
import { Red, Blue } from "./Colour";
export default {
name: "App",
data: function() {
return {
myitems: []
};
},
mounted() {
this.myitems.push(new Red());
this.myitems.push(new Blue(false));
this.myitems.push(new Blue(true));
}
};
</script>
Is this the best way to go about it? Any feedback is welcomed...
I've done this solution freehand, so it may require some tweaks. Since the only variable is the color/name, it seems like you could easily make a catch-all component Color like this:
<template>
<div>
<span v-if="name === ''">Error, no colour specified</span>
<h2 v-else :style="{ color: name }">Hi, I am {{name}}!</h2>
</div>
</template>
<script>
export default {
data() {
return {
name: ''
}
}
}
</script>
You'd create a parent element with a slot.
<template>
<div>
<slot></slot>
</div>
</template>
Then you'd bring it into the parent with a v-for directive, below. You wouldn't need myItems to be a list of components--just a list of string color names:
<template>
<parent-element>
<Color v-for="item in myItems" :name="item" />
</parent-element>
</template>
<script>
import ParentElement from './ParentElement.vue'
import Color from './Color.vue'
export default {
components: {
Color, ParentElement
},
data() {
return {
myItems: ['red', 'red', 'blue']
}
}
}
</script>
Updated: if all you really want is inheritability, create a mixin:
var myColorMixin = {
data() {
return {
name: ''
}
},
computed: {
message() {
return 'Error, no colour specified';
}
}
}
Then override the properties you want when you inherit it:
export default {
mixins: [myColorMixin],
computed: {
message() {
return `Hi, I am ${this.name}`;
}
}
}
Having had some time to think it over I have to conclude my annoyance came from having to introduce the 'f-dummy' intermediate component just so I was able to capture Vue's render function which I could then pass on to the pojo.
What I have changed now is that I render the list itself using the lower level 'render(createElement)' (or 'h') handle that I can then pass on to the child without having to resort to an intermediate component.
Here is the new code, first off I define the ES6 classes (Note that Colour, Orange, Blue are just examples):
export class Colour {
public colour: string = "";
constructor(colour: string) {
this.colour = colour;
}
render(h: any) {
return h("h1", "Error - please provide your own implementation!");
}
}
export class Orange extends Colour {
constructor() {
super("orange");
}
public getStyle() {
return {
class: ['example-class'],
style: { backgroundColor: this.colour, border: '3px solid red' }
}
}
render(h: any) {
return h('h4', this.getStyle(), "This is my colour: " + this.colour)
}
}
export class Blue extends Colour {
private _isDisabled = true;
constructor(disabled: boolean) {
super("lightblue");
this._isDisabled = disabled;
}
render(h: any) {
return h('div',
[
h('h4', {
class: ['example-class', { 'is-disabled': this._isDisabled }],
style: { backgroundColor: this.colour }
}, "Hello, I am " + this.colour)
]
)
}
}
To render the list I now do this:
Vue.component("fusion-toolbar", {
props: ["items"],
render(h: any) {
return h('div', this.items.map(function (item: any) {
return item.render(h);
}))
}
});
Now I can simply add Orange or Blue 'pojo' instances to my list and they will be rendered correctly. See content of App.vue below:
<template>
<div id="app">
<fusion-toolbar v-bind:items="myitems"> </fusion-toolbar>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { Colour, Orange, Blue } from "./components/DropDownItem";
export default Vue.extend({
name: "app",
data: function() {
return {
myitems: [] as Colour[]
}
},
mounted() {
this.myitems.push(new Orange());
this.myitems.push(new Blue(false));
this.myitems.push(new Blue(true));
}
});
</script>
<style lang="scss">
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
text-align: center;
}
.example-class {
width: 250px;
padding: 3px;
}
.is-disabled {
color: gray;
}
</style>
This is what the sample then looks like:
Very neat. I realise I can use a Babel plugin to use JSX instead of Vue's lower level rendering but I am quite enjoying the power it gives me!

Nuxt/Vue/Bootstrap-vue shrink navbar on scroll

Learning Vue with Nuxt. Want to change navbar classes depending on the page scroll position.
Looked in several places, but haven't found a working solution.
Here's what I have to work with:
``` default.vue
<template lang="pug">
div(v-on:scroll="shrinkNav", v-on:click="shrinkNav")
b-navbar.text-center(toggleable="sm" type="light" sticky v-b-scrollspy)
#myNav.mx-auto.bg-white
b-navbar-toggle(target="nav_collapse")
b-navbar-brand.mx-auto(href="#")
| Example.org
b-collapse#nav_collapse.mx-auto(is-nav='')
b-navbar-nav(justified, style="min-width: 600px").vertical-center
b-nav-item.my-auto(href='#home') Home
b-nav-item.my-auto(href='/how') How
i.fab.fa-earlybirds.fa-2x.mt-2.mb-3
b-nav-item.my-auto(href='/values') Values
b-nav-item.my-auto(href='/join-us') Join Us
#content.container(v-on:scroll="shrinkNav", v-on:click="shrinkNav")
nuxt
nuxt
</template>
<script>
// resize navbar on scroll
export default {
methods: {
shrinkNav() {
var nav = document.getElementById('nav')
var content = document.getElementById('content')
if (nav && content) {
if(content.scrollTop >= 150) {
nav.classList.add('shrink')
} else {
nav.classList.remove('shrink')
}
}
console.log(document.documentElement.scrollTop || document.body.scrollTop)
}
}
}
</script>
```
shrinkNav runs twice on click, but nothing on scroll. What is the Vue/Nuxt way to do this?
In your .vue:
<template> section:
<nav id="nav" class="navbar is-transparent is-fixed-top">
<script> section:
export default {
mounted() {
this.$nextTick(function(){
window.addEventListener("scroll", function(){
var navbar = document.getElementById("nav");
var nav_classes = navbar.classList;
if(document.documentElement.scrollTop >= 150) {
if (nav_classes.contains("shrink") === false) {
nav_classes.toggle("shrink");
}
}
else {
if (nav_classes.contains("shrink") === true) {
nav_classes.toggle("shrink");
}
}
})
})
},
}
Live Demo on codesandbox
Ok, here's a solution using Plugins. There may be a better way:
1) Define a directive in plugins/scroll.js
``` javascript
// https://vuejs.org/v2/cookbook/creating-custom-scroll-directives.html#Base-Example
import Vue from 'vue'
Vue.directive('scroll', {
inserted: function (el, binding) {
let f = function (evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f)
}
}
window.addEventListener('scroll', f)
}
})
```
2) Add plugin to project in nuxt.config.js
``` javascript
module.exports = {
head: { },
plugins: [
{ src: '~/plugins/scroll.js', },
]
}
```
3) Use the v-scroll directive to define custom behavior in menu in /layouts/default.vue
``` javascript
<template lang="pug">
div(v-scroll="shrinkNav")
b-navbar.text-center(toggleable="sm" type="light" sticky)
#myNav.mx-auto.bg-white
b-navbar-toggle(target="nav_collapse")
b-navbar-brand.mx-auto(href="/#home") Example.org
b-collapse#nav_collapse.mx-auto(is-nav='')
b-navbar-nav(justified, style="min-width: 600px").vertical-center
b-nav-item.my-auto(to='/#home') Home
#content.container
nuxt
</template>
<script>
export default {
methods: {
shrinkNav() {
var scrollPosition = document.documentElement.scrollTop || document.body.scrollTop
var nav = document.getElementById('myNav')
console.log(scrollPosition, nav)
if(scrollPosition >= 150) {
nav.classList.add('shrink')
} else {
nav.classList.remove('shrink')
}
},
},
}
</script>
<style>
nav.navbar {
margin: 0;
padding: 0;
}
#myNav {
border-radius: 0 0 10px 10px;
border: 2px solid purple;
border-top: none;
}
#myNav.shrink {
border: none;
}
</style>
```

create a component inside a component, getting error: "Failed to mount component: template or render function not defined."

trying to make a component (tag: simple-div-container) that on a button press will create 2 (tag: simple-div) components, inside each new simple-div there will be a simple-div-container.
so I can create "endless" components inside each other.
I have simple-div-container and when I press the button I get 2 simple-div
but I don't get inside them the NEW simple-div-container.
I get an error:
Failed to mount component: template or render function not defined.
code for tag: simple-div-container
<template>
<div>
<button #click="insert2Div" class="div-controler">insert 2 div</button>
<div v-if="divs" class="horizontal-align">
<simplediv v-if="divs" :style="{height: simpleDivHeight + 'px',width: simpleDivWidthPrecent/2 + '%', border: 1 + 'px solid' }"
:height="simpleDivHeight" :widthPrecent="simpleDivWidthPrecent" :isRender="true"></simplediv>
<simplediv v-if="divs" :style="{height: simpleDivHeight + 'px',width: simpleDivWidthPrecent/2 + '%', border: 1 + 'px solid' }"
:height="simpleDivHeight" :widthPrecent="simpleDivWidthPrecent" :isRender="true"></simplediv>
</div>
</div>
</template>
<script>
import SimpleDiv from '../simple-div/simpleDiv.vue';
export default {
props: {
simpleDivHeight: {
require: true,
type: Number
},
simpleDivWidthPrecent: {
require: true,
type: Number
}
},
data() {
return {
divs: false,
}
},
methods: {
insert2Div() {
console.log('insert 2 div')
this.divs = true;
},
},
components: {
simplediv: SimpleDiv
},
}
</script>
<style scoped>
.horizontal-align {
display: flex;
flex-direction: row;
}
</style>
code for tag: simple-div
<template>
<div>
<simple-div-container v-if="isRender" :simpleDivHeight="height" :simpleDivWidthPrecent="widthPrecent/2"></simple-div-container>
</div>
</template>
<script>
import simpleDivContainer from '../simple-div-container/simpleDivContainer.vue';
export default {
props: {
height: {
require: true,
type: Number
},
widthPrecent: {
require: true,
type: Number
},
isRender:{
require: true,
type: Boolean
}
},
data() {
return {
isDivContainer: false
}
},
components: {
'simple-div-container': simpleDivContainer
}
}
</script>
<style scoped>
.div-controler {
position: fixed;
transform: translate(-10%, -320%);
}
</style>
an interesting point: if i add to simple-div data some property(while webpack listens to changes) than it will automatically rerender and the new simpe-div-container will show
You have a circular reference problem. You should check if registering the simple-div component in the beforeCreate lifecycle hook helps. In the simple-div-container:
In the simple-div-container:
beforeCreate () {
this.$options.components.simple-div = require('../simple-div/simpleDiv.vue')
}