I recently started working with Vue.js and I really enjoy it but I have come across something that I just can't figure out how to do. I have searched and read the documentation but found no solution.
When you add a CSS class to any of your components Vue.js will automatically add or merge the CSS classes with the root element in your child component.
This is a nice feature but I need to deactivate this feature for a specific component since I want the classes to be added to a child element of the root element.
I made this fiddle to illustrate http://jsfiddle.net/rasmuswoelk/9m2j0a9s/3
<div id="demo">
<child-component class="some-class"></child-component>
</div>
(The "some-class" is being added automatically and adds a green background color)
How can I prevent Vue.js from automatically merging CSS classes?
Updated
I disagree that it is intuitive to have a class that is applied to a component be moved by that component to an inner element, so my recommendation is still: Pass it as a prop.
However, it is possible to do what you want to do, with some caveats. The component will have a list of classes that belong on the outer element. In mounted, it looks at $el.classList and finds the classes that are not among the known outer element classes. Those get applied to the inner element and removed from the outer element. Caveat: if one of the outer element classes is applied to the component, it will not be moved to the inner element. And any updates to the applied class will not be caught.
Vue.component('child-component', {
template: `<div :class="outerClasses"><h1 :class="childClasses">Hello</h1></div>`,
data() {
return {
outerClasses: ['child'],
childClasses: []
};
},
mounted() {
this.childClasses = Array.from(this.$el.classList).filter((c) => !this.outerClasses.includes(c));
for (const c of this.childClasses) this.$el.classList.remove(c);
}
});
var demo = new Vue({
el: '#demo'
});
.child {
border: 1px solid red;
padding: 20px;
text-align: center;
}
.some-class {
background-color: green;
}
<script src="//vuejs.org/js/vue.min.js"></script>
<div id="demo">
<child-component class="some-class a b c"></child-component>
</div>
I think that you will want to wrap the whole thing in another <div></div> as Vue will merge the classes on the top element. If you just want the style for <h1> then try doing this:
template: `<div><h1 class="child">Hello</h1></div>`
This should give you the expected behavior, let me know if it doesn't work.
You can't abstract away the wrapper div that you have in your template, and trying to make the component work in such as way as though that div doesn't exist will only lead to confusion and incorrect styling.
Let's say that you managed to get your component to apply the outer class to a nested element in its template (which is what you want). Now when people use that component and apply a class to it, the style is applied to the input element and it is as though the whole component consists solely of the input element and there is no such wrapper div (this is the behavior you want).
The problem with this is that some styles will work (like background-color, or any appearance style), but other styles that affect the layout won't work (like position: absolute, because it will cause the input element to be positioned relative to the wrapper div which is not what was expected).
People who use that component should know about how the template is structured so they can style it correctly, because in reality there is a wrapper div there which they need to take into account.
I should mention you might be able to abstract away the wrapper div by using web components or the shadow DOM, but that's out of context of Vue.
A more simpler way (imo) is to bind the incoming class attribute and dynamic class prop to the data and remove them from the vnode (virtual node) during the created() hook.
Vue.component('child-component', {
template: `
<div class="child">
<h1 :class="staticClass">Hello</h1>
</div>
`,
data() {
return {
staticClass: [],
}
},
created() {
// dynamic :class prop
this.staticClass.push(this.$vnode.data.class)
// delete if not applying on this.$el
delete this.$vnode.data.class
// static class attr
this.staticClass.push(this.$vnode.data.staticClass)
// delete if not applying on this.$el
delete this.$vnode.data.staticClass
},
});
var demo = new Vue({
el: '#demo',
data: {
dynamicClass: 'dynamic-class'
},
});
.child {
border: 1px solid red;
padding: 20px;
text-align: center;
}
.some-class {
background-color: green;
}
.dynamic-class {
border: 1px solid yellow;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.min.js"></script>
<div id="demo">
<child-component :class="dynamicClass" class="some-class"></child-component>
</div>
Related
Is there a way in Svelte to add styles that only affect the current component and any descendant components?
Svelte supports a native :global() selector wrapper which will declare styles for that selector in the global scope, but I am looking for something similar which only matches selectors in the current or any descendant components.
For example (REPL):
App.svelte
<script>
import C1 from './C1.svelte';
let name = 'world';
</script>
<div><C1>Hello {name}!</C1></div>
C1.svelte
<script>
import C2 from './C2.svelte';
let name = 'world';
</script>
<style>
:global(div) {
padding: 10px;
background-color: blue;
}
div {
background-color: red;
}
</style>
<div><C2><slot /></C2></div>
C2.svelte
<div><slot /></div>
In the above example, all three components receive the global styling from the middle child component, C1.svelte. I am looking for a way to do a sort of hybrid styling (not passing down styles to child components) to add "global-down" styles that only affect components downward in the component tree.
When the :global() selector wrapper is not used, matched nodes are assigned a unique class which the selector then targets, added to the selector during compilation. What I am asking/suggesting would be something like this:
:find(div) {
background-color: blue;
}
…where :find() similarly assigns a unique class to any HTML elements matched in the same or descending components. Is this possible?
You can scope styles to only child components by combining :global() with a scoped selector. For instance, the following selector will apply to all divs in any component that are the descendant of a div in this component.
<style>
div :global(div) {
padding: 10px;
background-color: blue;
}
</style>
The selector is transformed to something like this:
div.svelte-hash div { /* etc */ }
If you also want to also target top-level divs in this component, you could write the rule like this (though this may have CSS specificity implications):
<style>
div, div :global(div) {
padding: 10px;
background-color: blue;
}
</style>
At the moment I need to have 3 card components, all with three different colors. The colors are applied to the :before pseudo-element (and multiple other areas), and therefore the easiest solution I believe would be to apply this with a CSS variable / property.
At the moment, I instantiate a new CSS property / variable in the mounted() of my component, and this works fine with 1 card, but breaks with multiple. When I have multiple components, only the first one gets the color and the second one does not even get the color. It seems to overwrite the previous color with the first component, and then ignores the second one completely (no color property is shown in dev tools in the second one).
My question is, what would be the best solution to this problem? Is there a way to easily add a scoped CSS variable that does not override the other variables? Or would be the best course of action be to add all these styles in JavaScript?
Vue component
<template>
<div :class="'card ' + clss">
<div class="card-top">
<h3>{{ title }}</h3>
</div>
<div class="card-center">
<p>{{ message }}</p>
</div>
<div class="card-bottom">
Learn more
</div>
</div>
</template>
<script>
export default {
name: "Card",
props: ['clss', 'color', 'title', 'message'],
data() {
return {
}
},
mounted() {
var style = document.querySelector('.card').style;
style.setProperty(`--color`, this.color);
},
}
</script>
<style lang="scss" scoped>
// Example, not all styles are shown
.card:before {
content: "";
position: absolute;
z-index: -1;
top: -16px;
right: -16px;
background: var(--color); // Example of where I need this color
}
</style>
Vue with all components
<template>
<div class="grid grid-cols-12">
<!-- Green -->
<Card
title="Lorem1"
message="Lorem"
color="#00838d"
clss="col-span-4"
>
</Card>
<!-- Purple -->
<Card
title="Lorem2"
message="--bg-color"
color="#0066b2"
clss="col-span-4"
>
</Card>
</div>
</template>
You have to use this.$refs here instead of document.querySelector('.card').
The document.querySelector('.card') is taking the one first element on the page. Using refs you're picking a reference to DOM element inside of your card item. Please add the ref attribute to your div in the template and replace document.querySelector('.card') with reference to your div using this.$refs .
Reference: https://v3.vuejs.org/guide/component-template-refs.html
CSS are cascading, the variable should be applied to a hierarchy of elements. It's a bad sign that DOM is directly accessed in Vue component. document.querySelector queries all elements, regardless of component instance they belong to, also accesses only one that can be unrelated to current component instance.
If DOM elements need to be acccessed in a component, this is commonly done through refs:
<div :class="'card ' + clss" ref="card">
and
mounted() {
this.$refs.card.style.setProperty(`--color`, this.color);
},
CSS variables (custom properties) are primarily needed to provide a value for nested styles. This isn't needed if a style is specified in the same component:
<div :class="'card ' + clss" :style="{ background: color }">
I'm building JavaScript widgets that are supposed to be added onto other people's websites.
I style my widgets by dynamically adding a CSS to the pages they're on.
For example,
My CSS code below applies to a DIV inside my widget:
.myWidget { background-color: red; }
But a CSS file outside my own on a remote page might have:
div { border: 5px solid green; }
The CSS above would also apply to my widgets. How can I disable all other CSS outside my own?
Thanks
You could be Using shadow DOM
Shadow DOM MDN Web Docs
An important aspect of web components is encapsulation — being able to keep the markup structure, style, and behavior hidden and separate from other code on the page so that different parts do not clash, and the code can be kept nice and clean. The Shadow DOM API is a key part of this, providing a way to attach a hidden separated DOM to an element.
You can use the all shorthand property and the unset keyword to set each property's value to its initial value.
.myWidget {
all:unset;
background-color: red;
}
div {
background-color:yellow;
}
<div class="myWidget">Hello World!</div>
Using custom elements, is it possible to have an element's display set to none (or in any other way) before it renders? Consider that I am using this particular one as a base class for other elements, so applying a global style won't work.
The element is simple
<my-element></my-element>
And it doesn't do much :)
class MyElement extends HTMLElement {}
customElements.define('my-element', MyElement);
But a lot of others extend it
<my-other-element></my-other-element>
class MyOtherElement extends MyElement {}
customElements.define('my-other-element', MyOtherElement);
This is why I could not have a global css rule
my-element {
display:none;
}
I thought I got smart and added a shadow dom with a <style> and :host style but this just makes any other HTML I've placed inside my elements disappear.
Any ideas?
With the :defined pseudo class, which is part of the Web Components spec, you can apply styles whenever the custom element has been successfully defined.
my-element {
display: none;
}
my-element:defined {
display: block;
}
In styled-components docs, they have this example:
https://www.styled-components.com/docs/advanced#referring-to-other-components
It shows an Icon that changes color when you hover its parent, which is a link, in this case.
const Link = styled.a`
display: flex;
align-items: center;
padding: 5px 10px;
background: papayawhip;
color: palevioletred;
`;
const Icon = styled.svg`
flex: none;
transition: fill 0.25s;
width: 48px;
height: 48px;
${Link}:hover & { // <---- This is what I'm not understanding
fill: rebeccapurple;
}
`;
From the Docs, we know that:
Doc Note #1: styled-components solves this use case cleanly via the "component
selector" pattern. Whenever a component is created or wrapped by the
styled() factory function, it is also assigned a stable CSS class for
use in targeting.
And also that:
Doc Note #2: Ampersands (&) get replaced by our generated, unique classname for
that styled component, making it easy to have complex logic.
Let's analyze ${Link}:hover &
I know it gets translated into the browser as:
and:
I understand that sc-kAzzGY is the "stable CSS class" (Doc Note #1) that is created whenever an element is wrapped by the styled function.
I also know that the Ampersand (&) gets replaced by their generated unique classname (Doc Note #2) for that styled components. Hence, kDmLky is that class.
QUESTION
But what does the resulting selector (picture below) is actually selecting? Can anybody explain that to me?
${Link} is pointing to const Link i.e.: "Hovering my parent changes my style" which gets a class of sc-kAzzGY.
& is kinda like saying "And add this to the current class(es)/id(s)/etc."
So,
.my-class {
some-css: awesomeness;
&:hover {
more-css: extra-cool;
}
}
is equivalent to:
.my-class {
some-css: awesomeness;
}
.my-class:hover {
more-css: extra-cool;
}
Therefore, & points to the containing element const Icon i.e. the speech bubble and gets a class of kDmLky.
When Link is hovered, cause Icon to have fill: rebeccapurple
EDIT:
Just to clarify things a bit more:
When you have a declaration block inside of another declaration block like the example below, that inner declaration block becomes an independent one.
const Icon = styled.svg`
flex: none;
transition: fill 0.25s;
width: 48px;
height: 48px;
${Link}:hover & { // This declaraition block becomes an independent one
fill: rebeccapurple;
}
`;
And the result, in this case, is a declaration block with a selection that says:
When you have a class & which is descendent of the class ${Link} which is in the hover state, apply these rules:
fill: rebeccapurple;
NOTE: ${Link} refers to the Link class and & refers to the Icon class (svg).
${Link}:hover &
Here the ampersand & is the short hand way for referring to the top level component being defined, so I would read it as
${Link}:hover ${Icon}
i.e. it is referring to an Icon component contained inside of a Link component being hovered over
I would also recommend this link to see the more general use case for component selectors with styled components, where it's used in a parent child configuration for selection, and applied to the child