How to set class attribute on vuejs functional component - javascript

I have a vuejs functional component that mimics a div behavior. To do so, I set it's class based on the props that it receives.
Something like this:
<MyDiv textAlign="left">Div with left aligned text</MyDiv>
Becomes:
<div class="text-left">Div with left aligned text</div>
However, if MyDiv component is the root element for some other component, like:
Card.vue
<template>
<MyDiv display="inline-block" textAlign="center">
<slot />
</MyDiv>
</template>
And if Card receive any class attribute when used, I can't set the class attribute value based on MyDiv props, instead, class (on MyDiv) is overridden by the class received by Card.
So this:
<Card class="cool-style">Card content</Card>
Becomes:
<div class="cool-style">Card content</div>
Ant not this (what I need):
<div class="cool-style inline-block text-center">Card content</div>
Here's MyDiv component:
MyDiv.vue
export default {
name: 'my-div',
functional: true,
props: {
textAlign: {
type: String,
allowed: ['left', 'right', 'center'],
default: null
},
display: {
type: String,
allowed: ['hidden', 'block', 'inline-block', 'flex'],
default: null
}
},
render: function(createElement, context) {
let classes = [];
// Adds parent class to itself
if (typeof context.data.staticClass == 'string') {
classes.push(context.data.staticClass);
}
let classes = classes.concat([
'my-div',
context.props.textAlign && `text-${context.props.textAlign}`,
context.props.display && context.props.display
]);
classes = classes.filter(elm => elm != null);
classes = classes.join(' ').trim();
return createElement(
props.type,
{
attrs: {
class: classes
}
},
context.children
);
}
};
TL;DR
How can I force my custom class to a vue functional component, when it's parent have already a class being set?
As a side note, it can be done using statefull (non-functional and regular) components.

Replace
attrs: {
class: classes
}
by
class: classes
Side note: Not really needed to join the array into a string, you can pass the array.

Related

Vue Prop Sync on multiple levels (TypeScript & JavaScript & ClassStyle)

I just started programming with Vue and ts/js so please forgive me if I miss something obvious :)
I am trying to pass a prop down two levels. If level 3 modifies the prop it also changes on level 2 but not on level 1.
Level 1 is a component I wrote and it is written in ts.
<template>
<Child :varr.sync="obj.attr" />
</template>
export default Parent class extends Vue {
obj: Object = {
attr: [1, 2, 3]
};
}
Level 2 is a component I wrote and it is written in ts.
<template>
<ChildsChild :layout.sync="arr" />
</template>
export default Child class extends Vue {
#PropSync("varr", { type: Array }) arr!: number[];
}
Level 3 is a component I DID NOT write and it is written in js.
export default {
props: {
layout: {
type: Array,
required: true,
}
}
}
When you pass something down to a child, you need to be careful to not update the prop directly on the child, because this is actually re-rendered every time the parent updates.
In your case, I'm not really sure why the PropSync isn't working. The similar pattern I've seen before is separating out the prop and adding a watcher as below
i.e.
Parent
<template>
<Child :varr.sync="obj.attr" />
</template>
export default Parent class extends Vue {
obj: Object = {
attr: [1, 2, 3]
};
}
Child:
<template>
<ChildsChild :layout.sync="passed_varr" />
</template>
export default Child class extends Vue {
props {
varr: {
type: Object
}
}
data: instance => ({
passed_varr: instance.varr,
}
watch: {
passed_varr: {
handler() {
this.$emit('update:varr', this.passed_varr);
},
deep: true,
},
},
}
In this case, the prop varr is synced with the parent component, so when layout is updated in the grandchild, it syncs with child.passed_varr which has a watcher to emit an update to parent.varr
Looking at the docs here it seems like PropSync is doing a similar thing, so not sure on the particular differences

How do I pass complex styles (i.e. stylesheets) to React components as props?

I'm working on a large React project where each member of the team has been making components and stylesheets separately. I'm trying to find the common elements and re-write the code, creating re-usable components. At the moment each of these components has a stylesheet -- SCSS -- already written.
What I'd like to do is be able to pass styles to the component so that it can be customised (somewhat) in different locations. I know how to do this for the top-level HTML element in the component
export default class BoxWithSliderAndChevron extends Component {
render() {
const {
props: {
styles
},
} = this;
return (
<div className="BoxWithSliderAndChevron-main" style={styles}>
but as I understand it, these styles will only apply to this outer div? How can I pass styles such that I can re-style elements further down in the component's structure, using their classNames? As if I were passing a new stylesheet that would override the default stylesheet?
I suppose I could pass a number of style objects, but that seems cumbersome -- I'm wondering if there is a simpler way?
What you are trying to achieve kinda goes against the whole idea of inline styles (non-global, non-separated from implementation, etc), however you are right, passing a style prop and trying to apply it to a div will inmediatly result to only the parent having the styles applied.
One suggestion would be to merge the component styles with the props, ex:
import { StyleSheet } from 'react-native';
class Foo extends React.PureComponent {
render() {
return (
<div style={StyleSheet.merge([styles.parentStyle, styles.parentStyle])}>
<div style={StyleSheet.merge([styles.childStyle, styles.childStyle])}>
</div>
)
}
}
const styles = StyleSheet.create({
parentStyle: {
backgroundColor: 'red'
},
childStyle: {
backgroundColor: 'blue'
}
});
It is tedious work, but it is basically what you are trying to achieve, another approach is having theming globally applied:
import { StyleSheet } from 'react-native';
import { t } from '../theming'; // <- You switch themes on runtime
class Foo extends React.PureComponent {
render() {
return (
<div style={StyleSheet.merge([styles.parentStyle, t().parentStyle])}>
<div style={StyleSheet.merge([styles.childStyle, t().childStyle])}/>
</div>
)
}
}
const styles = StyleSheet.create({
parentStyle: {
backgroundColor: 'red'
},
childStyle: {
backgroundColor: 'blue'
}
});
/// Theming file would be something like:
// PSEUDO IMPLEMENTATION
import theme1 from 'theme1.json';
import theme2 from 'theme2.json';
availableThemes = {
theme1,
theme2
}
currentTheme = availableThemes.theme1
function setTheme(theme) {
currentTheme = availableThemes[theme]
}
export function t() {
return current theme
}

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

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()
}
}

Vue.js Changing props

I'm a bit confused about how to change properties inside components, let's say I have the following component:
{
props: {
visible: {
type: Boolean,
default: true
}
},
methods: {
hide() {
this.visible = false;
}
}
}
Although it works, it would give the following warning:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "visible"
(found in component )
Now I'm wondering what the best way to handle this is, obviously the visible property is passed in when created the component in the DOM: <Foo :visible="false"></Foo>
Referencing the code in your fiddle
Somehow, you should decide on one place for the state to live, not two. I don't know whether it's more appropriate to have it just in the Alert or just in it's parent for your use case, but you should pick one.
How to decide where state lives
Does the parent or any sibling component depend on the state?
Yes: Then it should be in the parent (or in some external state management)
No: Then it's easier to have it in the state of the component itself
Kinda both: See below
In some rare cases, you may want a combination. Perhaps you want to give both parent and child the ability to hide the child. Then you should have state in both parent and child (so you don't have to edit the child's props inside child).
For example, child can be visible if: visible && state_visible, where visible comes from props and reflects a value in the parent's state, and state_visible is from the child's state.
I'm not sure if this is the behavour that you want, but here is a snippet. I would kinda assume you actually want to just call the toggleAlert of the parent component when you click on the child.
var Alert = Vue.component('alert', {
template: `
<div class="alert" v-if="visible && state_visible">
Alert<br>
<span v-on:click="close">Close me</span>
</div>`,
props: {
visible: {
required: true,
type: Boolean,
default: false
}
},
data: function() {
return {
state_visible: true
};
},
methods: {
close() {
console.log('Clock this');
this.state_visible = false;
}
}
});
var demo = new Vue({
el: '#demo',
components: {
'alert': Alert
},
data: {
hasAlerts: false
},
methods: {
toggleAlert() {
this.hasAlerts = !this.hasAlerts
}
}
})
.alert {
background-color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo" v-cloak>
<alert :visible="hasAlerts"></alert>
<span v-on:click="toggleAlert">Toggle alerts</span>
</div>
According to the Vue.js component doc:
When the parent property updates, it will flow down to the child, but not the other way around. So, how do we communicate back to the parent when something happens? This is where Vue’s custom event system comes in.
Use $emit('my-event) from the child to send an event to the parent. Receive the event on the child declaration inside the parent with v-on:my-event (or #my-event).
Working example:
// child
Vue.component('child', {
template: '<div><p>Child</p> <button #click="hide">Hide</button></div>',
methods: {
hide () {
this.$emit('child-hide-event')
}
},
})
// parent
new Vue({
el: '#app',
data: {
childVisible: true
},
methods: {
childHide () {
this.childVisible = false
},
childShow () {
this.childVisible = true
}
}
})
.box {
border: solid 1px grey;
padding: 16px;
}
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app" class="box">
<p>Parent | childVisible: {{ childVisible }}</p>
<button #click="childHide">Hide</button>
<button #click="childShow">Show</button>
<p> </p>
<child #child-hide-event="childHide" v-if="childVisible" class="box"></child>
</div>
If the prop is only useful for this child component, give the child a prop like initialVisible, and a data like mutableVisible, and in the created hook (which is called when the component's data structure is assembled), simply this.mutableVisible = this.initialVisible.
If the prop is shared by other children of the parent component, you'll need to make it the parent's data to make it available for all children. Then in the child, this.$emit('visibleChanged', currentVisible) to notify the parent to change visible. In parent's template, use <ThatChild ... :visibleChanged="setVisible" ...>. Take a look at the guide: https://v2.vuejs.org/v2/guide/components.html
After a read of your latest comments it seems that you are concerned about having the logic to show/hide the alerts on the parent. Therefore I would suggest the following:
parent
# template
<alert :alert-visible="alertVisible"></alert>
# script
data () {
alertVisible: false,
...
},
...
Then on the child alert you would $watch the value of the prop and move all logic into the alert:
child (alert)
# script
data: {
visible: false,
...
},
methods: {
hide () {
this.visible = false
},
show () {
this.visible = true
},
...
},
props: [
'alertVisible',
],
watch: {
alertVisible () {
if (this.alertVisible && !this.visible) this.show()
else if (!this.alertVisible && this.visible) this.hide()
},
...
},
...
To help anybody, I was facing the same issue. I just changed my var that was inside v-model="" from props array to data. Remember the difference between props and data, im my case that was not a problem changing it, you should weight your decision.
E.g.:
<v-dialog v-model="dialog" fullscreen hide-overlay transition="dialog-bottom-transition">
Before:
export default {
data: function () {
return {
any-vars: false
}
},
props: {
dialog: false,
notifications: false,
sound: false,
widgets: false
},
methods: {
open: function () {
var vm = this;
vm.dialog = true;
}
}
}
After:
export default {
data: function () {
return {
dialog: false
}
},
props: {
notifications: false,
sound: false,
widgets: false
},
methods: {
open: function () {
var vm = this;
vm.dialog = true;
}
}
}
Maybe it looks like on hack and violates the concept of a single data source, but its work)
This solution is creating local proxy variable and inherit data from props. Next work with proxy variable.
Vue.component("vote", {
data: function() {
return {
like_: this.like,
dislike_: this.dislike,
}
},
props: {
like: {
type: [String, Number],
default: 0
},
dislike: {
type: [String, Number],
default: 0
},
item: {
type: Object
}
},
template: '<div class="tm-voteing"><span class="tm-vote tm-vote-like" #click="onVote(item, \'like\')"><span class="fa tm-icon"></span><span class="tm-vote-count">{{like_}}</span></span><span class="tm-vote tm-vote-dislike" #click="onVote(item, \'dislike\')"><span class="fa tm-icon"></span><span class="tm-vote-count">{{dislike_}}</span></span></div>',
methods: {
onVote: function(data, action) {
var $this = this;
// instead of jquery ajax can be axios or vue-resource
$.ajax({
method: "POST",
url: "/api/vote/vote",
data: {id: data.id, action: action},
success: function(response) {
if(response.status === "insert") {
$this[action + "_"] = Number($this[action + "_"]) + 1;
} else {
$this[action + "_"] = Number($this[action + "_"]) - 1;
}
},
error: function(response) {
console.error(response);
}
});
}
}
});
use component and pass props
<vote :like="item.vote_like" :dislike="item.vote_dislike" :item="item"></vote>
I wonder why it is missed by others when the warning has a hint
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "visible" (found in component )
Try creating a computed property out of the prop received in the child component as
computed: {
isVisible => this.visible
}
And use this computed in your child component as well as to emit the changes to your parent.

React extends defautProps

I am still on the way of the conquest React. I prefer to use a es6 component-based approach when creating React classes. And I detained at the moment when it is necessary to inheritance of some existing class with already defined defaultProps static property.
import {Component} from 'react';
class MyBox extends Component {
}
// Define defaultProps for MyBox
MyBox.defaultProps = {
onEmptyMessage: 'Nothing at here'
}
When I define a static property defaultProps of class that extend MyBox, it completely overwrites defaultProps of parent class.
class MyItems extends MyBox {
render() {
// this.props.onEmptyMessage is undefined here
// but this.props.onRemoveMessage is present
return <i>{this.props.onEmptyMessage}</i>; //<i></i>
}
}
// Define defaultProps for MyItems
MyItems.defaultProps = {
onRemoveMessage: 'Are you sure?'
}
But i need to extend defaultProps of parent class, not overwrite. I understand that is possible by extending directly defaultProps property of parent class.
MyItems.defaultProps = _.extend(MyBox.defaultProps, {
onRemoveMessage: 'Are you sure?'
});
But i think that such trick is dirty. Is there a way to perform it according to React plan?
In my opinion you the issue is not how you can merge the properties but how you can compose it without extending. You would have your components like this:
class MyBox extends Component {
static defaultProps = { title: 'Box title' }
render() {
return (
<div className={'mybox ' + this.props.className}>
<h1>{this.props.title}</h1>
{this.prop.children}
</div>
)
}
}
class MyItems extends Component {
render() {
return (
<MyBox className="itembox" title="Items title">
<i>{this.props.children}</i>
</MyBox>
)
}
}
Then use it like that
<MyBox>
box
</MyBox>
or
<MyItems>
items box
</MyItems>
That will render:
<div class="mybox">
<h1>Box title</h1>
box
</div>
or
<div class="mybox itembox">
<h1>Items title</h1>
<i>items box</i>
</div>
It looks like you have extending/overriding the className or the title, but you have composed. If you use this way of building your UI, you'll see it will be easier and faster

Categories