Vue.js: Known issues with scoped styles and the v-html directive? - vue.js

I'm just wondering if there is a known issue with scoped styles and the v-html directive? I seem to find that either applying the same styling to the parent or removing the scoped key work from styles everything seems to work ok...?
Example component with the issue (this works with the wordpress API if anyone with a Wordpress site want's to test with their setup):
<template>
<div v-bind:id="posts">
<div v-for="(post, i) in posts" v-bind:key="i">
<div v-html="post.content.rendered"></div>
</div>
</div>
</template>
<script>
export default {
name: "Posts",
props: {
msg: String
},
data() {
return {
posts: null
};
},
created() {
this.$http.get(this.$store.state.endpoint + "posts").then(response => {
this.posts = response.body;
});
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
p {
color: #42b983; // v-html in this example spits out numerous <p></p> tags...
}
</style>
*for replicating example, you can replace this.$store.state.endpoint with your Wordpress API endpoint, e.g., http://localhost:8000/wp-json/wp/v2 or similar.

From vue-loader docs:
Dynamically Generated Content
DOM content created with v-html are not affected by scoped styles, but you can still style them using deep selectors.
So to style the dynamic content, you should use deep selectors as seen in the following example:
<div class="posts">
<div v-for="(post, i) in posts" v-bind:key="i">
<div v-html="post.content.rendered"></div>
</div>
</div>
...
<style scoped>
.posts >>> p {
color: blue;
}
</style>
demo

Related

Nuxt - Using router.push inside a component not changing pages correctly

index.vue -
<template>
<div>
<Header />
<div class="container">
<SearchForm />
</div>
</div>
</template>
<script>
const Cookie = process.client ? require('js-cookie') : undefined
export default {
data() {
return {
form: {
email: '',
password: ''
},
show: true
}
},
methods: {
logout() {
// Code will also be required to invalidate the JWT Cookie on external API
Cookie.remove('auth')
this.$store.commit('setAuth', {
auth: null,
user_type: null
})
}
}
}
</script>
<style>
.container {
/* margin: 0 auto; */
/* min-height: 100vh; */
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
</style>
jobs.vue -
<template>
<div>
<Header />
<SearchForm />
<b-container class="main_container">
<b-row>
<h1> Results for "{{q}}"</h1>
</b-row>
<b-row>
<ul id="array-rendering">
<li v-for="item in results" :key="item.job_id">
{{ item.job_title }}
{{ item.job_city }}
{{ item.job_state }}
{{ item.job_work_remote }}
</li>
</ul>
</b-row>
</b-container>
</div>
</template>
<script>
const Cookie = process.client ? require('js-cookie') : undefined
export default {
// middleware: 'notAuthenticated',
watchQuery: ['q'],
data() {
return {
q: null,
results: []
}
},
async fetch() {
this.q = this.$route.query.q
this.results = await this.$axios.$get('/api/job/search', {
params: {
keyword: this.q,
}
})
},
methods: {
}
}
</script>
<style>
.container {
align-items: center;
text-align: center;
}
</style>
SearchForm.vue component -
<template>
<div id='searchFormDiv'>
<b-form inline #submit.prevent="onSubmit">
<label class="sr-only" for="inline-form-input-name"> keyword</label>
<b-form-input v-model="form.keyword" id="inline-form-input-name" class="mb-2 mr-sm-2 mb-sm-0" placeholder="Job title or keyword" size="lg"></b-form-input>
<label class="sr-only" for="inline-form-input-username">location</label>
<b-input-group class="mb-2 mr-sm-2 mb-sm-0">
<b-form-input v-model="form.location" id="inline-form-input-username" size="lg" placeholder="City, state or zip"></b-form-input>
</b-input-group>
<b-button type="submit" variant="primary">Find Jobs</b-button>
</b-form>
</div>
</template>
<script>
import {
BIconSearch,
BIconGeoAlt
} from 'bootstrap-vue'
export default {
data() {
return {
form: {
keyword: '',
location: ''
}
}
},
created () {
this.form.keyword = this.$route.query.q
},
methods: {
onSubmit() {
this.$router.push({
path: 'jobs',
query: {
q: this.form.keyword
}
});
}
},
components: {
BIconSearch,
BIconGeoAlt
},
}
</script>
<style>
#searchFormDiv {
margin-top: 50px
}
</style>
The route for "http://localhost:3000/" returns the index.vue page.
In this vue page, I have a component with a search form. Once you complete these form and hit the seach button, it will re-direct to a results page
if this.form.keyword = "Data", the next URL will be "http://localhost:3000/jobs?q=Data" and it will be using the jobs.vue page.
The issue I'm running into is the CSS is not being loaded from the jobs.vue page. It's still coming from the index.vue page for some reason. If I refresh the page, then the CSS from jobs.vue is loading. I need the CSS to load from jobs.vue on the initial redirect. All of the query data is working as expected so thats a plus.
However, the following CSS is being applied from index.vue for some reason instead of the CSS from the jobs.vue page -
display: flex;
justify-content: center;
Does anyone know whats going on here? This app is SSR and not SPA.
You have to scope your css from the index.vue page to the other pages with the scoped directive (see docs https://vue-loader.vuejs.org/guide/scoped-css.html)
<style scoped>
/* local styles */
</style>
<style>
/* global styles */
</style>
You can add your global CSS in your layouts/default.vue file.
This solved the issue -
methods: {
onSubmit() {
window.location = 'http://localhost:3000/jobs?q=' + this.form.keyword;
}
},

Delete item for todo app in with $emit 2 level up or 1 level up? [duplicate]

This question already has answers here:
Vue 2 - Mutating props vue-warn
(28 answers)
Closed 2 years ago.
I have 3 .vue here: App.vue (default), Todos.vue and Todoitem.vue. I am following the tutorial from https://www.youtube.com/watch?v=Wy9q22isx3U&t=2458. May I know why the author in TodoItem.vue emit id two level up to App.vue to perform the method to delete? Is it best practice or better coding style? Is it easier to just go up one level for Todos.vue to do the same? Below is my one level up approach for any comment.
Below is my TodoItem.vue code
<template>
<div class="todo-item" v-bind:class="{'is-complete':todoObj.completed}">
<p>
<input type="checkbox" v-on:change="markComplete" />
{{todoObj.title}}
<button #click="$emit('del-todo',todoObj.id)" class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props: ["todoObj"], // todoObj is defined in the parent.
methods: {
markComplete() {
this.todoObj.completed = !this.todoObj.completed;
}
}
};
</script>
<style scoped>
.todo-item {
background: #f4f4f4;
padding: 10px;
border-bottom: 1px #ccc dotted;
}
.is-complete {
text-decoration: line-through;
}
.del {
background: #ff0000;
color: #fff;
border: none;
padding: 5px 9px;
border-radius: 50%;
cursor: pointer;
float: right;
}
</style>
Below is my Todo.vue code
<template>
<div>
<h1>Todo List2</h1>
<!-- :key= and v-bind:key= are exactly the same. -->
<!-- v-bind. Shorthand: : -->
<div v-for="todo in ptodos" :key="todo.id">
<!-- Define todoObj here which to be used in the child component, TodoItem -->
<MyTodoItem v-bind:todoObj="todo" v-on:del-todo="deleteTodo" />
<!-- del-todo is from the child. child goes up to parent and then to grandparent (App.vue) -->
</div>
</div>
</template>
<script>
import MyTodoItem from "./TodoItem.vue";
export default {
name: "Todos",
components: {
MyTodoItem
},
props: ["ptodos"],
methods: {
deleteTodo(id) {
this.ptodos = this.ptodos.filter(todo => todo.id !== id);
}
}
};
</script>
<style scoped>
</style>
Below is my App.vue code
<template>
<MyToDos v-bind:ptodos="todos" />
</template>
<script>
import MyToDos from "./components/Todos";
export default {
name: "App",
components: { MyToDos },
data() {
return {
todos: [
{
id: 1,
title: "Todo One",
completed: false
},
{
id: 2,
title: "Todo Two",
completed: true
},
{
id: 3,
title: "Todo Three",
completed: false
}
]
};
}
};
</script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Arial, Helvetica, sans-serif;
line-height: 1.4;
}
</style>
If you can do it with one level up it's better. To have multiple props on each child can be a bad practice called prop drilling.
Vuex is a good alternative to avoid to get props nested.

How to properly set a h1 in another component?

I'm on the path of learning Vue and i have a question that you might find trivial but i struggle to find an answer. First of all, i'm using Vue with Vue Router, in order to build a test blog.
Let's say the h1 of my page is inside the header component, itself included inside the layout.
But then, how can i set the content of this h1 tag for each page, since it belongs to the header, which is only included in the layout? One solution i found was to set meta.h1 on each route declaration, something like that:
{
path: 'articles',
meta: {
h1: 'All the articles'
}
}
Then, inside the header, i can use a method to get the value of this.$route.meta.h1. Something like that:
methods: {
h1() {
return this.$route.meta.h1
}
}
It works, but it seems kind of wrong, since i don't think that's the job of the route file to handle this. To make it simple, what is the best way, the preferred way, to handle this situation? ty :)
[EDIT]
Here is a simple example page from the bootstrap documentation that could well illustrate my question: https://getbootstrap.com/docs/4.0/examples/blog/
Title of a longer featured blog post
is the h1 and would probably be dynamic, changing from page to page. But it's part of the header component, not part of the articles, of the forum, of the comments or any other component... so if i can reformulate my question, that would be "how can i set this h1 title elegantly" ? Don't think of something complicated, this is a very basic question from a beginner looking toward some answers :p
You can pass props via routes like this, but I'm not sure how you would supply that variable.
I'm not sure of the structure of your app, and if the header component is the header for the entire app, or if it's just a header for each article.. If it's a header just for the article, there is no need to emit events.. (see example below).
[CodePen mirror]
const appHeader = {
template: "#app-header",
computed: {
dynamicHeader() {
let r = this.$route.name;
// caps first letter
let f = r.charAt(0).toUpperCase() + r.slice(1);
switch(f){
case "Home": return f + " Page!";
case "Contacts": return "Contact Us!";
case "About": return f + " Us!!"
}
}
}
};
const contactsPage = {
template: "#contacts-page"
};
const aboutPage = {
template: "#about-page"
};
const homePage = {
template: "#home-page"
};
const routes = [
{
path: "/",
name: "home",
components: {
header: appHeader,
content: homePage
}
},
{
path: "/contact",
name: "contacts",
components: {
header: appHeader,
content: contactsPage
}
},
{
path: "/about",
name: "about",
components: {
header: appHeader,
content: aboutPage
}
}
];
const router = new VueRouter({ routes });
new Vue({
router
}).$mount("#app");
nav.mainNav > * {
padding: 0 0.75rem;
text-decoration: none;
}
nav.mainNav > *:nth-last-child(n+2) {
border-right: 1px solid #aaa;
}
#headerContainer {
background-color: yellow;
margin-bottom: -20px;
}
#page {
border: 1px solid black;
margin: 20px 20px 20px 20px;
}
#page > * {
margin: 10px 10px 10px 10px;
text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.4/vue-router.min.js"></script>
<div id="app">
<div>
<router-view name="header"></router-view>
<router-view id="page" name="content"></router-view>
</div>
</div>
<!-- -------------------- -->
<!-- APP HEADER COMPONENT -->
<!-- -------------------- -->
<script type="text/x-template" id="app-header">
<div>
<div id="headerContainer">
<h1>{{ dynamicHeader }}</h1>
</div>
<nav class="mainNav">
<router-link
:to="{ name: 'home' }"
>Home</router-link>
<router-link
:to="{ name: 'about' }"
>About</router-link>
<router-link
:to="{ name: 'contacts' }"
>Contacts</router-link>
</nav>
</div>
</script>
<!-- -------------------- -->
<!-- ----------------------- -->
<!-- CONTACTS PAGE COMPONENT -->
<!-- ----------------------- -->
<script type="text/x-template" id="contacts-page">
<div style="background-color:blue;">
<h2>this is the contacts page</h2>
</div>
</script>
<!-- ----------------------- -->
<!-- -------------------- -->
<!-- ABOUT PAGE COMPONENT -->
<!-- -------------------- -->
<script type="text/x-template" id="about-page">
<div style="background-color:green;">
<h2>this is the about page</h2>
</div>
</script>
<!-- -------------------- -->
<!-- ------------------- -->
<!-- HOME PAGE COMPONENT -->
<!-- ------------------- -->
<script type="text/x-template" id="home-page">
<div style="background-color:red;">
<h2>this is the home page</h2>
</div>
</script>
I found a solution that seems to better fit what i requested, which is using an event bus as described in this video:
https://www.youtube.com/watch?v=jzh4zQcfB0o
To sum up what the guy says, the idea is to use another instance of Vue as an event bus in order to emit and listen to events between components not related to each other. This way, i can set the h1 title of my page from any components.

Vue DOM not reactive to computed property

I have a button that is set to be disabled if a computed property's valid property is false. If true, the button should be enabled and allow a user to move to the next step in the current flow.
My currentStep computed property is updating perfectly on changes to the current step's inputs, but the button :disabled="currentStep.valid" isn't recognizing the changes that ARE HAPPENING to currentStep.valid.
If I click on the current component (addnewprogram) in vue-devtools to see it's data, the button displays correctly!
Seen here: http://recordit.co/XH6HX7JLhV
However, without clicking on addnewprogram in the devtools, it doesn't function correctly.
Is there an obvservation caveat I'm missing?
The code for this functionality can be found here:
<template>
<section class="newprogram">
<div class="newprogram--content">
<div class="newprogram--stepone" v-if="progression.current === 1">
<div class="content--left">
<a class="link uppercase">use existing program information<i class="fa fa-picture-o"></i></a>
<base-input v-for="input in currentStep.inputs"
:key="input.id"
:data="input"
v-model="input.value"></base-input>
</div>
<div class="content--right">
<!-- images -->
</div>
</div>
<div class="newprogram--steptwo" v-if="progression.current === 2">
<choice-progression :step="1"></choice-progression>
</div>
</div>
</div>
<!-- Consistent among all steps -->
<div class="container--bottomnav">
<div class="bottomnav--left">
<base-btn class="button-fluid"
:data="currentStep.btns[0]"></base-btn>
</div>
<div class="bottomnav--right">
<base-btn :data="currentStep.btns[1]"
:disabled="currentStepValid"></base-btn>
</div>
</div>
<!-- -->
</section>
</template>
<script>
import bottomNav from '../main/bottom-nav.vue';
import baseProgressionBarbell from '../base/base-progression-barbell.vue';
import baseInstruction from '../base/base-instruction.vue';
import baseInput from '../base/base-input.vue';
import baseBtn from '../base/base-btn.vue';
import choiceProgression from '../secondary-flows/choice-progression.vue';
export default {
name: 'addNewProgram',
components: {
bottomNav,
baseProgressionBarbell,
baseInstruction,
baseInput,
baseBtn,
choiceProgression
},
computed: {
progression () {
return this.$store.getters.getProgression('addnewprogram');
},
steps () {
return this.$store.getters.getSteps('addnewprogram');
},
currentStep () {
return this.steps[this.progression.current - 1];
},
currentStepValid () {
return this.currentStep.valid == false ? true : false;
},
stepOneValidation () {
this.steps[0].inputs.forEach(input => {
if (!input.value) {
return this.$set(this.steps[0], 'valid', false);
}
this.$set(this.steps[0], 'valid', true);
});
},
stepTwoChoices() {
return this.$store.getters.getChoices('addnewprogram', 1);
}
}
}
</script>
<style lang="sass" scoped>
#import '../../sass/_variables.sass'
.newprogram
display: flex
flex-direction: column
.container--newprogram
display: flex
flex-direction: column
height: 100%
padding: $s1
.newprogram--header
display: flex
justify-content: space-between
align-items: center
h1
padding: 0
.newprogram--content
display: flex
// justify-content: center
// align-items: center
height: 100%
padding-top: $s2
</style>
You are updating members of a computed item. Computed items aren't editable. You need a data item, or you need to write your changes to the $store and have them refresh from there.

VueJS adding transition on new data view

I want to add some transition effect on newly created element on VueJS. Like in the following code if I create a new list element using the input field, I want it to appear using 'fade' or 'slide-in' effect. I couldn't figure out it from the official documentation. how can I do that?
var vm = new Vue({
el: "#vue-instance",
data: {
newelement: '',
list: []
},
methods: {
addelement: function() {
this.list.push(this.newelement);
this.newelement = '';
}
}
});
<div id="vue-instance">
<input type="text" v-model="newelement" #keyup.enter="addelement">
<ul v-for="element in list">
<li>{{ element }}</li>
</ul>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.16/vue.js"></script>
According to official docs u need to wrap you loop with a transition element. And specify a name and a (optional) tag attributes.
Like so:
<transition-group name="list" tag="p">
<span v-for="item in list" class="list-item"></span>
</transition-group>
And add some css classes with name of transition:
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
More info at the https://v2.vuejs.org/v2/guide/transitions.html#List-Transitions