Is there a way to create a custom event in in Lit-Element/Polymer, such as a mouse-over event? I've been searching for this a while now, but I can seem to find a way of doing it. I know about events in Lit-Element, like #click, but nothing about mouse-over events.
This can be done using lit-html #event bindings.
For the mouseover event type, use #mouseover="${this.handleMouseover}"
For more information on lit-element event listeners, see
https://lit-element.polymer-project.org/guide/events
Live example:
<script src="https://unpkg.com/#webcomponents/webcomponentsjs#latest/webcomponents-loader.js"></script>
<script type="module">
import { LitElement, html, css } from 'https://unpkg.com/lit-element/lit-element.js?module';
class MyElement extends LitElement {
static get styles() {
return [
css`
span {
padding: 4px 8px;
border: 2px solid orange;
}
`
];
}
render() {
return html`
<span
#mouseover="${this.handleEvent}"
#mouseleave="${this.handleEvent}">
Hover me
</span>
`;
}
handleEvent(e) {
console.log(`Event type: ${e.type}`);
}
}
customElements.define('my-element', MyElement);
</script>
<my-element></my-element>
So I just figured it out, and just going to post here if anyone has the same difficulty.
You have to use CustomEvents, here some code example:
in your element's firstUpdate method you should add a new EventListener
firstUpdated(){
this.addEventListener('mouseover', this.mouseOverHandler);
}
and declare the method
mouseOverHandler(){
console.log('Hello');
}
Simple as that!!!
Related
I have created a vanilla web component or HTML element. It just displays two links.
To encapsulate the thing, I use shadow DOM. However it does not seem to be encapsulated. In the DOM tree it's inside #shadow-root which is good.
Why does the web component use the global style instead of the style I provided in the template for my web component?
The text is red and I expected it to be green.
class MyEl extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `
<style>
a {
color: green;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = template;
}
}
window.customElements.define("my-el", MyEl);
a {
color: red
}
<my-el>
Item1
Item2
</my-el>
While this question already has an accepted answer, moving a slot's children to the shadowRoot isn't desirable for most use cases.
What you probably want to do is to use the ::slotted() selector.
Just bear in mind that styles applied to a slot's children through the ::slotted() selector only act as "default" styles and can still be overridden by using styles in light DOM.
For example, check this edited version of your snippet:
As you can see, this time my-el tries to apply both a color and a text-decoration style to anchor (<a>) children in any of it's slots.
However, in light dom, we have a a.special selector that overrides the color, so the <a class="special"> will be red, not green
class MyEl extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `
<style>
::slotted(a) {
color: green;
text-decoration: none;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = template;
}
}
window.customElements.define("my-el", MyEl);
a.special {
color: red
}
<my-el>
Item1
<a class="special" href="example.com">Item2</a>
</my-el>
The full, detailed explanation is in: ::slotted CSS selector for nested children in shadowDOM slot
TL;DR
Your links are in lightDOM and thus styled by its DOM (in your code the document DOM)
Moving the nodes from lightDOM to shadowDOM is one "solution"; but you are not using slots then.
FYI, your code can be compacted to:
class MyEl extends HTMLElement {
constructor() {
super().attachShadow({ mode: "open" })
.innerHTML = `<style>a{color:green}</style><slot></slot>`;
}
}
window.customElements.define("my-el", MyEl);
More SLOT related answers can be found with StackOverflow Search: Custom Elements SLOTs
observe this line, you have to move/copy elements to shadow for example with:
this.shadow.innerHTML = this.innerHTML + template;
I've added this to demonstrate that only inline style will be applied to shadow dom elements .. so copied links in SD are using your style :)
so red will be GLOBAL, green will be SHADOW elements
class MyEl extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.shadow = this.attachShadow({ mode: "open" });
const template = `
<style>
a {
color: green;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = this.innerHTML + template;
}
}
window.customElements.define("my-el", MyEl);
a {
color: red
}
<my-el>
Item1
Item2
</my-el>
UPDATE: This turned out to be a Babel configuration issue in the end.
I'm working with React function components for the first time and am getting some unexpected results with event handlers. Here's a massively reduced test case I created after encountering problems with a more fleshed-out component:
const App = () => {
const handler = function(e) {
e.preventDefault();
console.log('handleAppClick');
};
return (
<div onClick={ handler }>This is the whole thing.</div>
);
};
ReactDOM.render(<App />, document.querySelector('#app'));
body {
font-family: monospace;
}
#app {
border: 2px solid black;
padding: 8px;
border-radius: 2px;
cursor: pointer;
user-select: none;
}
<script src="https://unpkg.com/react#16.12.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.12.0/umd/react-dom.development.js"></script>
<div id="app"></div>
It's my expectation/understanding that there is no binding necessary in this case because there's no need for a this reference in the handler. I've tried defining the handler inside and outside App, defining it as an arrow function, defining it as simply function handler(e) { ... }, but in all cases, I don't get a working click handler on this div on the rendered page, and if I inspect the DOM I can see a on="[object Object]" attribute on the div.
What's going on here? Seems like I'm just missing something glaringly obvious here.
This turned out to be a Babel configuration issue in the end. Thanks to all who commented or made suggestions.
i think you have to call the handler not reference it
<div onClick={ handler() }>This is the whole thing.</div> ----> also try
<div onClick={ (e) => handler(e) }>This is the whole thing.</div>
I want an overlay to show up when I click a search icon.
I managed to get it working using jQuery. But can't seem to get it working with javascript.
The click event does not seem to be registering and I don't know why.
I've checked all the class names so they match in the same in both the HTML and javascript
Here is the jQuery code that works:
import $ from 'jquery';
class Search {
constructor() {
this.openButton = $('.js-search-trigger');
this.closeButton = $('.search-overlay__close');
this.searchOverlay = $(".search-overlay");
this.events();
}
events() {
this.openButton.on('click', this.openOverlay.bind(this));
this.closeButton.on('click', this.closeOverlay.bind(this));
}
openOverlay() {
this.searchOverlay.addClass("search-overlay--active");
}
closeOverlay() {
this.searchOverlay.removeClass("search-overlay--active");
}
}
export default Search;
Here is the javascript code that does not work:
class Search {
constructor() {
this.openButton = document.querySelector('.js-search-trigger');
this.closeButton = document.querySelector('.search-overlay__close');
this.searchOverlay = document.querySelector('.search-overlay');
this.events();
}
events() {
this.openButton.addEventListener('click', function() {
this.openOverlay.bind(this);
});
this.closeButton.addEventListener('click', function() {
this.closeOverlay.bind(this);
});
}
openOverlay() {
this.searchOverlay.classList.add('search-overlay--active');
}
closeOverlay() {
this.searchOverlay.classList.remove('search-overlay--active');
}
}
export default Search;
No errors were shown in the javascript where the overlay was not showing.
You'll probably want to change your event listeners to use the correct this binding:
this.openButton.addEventListener("click", this.openOverlay.bind(this));
Or use an arrow function to go with your approach - but make sure you actually call the resulting function, as in the above approach the function is passed as a reference and is called. If you removed the additional () from the code below, it would be the same as writing a function out in your code normally - it would be defined, but nothing would happen.
this.openButton.addEventListener("click", () => {
this.openOverlay.bind(this)();
});
jQuery also uses collections of elements rather than single elements, so if you have multiple elements, querySelectorAll and forEach might be in order.
If we are speaking of ecmascript-6 (I see the tag), I would recommend to use arrow function to have this inherited from the above scope, and no bind is needed:
this.openButton.addEventListener('click', () =>
this.openOverlay()
);
The problems with your code are that a) the function creates new scope with its own this; b) bound methods are not being invoked.
Why Search? You're creating an Overlay. Stick with the plan.
No need to bind anything. Use Event.currentTarget if you want to.
No need to handle .open/.close if all you need is a toggle.
And the below should work (as-is) for multiple Overlays. The overlay content is up to you.
class Overlay {
constructor() {
this.toggleButtons = document.querySelectorAll('[data-overlay]');
if (this.toggleButtons.length) this.events();
}
events() {
this.toggleButtons.forEach(el => el.addEventListener('click', this.toggleOverlay));
}
toggleOverlay(ev) {
const btn = ev.currentTarget;
const sel = btn.getAttribute('data-overlay');
const overlay = sel ? document.querySelector(sel) : btn.closest('.overlay');
overlay.classList.toggle('is-active');
}
}
new Overlay();
*{margin:0; box-sizing:border-box;} html,body {height:100%; font:14px/1.4 sans-serif;}
.overlay {
background: rgba(0,0,0,0.8);
color: #fff;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
padding: 5vw;
transition: opacity 0.4s, visibility 0.4s;
visibility: hidden;
opacity: 0;
}
.overlay.is-active {
opacity: 1;
visibility: visible;
}
<button type="button" data-overlay="#search">OPEN #search</button>
<button type="button" data-overlay="#qa">OPEN #qa</button>
<div class="overlay" id="search">
<button type="button" data-overlay>CLOSE</button>
<h2>SEARCH</h2>
<input type="text" placeholder="Search…">
</div>
<div class="overlay" id="qa">
<button type="button" data-overlay>CLOSE</button>
<h2>Q&A</h2>
<ul><li>Lorem ipsum</li></ul>
</div>
The above is still not perfect, still misses a way to "destroy" events and not re-attach duplicate events to already initialised buttons when trying to target dynamically created ones.
Also, the use of Classes for the above task is absolutely misleading and unnecessary.
I would like to add and remove 'over' from my class on an element created using a lit-html template triggered by 'dragEnter' and 'dragLeave':
#app {
background-color: #72a2bc;
border: 8px dashed transparent;
transition: background-color 0.2s, border-color 0.2s;
}
#app.over {
background-color: #a2cee0;
border-color: #72a2bc;
}
const filesTemplate = () =>
html`
<button id="app"
#dragover=${??}
#dragleave=${??}
>
Click Me
</button>
`;
In my old system I called these methods in a separate module via an event emitter, but I am hoping I can make it all defined in the template using lit-html.
dragEnter(e) {
this.view.element.className += ' over';
}
dragLeave(e) {
this.view.element.className = element.className.replace(' over', '');
}
It depends what your custom element looks like. With your template you could just put #dragover=${this.dragEnter}. However, if you want this to apply to your entire custom element and not just the button you can do something like this:
connectedCallback() {
super.connectedCallback();
this.addEventListener('dragover', this.dragEnter);
}
If you do not have custom element and just use lit-html by itself you have to put your event handlers dragEnter(e)and dragLeave(e) into the template like so: #dragover=${this.dragEnter}
You need to add the class with classList.add in dragEnter and remove it in dragLeave. In the future you maybe can use classMap directive in lit-html, however there is nothing wrong with just using classList. I would stick with just using classList. In a very distant future css might also have a selector for it: Is there a CSS ":drop-hover" pseudo-class?
I think that, in order to solve the problem in a "lit-html style", the solution has to be something like this:
import { html, render} from 'lit-html';
import { classMap } from 'lit-html/directives/class-map.js';
const myBtnClasses = {
over: false
};
function dragEnter(e) {
myBtnClasses.over = true;
renderFiles();
}
function dragLeave(e) {
myBtnClasses.over = false;
renderFiles();
}
const filesTemplate = (classes) =>
html`
<button id="app" class="${classMap(myBtnClasses)}"
#dragover=${dragEnter} #dragleave=${dragLeave}
>
Click Me
</button>
`;
function renderFiles() {
render(filesTemplate(myBtnClasses), YOUR_CONTAINER);
}
When using lit-html you have to express your UI as a function of your "state" and "rerender" each time your state changes, so the best solution in this little example is to consider your classes as part of your state.
Anyway better than
this.view.element.className += ' over';
is
this.view.element.classList.add('over');
And instead
this.view.element.className = element.className.replace(' over', '');
use
this.view.element.classList.remove('over');
This is better because of allowing to avoid many bugs like adding the same class many times.
I do not know lit-html but try
let sayHello = (name, myClass) => html`<h1 class="${myClass}">Hello ${name}</h1>`;
https://lit-html.polymer-project.org/
This is for both regular DOM elements and jQuery elements. I want the actual elements to be stored some where in the class. Is there a reference to a common pattern for doing this?
Also, I'm aware that in general you should not use jQuery with React but I need the jQuery functions to make my menu work and don't have time for a refactor but it works fine.
import React from 'react';
import $ from 'jquery';
class MobileMenu extends React.Component {
constructor (props) {
super(props);
this.clickHandler1 = this.clickHandler1.bind(this);
this.clickHandler2 = this.clickHandler2.bind(this);
this.clickHandler3 = this.clickHandler3.bind(this);
}
clickHandler1 () {
this.toggleMenu();
}
clickHandler2 () {
// MenuPage.flip('fave');
this.toggleMenu();
this.toggleMarker($A.el('#nav_fave'));
}
clickHandler3 () {
// MenuPage.flip('splash');
this.toggleMenu();
this.toggleMarker($A.el('#nav_splash'));
}
toggleMarker (current_item) {
if ((this.previous_item !== undefined) && (this.previous_item !== current_item)) {
this.previous_item.style.borderBottom = '';
}
if (this.previous_item !== current_item) {
current_item.style.borderBottom = '3px solid #31baed';
}
this.previous_item = current_item;
}
toggleMenu () {
if (window.getComputedStyle($A.el('#top_menu_list'), null).display === 'block') {
$('#icon_bars').toggleClass('active');
$('#top_menu').slideToggle();
}
}
// ... snip
}
export default MobileMenu
You can try the ref pattern.
Keep in mind that you are using react the wrong way, one of its best features is the super fast rendering due to the virtual DOM and Diff algorithm.
Which you are ruing!!! :)
EDIT as a followup to your comment
The ref attribute will allow you to add an object to your react component class, this object is a reference to the actual DOM element.
So basically you can wrap it with a jQuery method and do whatever you need.
With That being said, again i really advice against that!
An example:
class App extends React.Component {
constructor(props){
super(props);
this.changeColor = this.changeColor.bind(this);
}
changeColor(e){
$(this.myelement).addClass('highlight');
}
render() {
return (
<div>
<h3>Do not use jQuery with React!</h3>
<div ref={(el) => {this.myelement = el}} onClick={this.changeColor}>
Click me!
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.highlight {
border: 2px solid #eee;
background: #333;
color: #ccc;
width: 100px;
height: 50px;
text-align:center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
EDIT #2
You are now asking how to modify elements (adding a css class for example) in react.
Well this is entirely a different question which i'm sure there are several good answers on SO, but i will give you a small example anyway.
I'm using the same code as the example above but now i'm doing it without jQuery.
As for css and styling in general with react i urge you to read about the different approaches which i won't explain here (css modules, styled components etc..).
Keep in mind that this is a really small and simple example and i used 1 of many patterns out there to tackle this challenge.
const MyComponent = ({highlight, onClick}) => {
const cssClassName = highlight && 'highlight'; // this will be undefined or 'highlight'
return(
<div className={cssClassName} onClick={onClick} >Click me for a COMPONENT example!</div>
);
}
class App extends React.Component {
constructor(props){
super(props);
this.state = {
isDivClicked: false,
isComponentClicked: false
}
this.changeDiv = this.changeDiv.bind(this);
this.changeComponent = this.changeComponent.bind(this);
}
changeDiv(e){
this.setState({isDivClicked: true});
}
changeComponent(e){
this.setState({isComponentClicked: true});
}
render() {
const {isDivClicked, isComponentClicked} = this.state;
return (
<div>
<h3>Do not use jQuery with React!</h3>
<div onClick={this.changeDiv} className={isDivClicked && 'highlight'}>
Click me for a simple DIV example!
</div>
<hr/>
<MyComponent onClick={this.changeComponent} highlight={isComponentClicked}/>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.highlight {
display:inline-block;
border: 2px solid #eee;
background: #333;
color: #ccc;
width: auto;
height: 50px;
text-align:center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Why not use a basic object array and push them by ID?
I created a simple demo. Maybe there is a more elegant way to do this with react, but using jquery as a helper this is pretty easy.
https://jsfiddle.net/cfnb4fkw/
var domCache = new Array();
function cacheDomObj(domObj){
domCache[$(domObj).attr('id')] = domObj;
}
function getDomObj(id){
return domCache[id];
}
function demo(){
cacheDomObj($("#domElement1"));
alert("Element cached" + domCache.length);
$("#domElement1").remove();
alert("Removed element");
var retreivedElement = getDomObj('domElement1');
$("#container").append(retreivedElement);
}
demo();
You could integrate this into your class and speed it up a little if you wanted to make cacheDomObj more verbose...
function cacheDomObj(id,obj){
domCach[id] = obj;
}