Vue: Child string prop wont render - javascript

Hi I'm new to vue and am trying to understand its one-way data bind model component registration and passing props.
In my index.js I have my parent component in which I want to render a single child right now
import Vue from 'vue'
import StyledTitle from './components/StyledTitle'
new Vue({
el: '#app',
components: {
StyledTitle,
},
})
Child Component is StyledTitle.js
import Vue from 'vue'
import styled from 'vue-styled-components'
const StyledTitle = styled.h1`
font-size: 1.5em;
text-align: center;
color: #ff4947;
&:hover,
&:focus {
color: #f07079;
}
`
Vue.component('styled-title', {
props: ['text'],
components: {
'styled-title': StyledTitle,
},
template: `<StyledTitle>{{ text }}</StyledTitle>`,
})
export default StyledTitle
Finally my HTML is which I expect to render a red Hi
<div id="app">
<styled-title text="Hi"></styled-title>
</div>
The HI is not showing up though and the props value is undefined. Coming to this from react so wondering why this isnt working, thanks!
Ps screenshot of my vue devtools

The issue is that your StyledTitle.js file exports a normal styled <h1> component which uses a default slot for its content instead of your custom component that accepts a text prop.
If you're still keen on using a prop-based component, you need to export that instead of the one from vue-styled-components. You should avoid global component registration in this case too.
For example
// StyledTitle.js
import styled from 'vue-styled-components'
// create a styled title locally
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: #ff4947;
&:hover,
&:focus {
color: #f07079;
}
`
// now export your custom wrapper component
export default {
name: 'StyledTitle',
props: ['text'],
components: {
Title, // locally register your styled Title as "Title"
},
template: `<Title>{{ text }}</Title>`,
})
Given your component doesn't maintain any state, you could make it purely functional. Using a render function will also help, especially if your Vue runtime doesn't include the template compiler (which is the default for most Vue CLI apps)
export default {
name: 'StyledTitle',
functional: true,
props: { text: String },
render: (h, { props }) => h(Title, props.text)
}

Related

Vue - Pass components to router-view on dynamically loaded component

I need to render a different layout for the same route for a specific URI with different components depending on the user being on mobile or in desktop.
I would like to avoid having route path checks in the PageCommon(layout component) to keep it clean.
The app has a main component taking care of the layout, it has different router-views where we load the different components for each page URI. This would be a normal route for that.
{
path: '',
component: PageCommon,
children: [
{
path: '',
name: 'Home',
components: {
default: Home,
header: Header,
'main-menu': MainMenu,
'page-content': PageContent,
footer: Footer,
'content-footer': ContentFooter
}
},
I can't change the route components property once the component is loaded so I tried to make a wrapper and pass the components dynamically.
{
path: 'my-view',
name: 'My_View',
component: () => import('#/components/MyView/ViewWrapper')
},
In /components/MyView/ViewWrapper'
<page-common v-if="isMobile">
<my-mobile-view is="default"></my-mobile-view>
<main-menu is="main-menu"></main-menu>
</page-common>
<page-common v-else>
<my-desktop-view is="default"></my-desktop-view>
<header is="header"></header>
<main-menu is="main-menu"></main-menu>
<footer is="footer"></footer>
</page-common>
</template>
I would expect that the components passed inside page-common block would be substituted on the appropriate , but is not how it works, and Vue just loads page-common component with empty router-views.
Is there any approach for this?
Note that I already tried using :is property for loading different components, but the problem then is on how to tell the parent to use this or that component for this page. This is the code for that:
<template>
<component :is="myView"></component>
</template>
<script>
import DesktopView from "#/components/MyView/DesktopView";
import MobileView from "#/components/MyView/MobileView";
export default {
name: 'MyView',
components: {
DesktopView,
MobileView,
},
data(){
return {
myView: null,
isMobile: this.detectMobile()
}
},
methods : {
getViewComponent() {
return this.isMobile ? 'mobile-view' : 'desktop-view';
}
},
created() {
this.myView = this.getViewComponent();
}
}
</script>
I could use this approach for each of the PageCommon router views, creating a component for each that does the above, but it looks like a very bad solution.
A computed method is all you need.
You should have this top level Logic in App.vue and the <router-view> should be placed in both DesktopView and MobileView.
// App.vue
<template>
<component :is="myView"></component>
</template>
<script>
import DesktopView from "#/components/MyView/DesktopView";
import MobileView from "#/components/MyView/MobileView";
export default {
name: 'MyView',
components: {
DesktopView,
MobileView,
},
computed: {
myView() {
return this.detectMobile() ? 'mobile-view' : 'desktop-view';
}
}
}
</script>
You may also want to consider code splitting by setting up Dynamic Components for those layouts since Mobile will load Desktop View because it is compiled into final build, register them globally as dynamic imports instead if importing them in MyView and then delete components also after doing the following instead, this way only the one that is needed will be downloaded saving mobile users their bandwidth:
// main.js
import LoadingDesktopComponent from '#/components/LoadingDesktopComponent '
Vue.componenet('desktop-view', () => ({
component: import('#/components/MyView/DesktopView'),
loading: LoadingDesktopComponent // Displayed while loading the Desktop View
})
// LoadingDesktopComponent .vue
<template>
<div>
Optimizing for Desktop, Please wait.
</div>
</template>
<script>
export default {
name: 'loading-component'
}
</script>
Routing logic will only be processed when <router-view> is available,this means you can delay the presentation of Vue Router, for example you can have :is show a splash screen like a loading screen on any URI before displaying a component in :is that contains <router-view>, only than at that point will the URI be processed to display the relevant content.

How to create a custom style property for a Vue.js component

I am trying to use Vue.js without needing a build step. But Vue doesn't have a style property.
So I had an idea to create a custom "style" property on my Vue component instance and then inject the content of this property in the DOM when the component is created or mounted.
The only problem is I can't understand how to do it. (I looked at the plugins docs). I would need to create some sort of a plugin that would first check if a "style" property exists and then take it and insert it in the DOM. Also I don't want to use the Vue.component() function because I want to import and export using ES6. Here is how the result would look like:
// MyComponent.js
export default {
template: `<div>My component</div>`,
style: `
.hello {
background: #ccc;
}
`,
}
// App.js
import MyComponent from './MyComponent.js'
new Vue({
el: '#app',
components: {
MyComponent
}
})
When MyComponent is created, it should take the value of the "style" property and add it to the DOM like this. Any ideas would be much appreciated.
$('body').append('<style>' + STYLE + '</style>')
Here is a plugin using Vue.component() function. But I don't want to use the component function.
https://github.com/NxtChg/pieces/tree/master/js/vue/vue-css
You can use computed inline styles using v-bind:style or just :style for short. It will map the given object to correct CSS styles. Use camelCase, i.e. backgroundColor instead of background-color.
If you want global style, you can inject a style tag into head using the mounted life-cycle hook. You should remove it again in destroyed.
EDIT: I misunderstood your post, updated answer
var app = new Vue({
el: '#app',
data: {
subject: 'World'
},
computed: {
subjectStyle() {
return {
color: 'yellow',
backgroundColor: 'rebeccapurple',
};
}
},
mounted() {
const css = `
body {
background-color: #eee;
font-family: 'Comic Sans MS', sans-serif;
}
`;
this.styleTag = document.createElement('style');
this.styleTag.appendChild(document.createTextNode(css));
document.head.appendChild(this.styleTag);
},
destroyed() {
this.styleTag.remove();
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
Hello, <span :style="subjectStyle">{{ subject }}</span>!
</div>
Below is some code for a plugin that will allow each Vue instance to specify some styling. The styling will be injected into <head> and removed again when the component is destroyed.
const StylePlugin = {
install(Vue) {
Vue.mixin({
mounted() {
const css = this.$options.style;
if (!css) return;
this.$styleTag = document.createElement('style');
this.$styleTag.appendChild(document.createTextNode(css));
document.head.appendChild(this.$styleTag);
},
destroyed() {
if (this.$styleTag) {
this.$styleTag.remove();
}
}
});
}
};
Vue.use(StylePlugin);
var app = new Vue({
el: '#app',
data: {
subject: 'World'
},
style: `
body {
background-color: rebeccapurple;
color: white;
font-family: 'Comic Sans MS', sans-serif;
}
`,
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
Hello, World
</div>

Vuejs how to pass data from main.js to App.vue with props

I'm having an issue where the App component and HelloWorld component are not getting passed data from main.js. This should be a rather simple thing to do in Vue.
You can see in the image that the root element has counter defined as 10, it's just not being populated in any of the child components. almost like line 12 in main.js is not taking any effect. If I click and it says 'counter: undefined'. What am I doing wrong? I've been beating my head against the wall for a few hours now.
Here is my main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
components: {App},
data: {
counter: 10
},
template: '<App :counter="counter" />',
//computed: {
// counterInc: function () {
// return this.counter++
// }
//},
methods: {
updateCounter (x) {
this.counter = x
}
},
render: h => h(App)
}).$mount('#app')
Here is my App.vue
<template>
<div id="app">
<img alt="logo" src="./assets/logo.png">
<HelloWorld msg="Our Message" :counter="counter"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'app',
props: ['counter'] ,
components: {
HelloWorld
},
}
</script>
<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>
Here is my helloworld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
Here lies all of our operations for automating some strenuous tasks. <br>
</p>
<h3>Get Started {{ counter }}</h3>
<ul>
<li><a v-on:click="updateCounter()" class="generateRollup">Generate Purchase Price</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String,
counter: String,
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #0093D0;
}
.generateRollup:hover {
cursor: pointer;
}
</style>
So I don't personally use the render function much, but what you can do to get your code working is supply the initial template in the actual html page and mount the Vue instance to it. I've made a codepen here : https://codepen.io/crustyjew/pen/jeWPgY
The essentials are to remove your render function, add the following to html
<div id="app">
<app :counter="counter" />
</div>
leaving .$mount('#app') to mount it to that html you provide.
If you don't want to use the template compiler-included vue build, then you can implement a render function like this to pass prop values:
render: h => h(App, {
props: {
'counter': 10
}
})
(Source: https://v2.vuejs.org/v2/guide/render-function.html#createElement-Arguments - the h argument of render functions is an alias for createElement.)
Matti gave you already an answer, but you may want to think about your project structure. It shouldn't be the goal to pass data from your root component to the lowest child component.
In your case you have two options:
Use events to emit an event from your component to update the state in another component. See https://v2.vuejs.org/v2/guide/components-custom-events.html for more information.
Use a state management like vuex. Vuex is used to handle you state globally. You can access the state with getters from all your components, without passing your data manually to each component, which needs access to the data. Furthermore vuex provides actions/mutations, which allows you to update the state. For more information see https://vuex.vuejs.org/.
For small projects vuex might be to much overhead for an equal result. But if you project gets bigger and bigger it's really hard to know what's going on in your components, when passing data through multiple components.

Using Global module object within Vue instance methods

I'm trying to create a global object I can use in Vue but having trouble accessing the methods. I want to use these methods in different components. Also, what's the best approach. I heard about using Mixins but I was thinking about trying a basic javascript object. I hope I'm asking this question the right way.
src/myobject.js
exports.myObj = () => {
function testMethod() {
console.log('working');
}
}
src/main.js
import Vue from 'vue'
import App from './App'
import { store } from './store/store'
import { myObj } from './myobject'
Vue.config.productionTip = false
myObj.testMethod() // NOT WORKING - TypeError: __WEBPACK_IMPORTED_MODULE_3_
/* eslint-disable no-new */
new Vue({
el: '#app',
store: store,
components: { App },
template: '<App/>'
})
src/components/App.vue
<template>
<div id="app">
<img src="./assets/logo.png">
</div>
</template>
<script>
export default {
name: 'App',
mounted: function () {
myObj.testMethod() // NOT WORKING
},
components: {
}
}
</script>
<style>
body {
margin: 0;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
</style>
To create a simple ES module that exports both named functions and a default object, you can define it like this:
export function testMethod() {
console.log('working');
}
export function otherMethod() {
console.log('other method');
}
// optionally export a default object
export default {
testMethod,
otherMethod
}
Then, it will be possible to import it:
import { testMethod } from './myobject';
// or import the default export
import myObj from './myobject';
myObj.testMethod();
Now, to use it in your Vue components, there are multiple ways which I already explained in another answer. Using Vue mixins is one way (beware of global mixins) and writing a plugin is another way.
In your case, it could be a simple mixin:
// my-mixin.js
import { testMethod } from './myobject';
export default {
mounted() {
testMethod();
}
}
Hook functions with the same name are merged into an array so that all of them will be called.
<script>
// components/App.vue
import MyMixin from '../my-mixin'
export default {
name: 'App',
mixins: [MyMixin],
mounted() {
console.log('both this function and the mixin one will be called');
},
components: {
}
}
</script>
The reason your code doesn't work is because you're exporting a function which does nothing. testMethod isn't exposed, it's just declared within the exported function as a local function.

Multiple components, but only first one rendered

I'm trying to create simple ToDo app using Ractive.js and Redux, but I ran into a problem with rendering more than one component on my page.
Javascript code:
import Ractive from 'ractive';
import template from './Home.html';
import './Home.less';
import { AddToDoForm, ToDoList } from '../';
export default Ractive.extend({
template,
data: {
message: 'This is the home page.',
},
components: {
AddToDoForm,
ToDoList
}
});
HTML of the component:
<AddToDoForm store={{store}} />
<ToDoList store={{store}} />
But only the first component is rendered. The store parameter I'm passing is the Redux store, but it doesn't work even without it.
I would add to verify defaults as a
...
components:{
AddToDoForm: AddToDoForm,
ToDoList: ToDoList
}
...
syntax examples (answer/31096635)

Categories