Can web components handle CSS (cssChangedCallback?) - javascript

Is there any way to handle how CSS is applied to a web component like you can do with attributes using attributeChangedCallback.
I am working on a couple web components that would benefit from being styled with CSS classes, but I need to change multiple styles for it to look correct (e.g. if you set the color of the control, the user would expect the border color of one element and the font color of another to change in the shadow DOM).
Is there any way to get .usingCSS { color: red; } to change the color of the toggle switch in the following simple web component example?
// based on https://www.w3schools.com/howto/howto_css_switch.asp
class W3schoolsToggleSwitch extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({ mode: "open" });
this.span = document.createElement("span");
this.span.innerHTML = `
<style>
/* The switch - the box around the slider */
.switch {
--color: #2196F3;
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: var(--color);
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
</style>
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
`;
shadow.appendChild(this.span);
}
static get observedAttributes() {
return ["color"];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name, newValue);
if ("color" === name) {
this.shadowRoot
.querySelector(".switch")
.style.setProperty("--color", newValue);
}
}
get color() {
return this.getAttribute("color");
}
set color(value) {
return this.setAttribute("color", value);
}
}
customElements.define("w3schools-toggle-switch", W3schoolsToggleSwitch);
.usingCSS {
color: red;
}
default:
<w3schools-toggle-switch></w3schools-toggle-switch>
<br><br> color attribute used to change the color to green:
<w3schools-toggle-switch color="green"></w3schools-toggle-switch>
<br><br> can you change the color with CSS?:
<w3schools-toggle-switch class="usingCSS"></w3schools-toggle-switch>

From the outside with <link>
You could apply CSS style to a Web Component uning a <link> element in the Shadow DOM.
#shadow-root
<link rel="stylesheet" href="default.css">
attributeChangedCallback( name, old, value ) {
if (name === 'class')
this.shadowRoot.querySelector( 'link' ).href = value + ".css"
}
With style defined inside Shadow DOM :host() pseudo-class function
You can apply different styles based on the context. You can combine multiple classes.
customElements.define( 'custom-element', class extends HTMLElement {
constructor() {
super()
this.attachShadow( { mode: 'open' } )
.innerHTML = `
<style>
:host( .red ) { color: red }
:host( .blue ) { color: blue }
:host( .border ) { border: 1px solid }
</style>
Hello`
}
} )
ce1.onclick = ev => ev.target.classList.add( 'border' )
<custom-element class="red" id="ce1"></custom-element>
<custom-element class="blue border"></custom-element>
On Chrome / Opera: with Constructable stylesheets
Create one (or several) Stylesheet(s) and apply it(them) to the Shadow DOM. You can apply multiple stylesheets to the same Shadow DOM.
var ss = []
ss['red'] = new CSSStyleSheet
ss.red.replaceSync( 'span { color: red }' )
ss['green'] = new CSSStyleSheet
ss.green.replaceSync( 'span { color: green }' )
ss['border'] = new CSSStyleSheet
ss.border.replaceSync( 'span { border: 1px solid }' )
customElements.define( 'custom-element', class extends HTMLElement {
constructor() {
super()
this.attachShadow( { mode: 'open' } )
.innerHTML = `<span>Hello</span>`
}
static get observedAttributes() { return [ 'class' ] }
attributeChangedCallback() {
this.shadowRoot.adoptedStyleSheets = [ ...this.classList ].map( cl => ss[ cl ] )
}
} )
ce1.onclick = ev => ev.target.classList.add( 'border' )
<custom-element class="red" id="ce1"></custom-element>
<custom-element class="green border"></custom-element>

Extending on Supersharps answer.
when you can not use Constructable Stylesheets yet:
You could (brutally) import a whole STYLE definition from the Host document.
onload=this.disabled=true to prevent styling the document DOM
or create a <my-themes></my-themes> Component that hosts (and serves) the STYLE elements
customElements.define( 'custom-element', class extends HTMLElement {
constructor() {
super()
this.root=this.attachShadow( { mode: 'open' } );
this.root.innerHTML = `<style>div{font-size:40px}</style>`
+`<style id="theme"></style><div>Click Me</div>`;
let themes = window.themes;//duplicate IDs create a global NodeList
let themeNr = 0;
this.root.addEventListener('click', ev =>
this.theme = themes[ themeNr<themes.length ? themeNr++ : themeNr=0 ].innerHTML);
}
set theme(css){
this.root.getElementById('theme').innerHTML = css;
}
} )
<style id="themes" onload="this.disabled=true">
div{
background:yellow;
}
</style>
<style id="themes" onload="this.disabled=true">
div{
background:hotpink;
font-size:30px;
}
</style>
<style id="themes" onload="this.disabled=true">
div{
background:red;
color:white;
}
div::after{
content:" theme2"
}
</style>
<custom-element></custom-element>
<custom-element></custom-element>
<custom-element></custom-element>
<div>Main Document</div>

Related

Function to Open and Close the Overlay using single button element

I would like to open and close overlay using single button, so when the button is clicked an additional class is added, when closed the class is removed and overlay is closed.
So far I wrote the code that opens overlay and add/remove the class to the button.
Also I've created the method to close the overlay but I'm struggling to create a proper event to actually close it, so I would be happy if anyone can guide me a bit.
I think there should be an 'if' statement within the events() checking if the button have added class, if so, the overlay will be closed using this function element.classList.contains("active");
Also the button is animated, so when class is added 3 bars (hamburger icon) becomes X and this is the main reason I don't want to have separate buttons to open and close, I already achieved that but this is not what I'm looking for.
class OverlayNav {
constructor() {
this.injectHTML()
this.hamburgerIcon = document.querySelector(".menu-icon")
this.events()
}
events() {
this.hamburgerIcon.addEventListener("click", () => this.overlayOpen())
}
overlayOpen() {
document.getElementById("myNav").style.width = "100%";
this.hamburgerIcon.classList.toggle("menu-icon--close-x")
}
overlayClose() {
document.getElementById("myNav").style.width = "0%";
}
injectHTML() {
document.body.insertAdjacentHTML('beforeend', `
<div id="myNav" class="overlay">
<p>My Overlay</p>
</div>
`)
}
}
export default OverlayNav
You can make a function with a if statement handle Opening and closing the overlay
Here is your code edited
class OverlayNav {
constructor() {
this.injectHTML();
this.hamburgerIcon = document.querySelector(".menu-icon");
this.events();
}
events() {
this.hamburgerIcon.addEventListener("click", () => this.overlayHandle());
}
overlayOpen() {
document.getElementById("myNav").style.width = "100%";
this.hamburgerIcon.classList.toggle("menu-icon--close-x");
}
overlayClose() {
document.getElementById("myNav").style.width = "0%";
}
overlayHandle() {
if (element.classList.contains("active")) {
this.overlayClose();
} else {
this.overlayOpen();
}
}
injectHTML() {
document.body.insertAdjacentHTML(
"beforeend",
`
<div id="myNav" class="overlay">
<p>My Overlay</p>
</div>
`
);
}
}
export default OverlayNav;
You can add a property that keeps track of the state of the nav bar.
constructor() {
this.injectHTML()
this.hamburgerIcon = document.querySelector(".menu-icon")
this.events()
this.overlayVisible=true;
}
Then add a method that toggles the state and calls the right open/close-method:
toggleOverlay() {
if (this.overlayVisible)
this.overlayOpen();
else
this.overlayClose();
this.overlayVisible=!this.overlayVisible;
}
Finally make the events method call toggleOverlay() instead of overlayOpen().
events() {
this.hamburgerIcon.addEventListener("click", () => this.toggleOverlay())
}
Alternativly, a pure HTML + CSS solution, using only the details element and the [open] CSS attribute selector.
.overlay > p {
padding: 1rem;
margin: 0;
display: flex;
flex-direction: column;
width: 25vw
}
.overlay summary {
padding: 1rem 0.5rem;
cursor: pointer;
max-height: 90vh;
overflow: auto;
font-size: 4em;
list-style: none;
}
.overlay[open] summary {
background: black;
color: white;
padding: 0.5rem;
font-size: 1em;
}
.overlay[open] {
position: fixed;
/* top: calc(50% - 25vw); */
left: calc(50% - 15vw);
outline: 5000px #00000090 solid;
border: 5px red solid;
border-radius: 0.5rem;
font-size: 1em
}
.overlay[open] summary::after {
content: '❌';
float: right;
}
<details class="overlay">
<summary>☰</summary>
<p>
Hello world!
</p>
</details>

Dark Mode not working during loading time

I'm trying to make a dark mode toggle button which can toggle between dark and light mode on click, User preference is also stored using localStorage. The user should manually press the button to toggle to other mode. If the user's choice is dark mode, Every page will be in dark mode and it doesn't turn to light mode on refreshing. Everything looks fine upto now but the real issue comes with loading time. The load time of a page is nearly 1 second and in that time, Page appears to be in light mode even if user's choice is dark mode. I don't want that to happen. I want loading time section in dark mode if user's choice is dark.
This is my current code:
<script>
const body = document.querySelector('body');
function toggleDark() {
if (body.classList.contains('dark')) {
body.classList.remove('dark');
localStorage.setItem("theme", "light");
} else {
body.classList.add('dark');
localStorage.setItem("theme", "dark");
}
}
if (localStorage.getItem("theme") === "dark") {
body.classList.add('dark');
}
</script>
<style>
body {background-color: #ffffff}
body.dark {background-color: #000000; color: #ffffff}
</style>
<button class="dark-mode" id="btn-id" onclick="toggleDark()"></button>
Another alternative is to load the script in the <head> element, and toggle the class on html element
To do so, you use document.documentElement.classList as that is the HTML element
Then change your CSS to
html.dark body {}
etc .. the class selector on HTML
html body {background-color: #ffffff}
html.dark body {background-color: #000000; color: #ffffff}
<script>
const body = document.querySelector('body');
function toggleDark() {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
//localStorage.setItem("theme", "light");
} else {
document.documentElement.classList.add('dark');
//localStorage.setItem("theme", "dark");
}
}
//if (localStorage.getItem("theme") === "dark") {
document.documentElement.classList.add('dark');
//}
</script>
<button class="dark-mode" id="btn-id" onclick="toggleDark()">DARK</button>
Due to restrictions, localStorage is unavailable on stack overflow - uncomment those lines to see it work
Or - see https://jsfiddle.net/e9zg2p4c/
Store it to backend database. Then when serving HTML content put proper class/style for your elements. This will remove flickering between loading times:
<!DOCTYPE html>
<html>
<head>
<style>
/* Use Less/Sass for better management */
.theme.light {
background-color: #ffffff;
}
.theme.dark {
background-color: #000000; color: #ffffff;
}
</style>
</head>
<body class="theme <?= $user->themeName; ?>">
</body>
</html>
A little more tricky to toggle AND have a default theme
Note the localStorage calls do not work here at SO
working example
In the code below replace
const theme = "dark"; with
localStorage.getItem("theme") || "light"
and uncomment // localStorage.setItem("theme", body.classList.contains("dark") ? "light" : "dark");
on your server
.dark { background-color: black; color: white; }
<!DOCTYPE html>
<html>
<head>
<style>
.theme {
background-color: white;
color: black;
}
</style>
<script>
const theme = "dark"; // localStorage.getItem("theme") || "theme"
if (theme === "dark") {
const st = document.createElement("style");
st.id="darkStyle";
st.innerText = `body.theme { background-color: black; color: white; }`;
document.querySelector("head").appendChild(st);
}
window.addEventListener("load", function() {
document.getElementById("toggleTheme").addEventListener("click", function() {
const body = document.querySelector("body");
const darkStyle = document.getElementById("darkStyle");
if (darkStyle) {
darkStyle.remove(); // remove stylesheet now we know what the user wants
body.classList.remove("theme");
}
const theme = body.classList.contains("theme");
body.classList.toggle('theme',!theme);
body.classList.toggle('dark',theme);
// localStorage.setItem("theme", theme ? "light" : "dark"); // uncomment on your server
});
})
</script>
</head>
<body class="theme">
Here is the body
<button id="toggleTheme" type="button">Toggle theme</button>
</body>
</html>
Given that the only real difference between light and dark is the colours, why not simply create css variables for each colour you are going to use and use javascript to change the values of the variables. This way, once you have defined the classes using the variables in the appropriate places, changing the variable values changes the classes automatically. The choice of "dark" and "light" can be stored in whatever way is available to you - localStorage, cookies or backend etc - and you simply set the appropriate colours to the css variables when the page is being loaded. There's no need for separate definitions for each class and, as a developer, it allows you to quickly test the colour schemes without having to manually change every class one by one.
function changeTheme(t) {
if (t == "dark") {
document.documentElement.style.setProperty("--backgroundcolour", "black");
document.documentElement.style.setProperty("--fontcolour", "white");
} else {
document.documentElement.style.setProperty("--backgroundcolour", "white");
document.documentElement.style.setProperty("--fontcolour", "black");
}
}
:root {
--backgroundcolour:black;
--fontcolour:white;
}
body {
background-color:var(--backgroundcolour);
color:var(--fontcolour);
}
span {
background-color:var(--backgroundcolour);
color:var(--fontcolour);
}
div {
background-color:var(--backgroundcolour);
color:var(--fontcolour);
}
table {
background-color:var(--backgroundcolour);
color:var(--fontcolour);
}
<button onclick="changeTheme('dark');">Use dark theme</button><button onclick="changeTheme('light');">Use light theme</button>
<hr>
<span>Text in a span</span>
<hr>
<div>Text in a div</div>
<hr>
<table>
<tbody>
<tr><td>Text in a table</td></tr>
</tbody>
</table>
If you want to use checkbox, this solution for you.
if you want the value to remain unchanged, use localStorage. If you want a dark mode where you have values ​​disappear when you close a tab or browser, use sessionStorage.
const check = document.getElementById('chk');
check.addEventListener('change', () => {
document.body.classList.toggle('dark');
localStorage.darkMode=!localStorage.darkMode;
});
window.onload=function() {
if(localStorage.darkMode) document.body.classList.toggle('dark');
}
#modeSwitcher{
margin: 5% 50%;
}
#modeSwitcher .checkbox {
opacity: 0;
position: absolute;
}
#modeSwitcher .checkbox:checked + .label .ball{
transform: translateX(35px);
}
#modeSwitcher .checkbox:checked + .label .ball::after{
content: '';
position: absolute;
background-color: #0A0E27;
width: 13px;
height: 13px;
border-radius: 50%;
bottom: 50%;
left: -5%;
transform: translateY(50%);
}
#modeSwitcher .label {
background-color: #0A0E27;
border-radius: 50px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px;
margin: 0;
position: relative;
height: 16px;
width: 50px;
transform: scale(1.5);
}
#modeSwitcher .label .fa-moon{
color:#0A0E27 ;
}
#modeSwitcher .label .ball {
background-color: #FDC503;
border-radius: 50%;
position: absolute;
top: 3px;
left: 3px;
height: 20px;
width: 20px;
transform: translateX(0px);
transition: transform 0.2s linear;
}
body{
background-color: #fff;
}
body.dark{
background-color: black;
}
<div id="modeSwitcher">
<input type="checkbox" class="checkbox" id="chk" />
<label class="label" for="chk">
<i class="fas fa-moon"></i>
<div class="ball"></div>
</label>
</div>

How can one use CSS in an if statement of JavaScript code?

I have a Vue.js component and I want to use this CSS part when the if conditon is true:
<style type="text/css">
#sideNavBox {DISPLAY: none}
#contentBox {MARGIN-LEFT: 5px}
</style>
Here's the JavaScript:
IsCurrentUserMemberOfGroup("Admins", function (isCurrentUserInGroup) {
if(isCurrentUserInGroup) {
for (var i = 0; i < data.length; i++) {
$this.teams.push({
id: data[i].ID,
TeamName: data[i].TeamName.TeamName,
Coach: data[i].Coach,
CoachTel: data[i].CoachTel,
CoachAddress: data[i].CoachAddress,
CoachAssistant: data[i].CoachAssistant,
CoachAssistTel: data[i].CoachAssistTel,
CoachAssistAddress: data[i].CoachAssistAddress,
Email: data[i].Email
});
}
console.log("Works!")
}
});
I could add the CSS part in
<style>
</style>
but then it would be used all the time.
How can one use the CSS part if the if statement is true?
You have a couple options here:
Dynamically apply a class to the element based on the condition
Dynamically apply inline styling to the element based on the condition
Personally, I prefer to use classes. Here is an example:
const val = 42;
// Conditional
if (val === 42) {
setTimeout(() => {
document.querySelector('.box').classList.add('box--blue');
}, 1000);
}
if (val === 100) {
setTimeout(() => {
document.querySelector('.box').classList.add('box--green');
}, 1000);
}
.box {
width: 100px;
height: 100px;
background-color: deeppink;
transition: all 2s ease;
}
.box--blue {
background-color: dodgerblue;
}
.box--green {
background-color: limegreen;
}
<div class="box" />

Handling several DOM elements in Angular4

I'm building a SPA in Angular4/typescript.
I have several javascripts which manipulate DOM (add/remove CSS classes) which I want to integrate to the application.
What is the best way to do it?
Currently the structure of the application is the following:
-app:
-app.component.ts
-app.module.ts
-menu.component.ts
-menu.view.html
-menu.css
menu.component.ts handles data display of the application.
I want to integrate the following script:
<script>
const triggers = document.querySelectorAll('.cool > li');
const background = document.querySelector('.dropdownBackground');
const nav = document.querySelector('.top');
function handleEnter(){
this.classList.add('trigger-enter');
setTimeout(() => this.classList.contains('trigger-enter') &&
this.classList.add('trigger-enter-active'), 150);
background.classList.add('open');
const dropdown = this.querySelector('.dropdown');
const dropdownCords = dropdown.getBoundingClientRect();
const navCoords = nav.getBoundingClientRect();
const coords = {
height: dropdownCords.height,
width: dropdownCords.width,
top: dropdownCords.top - navCoords.top,
left: dropdownCords.left- navCoords.left
};
background.style.setProperty('width', `${coords.width}px`);
background.style.setProperty('height', `${coords.height}px`);
background.style.setProperty('transform', `translate(${coords.left}px, ${coords.top}px)`);
}
function handleLeave(){
this.classList.remove('trigger-enter', 'trigger-enter-active');
background.classList.remove('open');
}
triggers.forEach(trigger => trigger.addEventListener('mouseenter', handleEnter));
triggers.forEach(trigger => trigger.addEventListener('mouseleave', handleLeave));
</script>
CSS:
nav {
position: relative;
perspective: 600px;
}
nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
}
.cool > li {
position: relative;
display:flex;
justify-content: center;
}
.cool > li > a {
color: yellow;
text-decoration: none;
font-size: 20px;
background: rgba(0,0,0,0.2);
padding:10px 20px;
display: inline-block;
margin:20px;
border-radius:5px;
}
.dropdown {
opacity: 0;
position: absolute;
overflow: hidden;
padding:20px;
top:-20px;
border-radius:2px;
transition: all 0.5s;
transform: translateY(100px);
will-change: opacity;
display: none;
}
.trigger-enter .dropdown {
display: block;
}
.trigger-enter-active .dropdown {
opacity: 1;
}
.dropdownBackground {
width:100px;
height:100px;
position: absolute;
background: #fff;
border-radius: 4px;
box-shadow: 0 50px 100px rgba(50,50,93,.1), 0 15px 35px rgba(50,50,93,.15), 0 5px 15px rgba(0,0,0,.1);
transition:all 0.3s, opacity 0.1s, transform 0.2s;
transform-origin: 50% 0;
display: flex;
justify-content: center;
opacity:0;
}
.dropdownBackground.open {
opacity: 1;
}
.arrow {
position: absolute;
width:20px;
height:20px;
display: block;
background:white;
transform: translateY(-50%) rotate(45deg);
}
html:
<nav class="top" menuElement>
<div class="dropdownBackground">
<span class="arrow"></span>
</div>
<ul class="cool">
<li>
Some information
<div class="dropdown dropdown1">
Info
</div>
</li>
<li>
More information
<ul class="dropdown">
<li>
some info
</li>
</ul>
</li>
<li>
Other Links
<ul class="dropdown dropdown3">
<li>some links</li>
</ul>
</li>
</ul>
</nav>
There are 3 querySelector. I tried to implement the functionality by defining a Directive:
menu.directive.ts:
import {Directive, HostListener, ElementRef, Renderer2} from '#angular/core';
#Directive({
selector: '[menuElement]',
})
export class MenusDirective {
constructor(
private renderer: Renderer2,
private el: ElementRef
){}
//menuLi = this.el.nativeElement.querySelectorAll('.cool > li');
background = this.el.nativeElement.querySelector('.dropdownBackground');
handleEnter(target){
this.renderer.addClass(target, 'trigger-enter');
setTimeout(() => target.classList.contains('trigger-enter') &&
this.renderer.addClass(target, 'trigger-enter-active'), 150);
this.background.classList.add('open');
const dropdown = target.querySelector('.dropdown');
const dropdownCords = dropdown.getBoundingClientRect();
const filterNavCoords = this.el.nativeElement.getBoundingClientRect();
const coords = {
height: dropdownCords.height,
width: dropdownCords.width,
top: dropdownCords.top - filterNavCoords.top,
left: dropdownCords.left- filterNavCoords.left
};
this.background.style.setProperty('width', `${coords.width}px`);
this.background.style.setProperty('height', `${coords.height}px`);
this.background.style.setProperty('transform', `translate(${coords.left}px, ${coords.top}px)`);
}
handleLeave(target){
this.renderer.removeClass(target, 'trigger-enter');
this.renderer.removeClass(target, 'trigger-enter-active');
this.background.classList.remove('open');
}
#HostListener('mouseenter', ['$event']) onMouseEnter(event: Event) {
this.handleEnter(event.target);
}
#HostListener('mouseleave', ['$event']) onMouseLeave(event: Event) {
this.handleLeave(event.target);
}
}
If I define the selector [menuElement] equals to the upper nav, I'll be able to select :
background = this.el.nativeElement.querySelector('.dropdownBackground');
or
triggers = this.el.nativeElement.querySelectorAll('.cool > li');
but event Listener would be bind on the whole nav, so there is no way to know which one of the '.cool > li' is selected.
If I define [menuElement] as '.cool > li', I don't find a way to select '.dropdownBackground' or the upper nav.
const background = document.querySelector('.dropdownBackground');
returns null (I tried also getElementByClassName, etc.)
I can't define them in different Directives as they are manipulated simultaneous.
Also I tried to add the function in HTML:
<li (mouseenter)="handleEnter($event.target)" (mouseleave)="handleLeave($event.target)">
But the functions are not recognized as defined in menu.directive and not in menu.component.
What are the possible solution?
- integrate Javascript as is (loaded after the DOM is loaded)
- select several DOM object, but bind a EventListener to one of them.
Thanks a lot!
Veronika
The way I figured out to work:
add Event listeners in view.html:
<li (mouseenter)="handleEnter($event.target);" (mouseleave)="handleLeave($event.target)">
Define handleEnter and handleLeave functions in menu.component.ts, add MenuDirective as a provider:
menu.component.ts:
import { MenuDirective } from './menu.directive';
#Component({
selector: 'menu',
templateUrl: './menu.view.html',
styleUrls: [ './app.component.css'],
providers: [MenuDirective]
})
export class MenuComponent {
constructor(
private menuDirective: MenuDirective,
){}
handleEnter(target): void {
this.menuDirective.handleEnter(target);
}
handleLeave(target): void {
this.menuDirective.handleLeave(target);
}
}
and menu.directive.ts:
import {Directive, HostListener, ElementRef, Renderer2} from '#angular/core';
#Directive({
selector: '[menuElement]',
})
export class MenuDirective {
constructor(
private renderer: Renderer2,
private el: ElementRef
){}
handleEnter(target){
this.renderer.addClass(target, 'trigger-enter');
setTimeout(() => target.classList.contains('trigger-enter') &&
this.renderer.addClass(target, 'trigger-enter-active'), 150);
this.el.nativeElement.querySelector('.dropdownBackground').classList.add('open');
const dropdown = target.querySelector('.dropdown');
const dropdownCords = dropdown.getBoundingClientRect();
const filterNavCoords = this.el.nativeElement.getBoundingClientRect();
const coords = {
height: dropdownCords.height,
width: dropdownCords.width,
top: dropdownCords.top - filterNavCoords.top,
left: dropdownCords.left- filterNavCoords.left
};
this.el.nativeElement.querySelector('.dropdownBackground').style.setProperty('width', `${coords.width}px`);
this.el.nativeElement.querySelector('.dropdownBackground').style.setProperty('height', `${coords.height}px`);
this.el.nativeElement.querySelector('.dropdownBackground').style.setProperty('transform', `translate(${coords.left}px, ${coords.top}px)`);
}
handleLeave(target){
this.renderer.removeClass(target, 'trigger-enter');
this.renderer.removeClass(target, 'trigger-enter-active');
this.el.nativeElement.querySelector('.dropdownBackground').classList.remove('open');
}
}
It seems too complex for such simple feature...

How to toggle a selected class in a global click event?

I'm creating my component library in vue, and I defined my component checkbox, the code is like this:
<template>
<div class="checkboxcont" :class="{'checkboxcont-selected': isSelected}" #click="clickevent">
<span class="j-checkbox">
<input type="checkbox" />
</span>
<slot></slot>
</div>
</template>
<script>
export default {
data() {
return {
isSelected: false
}
},
methods: {
clickevent(event) {
if(this.isSelected) {
this.isSelected = false;
} else {
this.isSelected = true;
}
}
},
}
</script>
Now, I hope that when I click the checkbox to set the data "isSelected" false, I can give the component class "checkboxcont-selected-last", and when I click other checkbox component, the classname "checkboxcont-selected-last" can be removed, how can I listen my click event to finish it? I try to use native JavaScript code to add the classname of the dom, but it seemed to have nothing when I binded the classname of my component with Vue.js:
clickevent(event) {
if(this.isSelected) {
this.isSelected = false;
this.$el.classList.add("checkboxcont-selected-last");
} else {
this.isSelected = true;
}
}
What should I do to solve this problem, please?
Here is my style code using less:
<style lang="less" scoped rel="stylesheet/less">
#import '../../mixin/mixin.less';
.checkboxcont {
display: inline-block;
&:hover {
cursor: pointer;
.j-checkbox {
border-color: #jbluelight;
}
}
}
.j-checkbox {
position: relative;
top: 0;
left: 0;
width: 12px;
height: 12px;
display: inline-block;
border: 1px solid #border;
border-radius: 3px;
line-height: 12px;
vertical-align: -3px;
margin: 0 5px;
z-index: 20;
transition: all .2s linear;
input {
opacity: 0;
position: absolute;
left: 0;
top: 0;
visibility: hidden;
/*display: none;*/
}
}
.checkboxcont-selected {
.j-checkbox {
background: #jbluelight;
border-color: #jbluelight;
&:after {
content: '';
width: 4px;
height: 7px;
border: 2px solid white;
border-top: none;
border-left: none;
position: absolute;
left: 3px;
top: 0;
z-index: 30;
transform: rotate(45deg) scale(1);
}
}
}
</style>
<style lang="less" rel="stylesheet/less">
#import '../../mixin/mixin.less';
.checkboxcont-selected-last .j-checkbox {
border-color: #jbluelight;
}
</style>
My initial thought is that I add the class by using this.$el after I clicked the component, it can be accessed because I dispatched the click event, and I just can't access the other component:
if(this.isSelected) {
this.isSelected = false;
this.$el.classList.add("checkboxcont-selected-last")
} else {
this.isSelected = true;
}
And I remove the class by using native HTML DOM operation when I dispatch the click event because I can not access the other component, so the complete definition of clickevent is that:
clickevent(event) {
let selectedLast = document.querySelector(".checkboxcont-selected-last");
if(selectedLast) {
selectedLast.classList.remove("checkboxcont-selected-last")
}
if(this.isSelected) {
this.isSelected = false;
this.$el.classList.add("checkboxcont-selected-last")
} else {
this.isSelected = true;
}
}
It looks good, but I can not add classname of my component when I use v-bind to bind my component's classname, is it wrong? And Is it unable to use native HTML DOM operation when I bind my component's classname with Vue?
A better way to dynamically add or remove class can be using v-bind:class. There are different ways you can add a dynamic class based on a vue data variable.
I see you are already using it:
<div class="checkboxcont" :class="{'checkboxcont-selected': isSelected}" #click="clickevent">
So here this div will have only one class : checkboxcont if isSelected is false, and two classes : checkboxcont and checkboxcont-selected if isSelected is true.
Edited:
Given that you want to add a class to DOM on another component, I can think of two ways:
Using Web API: You can do following if you know the id of the element you want to add class using Element.className:
var d = document.getElementById("yourElem") d.className += " otherclass"
Vuex way: You can have a centralised state provided by vue or use vuex to manage state, these state variables can be changed across components, and can be used to add/remove class dynamically.
You can have a look at my this answer to understand more about vuex.

Categories

Resources