I have created a webcomponent for a generic input boxes that i needed across multiple projects.
the design functionality remains same only i have to use switch themes on each projects.so i have decided to go on with webcomponents.One of the projects is based on Vue Js.In Vue js the DOM content is re-rendered while each update for enabling reactivity. That re-rendering of vue template is reinitializing my custom webcomponent which will result in loosing all my configurations i have assigned to the component using setters.
I know the below solutions. but i wanted to use a setter method.
pass data as Attributes
Event based passing of configurations.
Using Vue-directives.
using v-show instead of v-if
-- Above three solutions doesn't really match with what i am trying to create.
I have created a sample project in jsfiddle to display my issue.
Each time i an unchecking and checking the checkbox new instances of my component is creating. which causes loosing the theme i have selected. (please check he active boxes count)
For this particular example i want blue theme to be displayed. but it keep changing to red
JSFiddle direct Link
class InputBox extends HTMLElement {
constructor() {
super();
window.activeBoxes ? window.activeBoxes++ : window.activeBoxes = 1;
var shadow = this.attachShadow({
mode: 'open'
});
var template = `
<style>
.blue#iElem {
background: #00f !important;
color: #fff !important;
}
.green#iElem {
background: #0f0 !important;
color: #f00 !important;
}
#iElem {
background: #f00;
padding: 13px;
border-radius: 10px;
color: yellow;
border: 0;
outline: 0;
box-shadow: 0px 0px 14px -3px #000;
}
</style>
<input id="iElem" autocomplete="off" autocorrect="off" spellcheck="false" type="text" />
`;
shadow.innerHTML = template;
this._theme = 'red';
this.changeTheme = function(){
this.shadowRoot.querySelector('#iElem').className = '';
this.shadowRoot.querySelector('#iElem').classList.add(this._theme);
}
}
connectedCallback() {
this.changeTheme();
}
set theme(val){
this._theme = val;
this.changeTheme();
}
}
window.customElements.define('search-bar', InputBox);
<!DOCTYPE html>
<html>
<head>
<title>Wrapper Component</title>
<script src="https://unpkg.com/vue"></script>
<style>
html,
body {
font: 13px/18px sans-serif;
}
select {
min-width: 300px;
}
search-bar {
top: 100px;
position: absolute;
left: 300px;
}
input {
min-width: 20px;
padding: 25px;
top: 100px;
position: absolute;
}
</style>
</head>
<body>
<div id="el"></div>
<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template" id="demo-template">
<div>
<div class='parent' contentEditable='true' v-if='visible'>
<search-bar ref='iBox'></search-bar>
</div>
<input type='checkbox' v-model='visible'>
</div>
</script>
<script type="text/x-template" id="select2-template">
<select>
<slot></slot>
</select>
</script>
<script>
var vm = new Vue({
el: "#el",
template: "#demo-template",
data: {
visible: true,
},
mounted(){
let self = this
setTimeout(()=>{
self.$refs.iBox.theme = 'blue';
} , 0)
}
});
</script>
</body>
</html>
<div class='parent' contentEditable='true' v-if='visible'>
<search-bar ref='iBox'></search-bar>
</div>
<input type='checkbox' v-model='visible'>
Vue's v-if will add/remove the whole DIV from the DOM
So <search-bar> is also added/removed on every checkbox click
If you want a state for <search-bar> you have to save it someplace outside the <search-bar> component:
JavaScript variable
localStorage
.getRootnode().host
CSS Properties I would go with this one, as they trickle into shadowDOM
...
...
Or change your checkbox code to not use v-if but hide the <div> with any CSS:
display: none
visibility: hidden
opacity: 0
move to off screen location
height: 0
...
and/or...
Managing multiple screen elements with Stylesheets
You can easily toggle styling using <style> elements:
<style id="SearchBox" onload="this.disabled=true">
... lots of CSS
... even more CSS
... and more CSS
</style>
The onload event makes sure the <style> is not applied on page load.
activate all CSS styles:
(this.shadowRoot || document).getElementById("SearchBox").disabled = false
remove all CSS styles:
(this.shadowRoot || document).getElementById("SearchBox").disabled = true
You do need CSS Properties for this to work in combo with shadowDOM Elements.
I prefer native over Frameworks. <style v-if='visible'/> will work.. by brutally removing/adding the stylesheet.
Related
I've been trying to make a custom HTML Element by extending the HTMLElement class. I try adding some style to it by linking a CSS file that is in the same directory as my other two files - index.html and custom.css.
Main folder
index.html
custom.css
custom.js
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="nofollow" type="text/css" href=''>
</head>
<body>
<script src="./custom.js"></script>
<smooth-button text="Smooth button" no-1 = 1 no-2 = 2></smooth-button>
</body>
</html>
custom.css:
smooth-button{
display: block;
color: blue;
background-color: orange;
}
custom.js:
class SmoothButton extends HTMLElement{
constructor(){
super();
this.shadow = this.attachShadow({mode: "open"})
}
connectedCallback(){
this.render();
}
render(){
this.SumOfNo1AndNo2 = null;
if(this.getAttribute("no-1")!=null && this.getAttribute("no-2")!=null){
this.SumOfNo1AndNo2 = parseInt(this.getAttribute("no-1")) +
parseInt(this.getAttribute("no-2"));
}
else{
console.log("Invalid attribute.")
}
this.shadow.innerHTML = `<button>` + this.getAttribute("text") + " " + this.SumOfNo1AndNo2
+ "</button>"
}
}
customElements.define("smooth-button", SmoothButton);
With this, I get a button as expected, with the text, but the style is applied to the element as a whole and not to the elements it's made of. How can I apply the styles separately to each of its elements (just a <button> for now) with an external CSS file? I'm using external CSS because it's somehow better as I read it here.
In addition to the answers from Brad and Emiel,
(Brad) Bluntly add a <style> element inside shadowDOM
Do read about adopted StylesSheets (Chromium only)
(Emiel) use cascading CSS properties
There are more options to style shadowDOM:
Learn about Inheritable Styles
https://lamplightdev.com/blog/2019/03/26/why-is-my-web-component-inheriting-styles/
use shadow parts
<style>
::part(smoothButton){
display: block;
color: blue;
background-color: orange;
}
</style>
<smooth-button></smooth-button>
<smooth-button></smooth-button>
<script>
customElements.define("smooth-button", class extends HTMLElement {
constructor(){
super()
.attachShadow({mode:"open"})
.innerHTML = `<button part="smoothButton">LABEL</button>`;
}
});
</script>
https://developer.mozilla.org/en-US/docs/Web/CSS/::part
https://meowni.ca/posts/part-theme-explainer/
https://css-tricks.com/styling-in-the-shadow-dom-with-css-shadow-parts/
https://dev.to/webpadawan/css-shadow-parts-are-coming-mi5
https://caniuse.com/mdn-html_global_attributes_exportparts
But...
The first question you should ask yourself:
Do I really need shadowDOM?
If you don't want its encapsulating behavior, then do not use shadowDOM
<style>
.smoothButton{
display: block;
color: blue;
background-color: orange;
}
</style>
<smooth-button></smooth-button>
<smooth-button></smooth-button>
<script>
customElements.define("smooth-button", class extends HTMLElement {
connectedCallback(){
this.innerHTML = `<button class="smoothButton">LABEL</button>`;
}
});
</script>
shadowDOM <slot>
Another alternative is to use shadowDOM <slot> elements, because they are styled by its container element
<style>
.smoothButton{
display: block;
color: blue;
background-color: orange;
}
</style>
<smooth-button><button class="smoothButton">LABEL</button></smooth-button>
<smooth-button><button class="smoothButton">LABEL</button></smooth-button>
<script>
customElements.define("smooth-button", class extends HTMLElement {
constructor(){
super()
.attachShadow({mode:"open"})
.innerHTML = `<slot></slot>`;
}
});
</script>
When you go down the <slot> rabbithole, be sure to read the (very long) post:
::slotted CSS selector for nested children in shadowDOM slot
Additionally to Brad's answer. One of the ways you can apply styles from the Light DOM to the Shadow DOM is with CSS Variables.
smooth-button{
display: block;
--button-color: blue;
--button-background-color: orange;
}
render() {
this.shadow.innerHTML = `
<style>
button {
color: var(--button-color);
background-color: var(--button-background-color);
}
</style>
<button>
${this.getAttribute("text")} ${this.SumOfNo1AndNo2}
</button>
`;
)
With this, I get a button as expected, with the text, but the style is applied to the element as a whole and not to the elements it's made of.
This is actually how the custom element is supposed to work. You can't apply styles to the shadow DOM from the outer document. If you could, you'd have a high likelihood of breaking the custom element styling through external modification.
All is not lost however! The reason the button is a different color from its background is due to the user agent stylesheet. You can actually set some CSS to tell the background to inherit the parent background color. Try adding this to your custom element:
const style = document.createElement('style');
style.textContent = `
button {
background: inherit;
}
`;
this.shadow.append(style);
JSFiddle: https://jsfiddle.net/5t2m3bku/
(Also note that it's not really a great idea to interpolate/concatenate text directly into HTML. That text gets interpreted as HTML, which can lead to invalid HTML if reserved characters are used, and even potential XSS vulnerabilities. You might modify that line where you set innerHTML to set the text, or switch to a template engine.)
I'm creating a commenting function for a text editor in a Vue.js project. The commenting function will highlight a portion of text and put a comment card on the right side, similar to Google comment system in docs.
As I was working on the highlight function of the feature I realized that the setAttribute class only worked on a global class and not scoped for the component the text is displayed in. Why is this? What is the functionality behind this? I'm curious about how it works under the hood.
tl;dr element.setAttribute('class', 'classname') on a dynamically created element (document.createElement('element')) does not work if the class is in scoped styles, why?
<template>
<div>
<p #click="highlight">
{{ text }}
</p>
</div>
</template>
<script>
export default {
name: 'Paper',
data() {
return {
text: "Some lorem ipsum"
}
},
methods: {
highlight() {
var selected = window.getSelection().getRangeAt(0)
var selectedText = selected.extractContents()
console.log(selected)
console.log(selectedText)
var span = document.createElement("span")
console.log(span)
span.setAttribute('class', 'highlight-text')
span.appendChild(selectedText)
selected.insertNode(span)
}
}
}
</script>
<style scoped>
div {
box-shadow: rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
height: 1123px;
width: 794px;
padding: 96px;
margin: 100px
}
p {
line-height: 1.4;
}
</style>
<style>
.highlight-text {
background-color: yellow;
}
</style>
Also, if you have any tips on how I can improve the code it is very much appreciated!
Vue will replace the scoped css class name with a unique data attributes and set this on the element.
I.e the setAttribute tries to add a class name which actually does not exists.
Example from docs:
https://vue-loader.vuejs.org/guide/scoped-css.html#mixing-local-and-global-styles
I have an html file(a webpage). I want when I press a button on it, the page should be replaced by another html file (with its own css, javascript functions etc) without being redirected to some other link.
For example, if link in first case is abc.com/def it should be same after too.
Using this code, I am able to change webpage look, but not getting how to change look (and also manage to load css and js functions) from another file.
<script type="text/javascript">
document.body.addEventListener('click',function(){
document.write("THIS IS NEW TEXT")
},
false);
</script>
You need to look into frameworks like AngularJS, Specially Routing of Angular. They provide such features built-in for web applications. However, you can do it the hard way, using javascript, like you are doing it right now. Add CSS and change whole body HTML using javascript if you don't want to learn any new framework or libraries.
You want to use PJAX,
Here's a link for an example.
As discuss by others, you should use a Framework to do this..
But this is a complete solution you can inspire of:
let layouts = {}
let current = null
// Display the new page by deleting current, and replacing by the good one
let displayLayout = (layout_id) => {
let parentNode = current.parentNode
parentNode.removeChild(current)
current = layouts[layout_id]
parentNode.appendChild(current)
loadEvents(current)
}
// Load event for HTML DOM you just created
let loadEvents = (layout_el) => {
Array.from(layout_el.getElementsByClassName('go-to-layout')).forEach(el => {
el.addEventListener('click', e => {
e.preventDefault()
displayLayout(e.currentTarget.dataset.layout)
})
})
}
// On init I get all the existing layout, but you can build you own dictionary an other way.
Array.from(document.getElementsByClassName('layout')).forEach(l => {
layouts[l.id] = l
if (l.classList.contains('active')) {
loadEvents(l)
current = l
}
else {
l.parentNode.removeChild(l);
}
})
/* Global CSS */
body, html, .layout {
height: 100%;
margin: 0;
}
* {
color: #FFF
}
.layout {
display: flex;
}
.nav, .page {
}
.nav {
width: 150px;
background: #555;
}
/* Special CSS for one layout */
#layout1 {
background: red;
}
#layout2 {
background: blue;
}
<div id="layout1" class="layout active">
<div class="nav">
Page 2
</div>
<div class="page">
This is page 1
</div>
</div>
<div id="layout2" class="layout">
<div class="nav">
Page 1
</div>
<div class="page">
This is page 2
</div>
<style>.page { font-size: 2em }</style>
</div>
i am new to reactjs and i am unable to toggle the class for div element on click event.
What i want to implement is below,
I have a div element created dynamically like below,
constructor(props) {
super(props);
this.element = document.createElement('div');
this.element.className = 'div_class';
}
I add and remove the div element on component mount and unmount as below,
componentDidMount() {
notifications_root.appendChild(this.element);
ReactDOM.render(<SvgSome onClick={this.handle_dialog_close} width="36"
style={{position:'absolute',cursor: 'pointer', right: '250px', top:
'105px'}} />, this.element);
}
componentWillUnmount() {
this.element.classList.remove('hidden');
root.removeChild(this.element);
}
handle_dialog_close = () => {
this.element.classList.add('hidden');
};
Also i add and remove class 'hidden' to div element on clicking the svg element.
However it does hide the div element on clicking svg element but doesnt show up the div element again...I guess the div element class is set to hidden it doesnt showup. Can somebody help me know where the problem is. Below is the css code. Thanks.
.div_class {
width:800px;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
margin-top: 100px;
margin-right: auto;
margin-bottom: 0px;
margin-left: auto;
&.hidden {
display: none;
}
}
One thing you could is create an variable (or array if you want more classes) and then add then conditionally.
Render(){
Const divClasses =[‘My-Div’, ‘Container’]
Return(
<div className=
{/*condition*\ && divClasses>
//content
</div>
)
}
So the class(es) will be the added based on a condition. Additionally you could add a handler to control the condition.
React is a declarative library. You don't need to create and remove DOM elements with it. You just declare them with jsx inside the render method.
Check out some react tutorials to learn how to work with react.
You are looking for some solution like this:
<div className=`div_class ${this.state.hidden ? 'hidden' : ''}`>
<svg onClick={() => this.setState({hidden: !this.state.hidden})}></svg>
</div>
What about using core javascript instead?
There is no point to use a library if it makes stuff harder!
function toggleClass(id,classA,classB){
if (document.getElementById(id).classList[0] === classA){
document.getElementById(id).classList = classB
}
else {
document.getElementById(id).classList = classA
}
}
.hidden {display:none}
.show {display:block}
#elem1 {background: blue; width:200px;height:200px;font-size:10rem;text-align:center}
<button onclick="toggleClass('elem1','show','hidden')">Hide/Show</button>
<div id="elem1" class="show">☀</div>
I'm trying to implement CSS nth-child on every number of elements. If a certain number is reached I want to hide the first element and make it reappear if the number reduces again.
The problem is that somehow the nth-child still counts the hidden element and thus wrongly implements the styling. Is this a bug or am I doing it wrong?
NOTE: The same thing also happens if I use jQuery
http://jsfiddle.net/bedex78/uZ5wn/23/
The View:
<div ng-app>
<div ng-controller="TheCtrl">
<p>Amount to add: <input type="text" ng-model="amount" ng-init="amount=1"></p>
<div class='holder'>
<div ng-class='elements.length < 6 ? "inside" : ""'
ng-hide="elements.length >= 6">
<button class='button' ng-click="add(amount)">Add more</button>
</div>
<div class='inside' ng-repeat="(k,v) in elements">
{{ $index }} Remove
</div>
</div>
</div>
</div>
The JS (AngularJS):
function TheCtrl($scope) {
$scope.elements = [{id:1},{id:2}]
$scope.add = function(amount) {
for (i=0; i < amount; i++){
$scope.elements.push({id:$scope.elements.length+1});
}
};
$scope.remove = function(index) {
$scope.elements.splice(index, 1);
};
}
The CSS:
.holder {
width: 300px;
height: 400px;
border: 1px solid black;
}
.inside {
height: 30px;
border: 1px solid black;
}
.inside:nth-child(3n+1) {
background-color: yellow;
}
.inside a {
float: right;
}
It happens because hidden element is still in DOM. So it is count as a child and styles applied accordingly.
You can try to use ng-if instead of ng-hide. It will make div disappear from DOM and styles will work fine.
Example