How to change VueJS's <slot> content before component creation - javascript

I have a VueJS component,
comp.vue:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
data () {
return {
}
},
}
</script>
And I call this Vue component just like any other component:
...
<comp>as a title</comp>
<comp>as a paragraph</comp>
...
I would like to change comp.vue's slot before it is rendered so that if the slot contains the word "title" then the slot will be enclosed into an <h1>, resulting in
<h1>as a title</h1>
And if the slot contains "paragraph" then the slot will be enclosed in <p>, resulting in
<p>as a paragraph</p>
How do I change the component slot content before it is rendered?

This is easier to achieve if you use a string prop instead of a slot, but then using the component in a template can become messy if the content is long.
If you write the render function by hand then you have more control over how the component should be rendered:
export default {
render(h) {
const slot = this.$slots.default[0]
return /title/i.test(slot.text)
? h('h1', [slot])
: /paragraph/i.test(slot.text)
? h('p', [slot])
: slot
}
}
The above render function only works provided that the default slot has only one text child (I don't know what your requirements are outside of what was presented in the question).

You can use $slots(https://v2.vuejs.org/v2/api/#vm-slots):
export default {
methods: {
changeSlotStructure() {
let slot = this.$slots.default;
slot.map((x, i) => {
if(x.text.includes('title')) {
this.$slots.default[i].tag = "h1"
} else if(x.text.includes('paragraph')) {
this.$slots.default[i].tag = "p"
}
})
}
},
created() {
this.changeSlotStructure()
}
}

Related

How to get text content of slot?

We can have stencilJS element with slot as below
<my-component>123</my-component>
I'm trying to get the value of 123 from my render method itself, wondering if that is possible?
#Component({ tag: 'my-component' })
export class MyComponent {
render() {
return (
<div><slot /></div>
)
}
}
I would like to do some string formatting on 123 instead of rendering slot directly
import { Element } from '#stencil/core';
#Component({ tag: 'my-component' })
export class MyComponent {
/**
* Reference to host element
*/
#Element() host: HTMLElement;
componentWillRender() {
console.log(this.host.innerHTML)
}
render() {
return (
<div><slot /></div>
)
}
}
In web components, the content inside of it is part of the main DOM, too. This content is not going to show if you don't use slots; but, the content is going to project next to the #shadow-root anyway (check it using the chrome developer tools in the "elements" section).
So, if you do not want to show the content using default slots, you can use the property decorator #Element() and declare a property of type HTMLElement:
Then, you can access to the content via innerHTML or innerText.
Finally, you can format the content. Check the code snippet bellow:
import { Component, Element, h } from "#stencil/core";
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Element() element: HTMLElement;
formatContent(content: any) {
if ( isNaN(content)){
// Your format here
return content;
} else {
return content + '.00';
}
}
render() {
return [
// Commented slot tag
// <slot></slot>,
<div> {this.formatContent(this.element.innerHTML)} </div>
];
}
}
Using three times the web component with 2 strings and a number as a entry data, the result should be:
My text
My text 2
123.00

How to re-render a component (Vue) with its data (and state) intact?

Here's how my code is structured: parent component shuffles through child components via v-if directives, one of the child components is using a state to define its data. Everything works except when I switch between the child components. When I get back, no data can be shown because the state has become null.
Parent component:
<template>
<div>
<Welcome v-if="view==0" />
<Courses v-if="view==1" /> //the component that I'm working on
<Platforms v-if="view==2" />
</div>
</template>
Courses component:
<template>
<div>Content</div>
</template>
<script>
export default {
name: 'Courses',
computed: {
...mapState([
'courses'
])
},
data () {
return {
courseList: [],
len: Number,
}
},
created () {
console.log("state.courses:")
console.log(this.courses)
this.courseList = this.courses
this.len = this.courses.length
},
}
</script>
Let say the default value for "view" is 1, when I load the page, the "Courses" component will be shown (complete with the data). If I click a button to change the value of "view" to 0, the "Welcome" component is shown. However, when I tried to go back to the "Courses" component, the courses component is rendered but is missing all the data.
Upon inspection (via console logging), I found that when the "Courses" component was initially rendered, the state was mapped correctly and I could use it, but if I changed the "view" to another value to render another component and then changed it back to the original value, the "Courses" component still renders but the state became undefined or null.
EDIT: Clarification.
Set courseList to a component name and use <component>
and set some enum
eg.
<template>
<component :is="this.viewState" />
</template>
<script>
export default {
// your stuff
data() {
return {
viewState: 'Welcome',
}
},
methods: {
getComponent(stateNum) {
const States = { '1': 'Welcome', '2': 'Courses', '3': 'Platforms' }
Object.freeze(States)
return States[stateNum]
}
},
created() {
// do your stuff here
const view = someTask() // someTask because I don't get it from where you're getting data
this.viewState = this.getComponent(view)
}
}
</script>
I don't actually understood correctly but here I gave some idea for approaching your problem.

Vue binding ref in renderless component

I have this problem where binding ref in renderless component won't work. I've tried adding class within the instance object and the class binding work but the ref didn't. I also tried logging this.$refs and it just returns an empty object.
App.vue
<template>
<div id="app">
<renderless>
<div slot-scope="{ instance, logger }">
<div v-bind="instance"></div>
<button #click="logger('One')">One</button>
<button #click="logger('Two')">Two</button>
</div>
</renderless>
</div>
</template>
<script>
import Renderless from "./components/Renderless";
export default {
components: {
Renderless
},
};
</script>
components/Renderless.vue
<script>
export default {
render(h) {
return this.$scopedSlots.default({
instance: {
ref: 'container'
},
})
},
mounted() {
this.init()
},
methods: {
init() {
console.log(this.$refs)
},
logger(value) {
console.log(value)
},
},
};
</script>
How can I bind the ref so that the child component know what element to target or any other better solutions/suggestions?
BTW code is also available on codesandbox.
You can use querySelector and querySelectorAll to pick out what you want within the child component. this.$el should provide the based element of the child class once the component has mounted.
init () {
const buttons = this.$el.querySelectorAll('button');
buttons.forEach(button => {
// access each button
});
// fetches an item by element id
const itemById = this.$el.querySelector('#elementid');
// fetches by classes
const itemsByClass = this.$el.querySelector('.className');
}

Change variable in component using vue.js

I have navbar blade, component with text and another components with page.
It works like I have component with text in navbar, and another component after navbar. That's three another components. How to change text from for example index.vue in text.vue?
That's what I have:
Text.vue:
<template>
<p class="title">{{msg}}</p>
</template>
<script>
export default {
props: [
'msg',
],
data() {
return {
}
},
mounted() {
},
methods: {
}
}
</script>
Component in navbar.blade.php:
<navbar-title></navbar-title>
And I try to change it in index.vue, that should work when we are on this page:
data() {
return {
msg: 'text',
}
But it doesn't work. How to do it correctly?
EDIT:
Vue.component('title', require('./components/Title.vue'));
To pass the message variable from your index.vue through navbar.vue to title.vue each needs to pass the property to the child and each child must pass the property on again all throughout the tree.
Something like this should work for your case: <title :msg="msg"></title>

vue.js passing data from parent single file component to child

Using single file architecture I'm trying to pass data (an object) from a parent component to a child:
App.vue
<template>
<div id="app">
<app-header app-content={{app_content}}></app-header>
</div>
</template>
<script>
import appHeader from './components/appHeader'
import {content} from './content/content.js'
export default {
components: {
appHeader
},
data: () => {
return {
app_content: content
}
}
}
</script>
appHeader.vue
<template>
<header id="header">
<h1>{{ app_content }}</h1>
</header>
</template>
<script>
export default {
data: () => {
return {
// nothing
}
},
props: ['app_content'],
created: () => {
console.log(app_content) // undefined
}
}
</script>
Seems to be such a trivial task and probably the solution is quite simple. Thanks for any advice :)
You're almost there.
In order to send the app_content variable from App.vue to the child component you have to pass it as an attribute in the template like so:
<app-header :app-content="app_content"></app-header>
Now, in order to get the :app-component property inside appHeader.vue you will have to rename your prop from app_component to appComponent (this is Vue's convention of passing properties).
Finally, to print it inside child's template just change to: {{ appContent }}

Categories