I have been looking everywhere and can't seem to find an answer so while this question may not contain a lot of code I was hoping that there was someone here with experience in subcomponents / Lit framework that could tell me how to unit test the components that you make.
Right now, I have the following element.
class SdkArticle extends LitElement {
static get properties() {
return {
data: {type: Object}
};
}
constructor() {
super();
this.data = {};
}
render() {
return html `
<style>
.row {
display: flex;
}
/* Create two equal columns that sits next to each other */
.column {
flex: ${100 / this.data.columnData.length}%;
padding: 10px;
height: 300px; /* Should be removed. Only for demonstration */
}
h2{
padding-left: 10px;
}
</style>
<div>
<h2>${this.data.title}</h2>
</div>
<div class="row">
${this.data.columnData.map(
columData =>
html`
${this.generateHtml(columData)}
`,
)}
</div>
`;
}
//Based on the settings we generate the extra html depending on the columns
generateHtml(columnData) {
switch (columnData.dataType) {
case 'image':
if (columnData.addon != null) {
if (columnData.addon !== null && columnData.addon.type === 'link') {
return html `<div class="column"><img src=${columnData.imageSrc}></div><div><sdk-button link=${columnData.addon.href}>${columnData.addon.text}</sdk-button></div>`;
}
else {
return html `<div class="column"><img src=${columnData.imageSrc}></div>`;
}
}
else {
return html `<div class="column"><img src=${columnData.imageSrc}></div>`;
}
case 'text':
if (columnData.addon != null && columnData.addon.type === 'link') {
return html `<div class="column"><p>${columnData.text} </p><sdk-button link=${columnData.addon.href}>${columnData.addon.text}</sdk-button> </div>`;
} else {
return html `<div class="column"><p>${columnData.text} </p> </div>`;
}
}
}
}
customElements.define('sdk-article', SdkArticle);
What framework do I use to test this? And how can I write a test to test my component?
Related
I am trying to sort an HTML table based on it's values in my Lit element. However, I'm running into a problem with my component. Here is an overview of my table;
The problem
In this application, you need to be able to sort on every table header. However, items which are considered 'done' need to move to the bottom of the table. My problem arises whenever I mark an item as done. In the following example I will mark the top todo (task: 123) as done. The expected behaviour is that the todo is moved to the bottom of the table with it's checkbox enabled. This is not however what is the outcome at the moment.
As you can see, the todo item with task 123 is moved to the bottom. However, the todo with task 456 also gets it's checkbox marked. This is not desired behaviour and I don't know what's causing it. You can also see that the colors are not correct (this is some styling to show you that a changed todo is being saved, yellow = saving, green = saved and red = error).
Things I have tried
Since I don't know what is exactly causing this issue I don't know what I should do. I gave all my inputs/rows/td's id's to make sure nothing gets mixed up, but that doesn't seem to work.
Code
import { LitElement, html, css } from 'lit-element';
class TableList extends LitElement {
static get properties() {
return {
data: {
type: Array
},
primaryKey: {
type: String
},
defaultSortKey: {
type: String
}
};
}
set data(value) {
let oldValue = this._data;
this._data = [ ... value];
this.sortByHeader();
this.requestUpdate('data', oldValue);
}
get data() {
return this._data;
}
async edit(entry, key, event) {
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('saved');
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('error');
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.add('saving');
if (entry[key].type === "checkbox") {
entry[key].value = event.target.checked;
} else {
entry[key].value = event.target.value;
}
if (await update(entry)) {
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('saving');
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.add('saved');
setTimeout(() => {
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('saved');
}, 1000);
} else {
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('saving');
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.add('error');
setTimeout(() => {
this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('error');
}, 5000);
}
}
sortByHeader(key) {
if (key === undefined) {
key = this.defaultSortKey;
}
let oldValue = this.data;
this._data = [ ... this.data.sort((a, b) => {
return a[this.defaultSortKey].value - b[this.defaultSortKey].value
|| a[key].value - b[key].value;
})];
this.requestUpdate('data', oldValue);
}
renderHeaders() {
let keys = Object.keys(this.data[0]);
return keys.map(key => html`
${this.data[0][key].visible ?
html`
<th id="${'header' + key}" #click="${() => this.sortByHeader(key)}">
${key}
</th>
`: ''
}
`)
}
renderRows() {
return this.data.map(entry => html`
<tr id="${'entry' + entry[this.primaryKey].value}">
${Object.keys(entry).map(key => html`
${entry[key].visible && !entry[key].editable ?
html`<td>${entry[key].value}</td>`
: ``
}
${entry[key].visible && entry[key].editable ?
html`<td id="${'td' + key + entry[this.primaryKey].value}">
<input
id="${'input' + key + entry[this.primaryKey].value}"
name="${'input' + key + entry[this.primaryKey].value}"
type="${entry[key].type}"
?checked="${entry[key].value}"
value="${entry[key].value}"
#change="${(event) => {
this.edit(entry, key, event)
}}"
/>
</td>`
: ``
}
`)}
</tr>
`)
}
render() {
return html`
<table id="table-list">
<thead>
<tr>
${this.renderHeaders()}
</tr>
</thead>
<tbody>
${this.renderRows()}
</tbody>
</table>
`;
}
static get styles() {
return css`
table {
width: 100%;
border-collapse: collapse;
font-family: Arial, Helvetica, sans-serif;
}
th {
padding-top: 12px;
padding-bottom: 12px;
text-align: center;
background-color: #4CAF50;
color: white;
}
tr {
text-align: right;
-moz-transition: all .2s ease-in;
-o-transition: all .2s ease-in;
-webkit-transition: all .2s ease-in;
transition: all .2s ease-in;
background: white;
padding: 20px;
}
.disabled {
color: lightgrey;
}
.saving {
background: yellow;
}
.saved {
background: lightgreen;
}
.error {
background: red;
}
.sort:after {
content: ' ↓';
}
`;
}
}
export default TableList;
If you are using array with id in template use repeat function of lit-html
import { repeat } from "lit-html/directives/repeat";
For styling don't add classes to DOM manually. Use classMap from lit-html
import { classMap } from "lit-html/directives/class-map";
I fixed your checkbox issue please follow this link.
For more information about lit-html please refer to their documentation.
Created a web component with a shadow DOM. When the button is clicked it adds the open attribute to the web component.
I would like to show the hidden div in the CSS when the open is added with CSS styling. Is it possible for the shadow DOM styles to reference attributes on the web component root? Otherwise, I have to add a superfluous class or attribute within the shadow DOM.
class CustomComponent extends HTMLElement {
constructor() {
super();
this.element = this.attachShadow({mode: 'open'});
}
static get observedAttributes() {
return ['open'];
}
attributeChangedCallback(attrName, oldValue, newValue) {
if (newValue !== oldValue) {
this[attrName] = this.hasAttribute(attrName);
}
}
connectedCallback() {
const template = document.getElementById('custom-component');
const node = document.importNode(template.content, true);
this.element.appendChild(node);
this.element.querySelector('button').addEventListener('click', () => {
this.setAttribute('open', '');
});
}
}
customElements.define('custom-component', CustomComponent);
<template id="custom-component">
<style>
div {
display: none;
}
[open] div {
display: block;
}
</style>
<button>Open</button>
<div>Content</div>
</template>
<custom-component></custom-component>
It appears the host CSS pseudo selector is designed to handle this precise situation.
class CustomComponent extends HTMLElement {
constructor() {
super();
this.element = this.attachShadow({mode: 'open'});
}
static get observedAttributes() {
return ['open'];
}
attributeChangedCallback(attrName, oldValue, newValue) {
if (newValue !== oldValue) {
this[attrName] = this.hasAttribute(attrName);
}
}
connectedCallback() {
const template = document.getElementById('custom-component');
const node = document.importNode(template.content, true);
this.element.appendChild(node);
this.element.querySelector('button').addEventListener('click', () => {
this.setAttribute('open', '');
});
}
}
customElements.define('custom-component', CustomComponent);
<template id="custom-component">
<style>
div {
display: none;
}
:host([open]) div {
display: block;
}
</style>
<button>Open</button>
<div>Content</div>
</template>
<custom-component></custom-component>
I am trying to create a rotating text animation using Vue.js and I used this CodePen as inspiration.
I got all the HMTL elements properly in place (i.e., as in the CodePen mentioned). In short:
each word is formed of several <span> elements, each containing one letter.
following a specific time interval, each <span> that holds a letter gets applied an .in and .out CSS class. This goes on indefinitely.
here is what it looks like in the DOM:
the problem is that no matter what CSS selectors I use, I can't target the .in and .out classes, unless I do it via Developer Tools in Chrome:
original output:
output after I added the classes in Developer Tools:
Here is the bare minimum code of my Vue Component:
<template>
<div id="app-loading">
<div class="words">
<span v-for="setting in settings" v-html="setting.lettersHTML" :id="setting.id" class="word"></span>
</div>
</div>
</template>
<script>
export default {
data() {
return {
settings: [
{ word: 'WordOne', id: 1, lettersArray: null, lettersHTML: null },
{ word: 'WordTwo', id: 2, lettersArray: null, lettersHTML: null }
],
currentWord: 1
}
},
created() {
this.splitLetters();
},
mounted() {
setInterval(this.changeWord, 1500);
},
methods: {
splitLetters() {
this.settings.forEach((setting) => {
let letters = [];
for (let i = 0; i < setting.word.length; i++) {
let letter = `<span class="letter">${ setting.word.charAt(i) }</span>`;
letters.push(letter);
}
setting.lettersArray = letters;
setting.lettersHTML = letters.join('');
});
},
changeWord() {
let current = document.getElementById(this.currentWord).getElementsByTagName('span');
let next = (this.currentWord == this.settings.length) ? document.getElementById(1).getElementsByTagName('span') : document.getElementById(this.currentWord + 1).getElementsByTagName('span');
// Animate the letters in the current word.
for (let i = 0; i < current.length; i++) {
this.animateLetterOut(current, i);
}
// Animate the letters in the next word.
for (let i = 0; i < next.length; i++) {
this.animateLetterIn(next, i);
}
this.currentWord = (this.currentWord == this.settings.length) ? 1 : this.currentWord + 1;
},
animateLetterOut(current, index) {
setTimeout(() => {
current[index].className = 'letter out';
}, index * 300);
},
animateLetterIn(next, index) {
setTimeout(() => {
next[index].className = 'letter in';
}, 340 + (index * 300));
}
}
}
</script>
<style lang="scss" scoped>
#app-loading {
font-size: 4rem;
}
.words, .word {
border: 1px solid rosybrown;
}
.letter {
text-decoration: underline; // Not working.
}
.letter.in {
color: red; // Not working.
}
.letter.out {
color: blue; // Not working.
}
</style>
What goes wrong that prevents these classes from being applied?
You're using v-html, but that doesn't work with scoped styles.
DOM content created with v-html are not affected by scoped styles, but you can still style them using deep selectors.
This worked for me:
<template>
<div class="a" v-html="content"></div>
</template>
<script>
export default {
data() {
return {
content: 'this is a <a class="b">Test</a>',
}
},
}
</script>
<style scoped>
.a ::v-deep .b {
color: red;
}
</style>
Yes,
v-html
doesn't work with scoped styles.
As Brock Reece explained in his article Scoped Styles with v-html, it should be solved like this:
<template>
<div class="a" v-html="content"></div>
</template>
<script>
export default {
data() {
return {
content: 'this is a <a class="b">Test</a>',
}
},
}
</script>
<style scoped>
.a >>> .b {
color: red;
}
</style>
Most answers are deprecated now that Vue3 is out.
Up-to-date usage of deep selector:
.letter{
&:deep(.in) {
color:blue;
}
&:deep(.out) {
color:red;
}
}
Vue3: In Single-File Components, scoped styles will not apply to content inside v-html, because that HTML is not processed by Vue's template compiler.
You can use :deep() inner-selector in Vue3 project.
Here is a example:
<script setup lang="ts">
import {onMounted,ref } from 'vue'
const content = ref("")
onMounted(()=>{
window.addEventListener('keydown',event =>{
content.value = `
<div class="key">
<span class="content">${event.keyCode}</span>
<small>event.keyCode</small>
</div>
`
})
})
</script>
<template>
<div class="container" v-html="content">
</div>
</template>
<style lang="scss" scoped>
.container{
display: flex;
:deep(.key){
font-weight: bold;
.content{
font-size: 1.5rem;
}
small{
font-size: 14px;
}
}
}
</style>
So let's say I am creating a simple web layout, where I have a feedback message component above the MainContent component, as so:
class WebLayout extends Component {
render() {
<div>
<Header />
<FeedBackMessage
shouldRenderMessage={true}
typeMessage={"error"}
message={"Wrong input!"}
/>
<MainContent />
</div>
}
}
And let's assume that I have different types of messages such as error, warning, success.
Inside the FeedBackMessage, I may have something as so:
class FeedBackMessage extends Component {
renderMessage(){
const {shouldRenderMessage, typeMessage, message } = this.props;
if (shouldRenderMessage === true){
<div>
{message}
</div>
}
}
render(){
return (
<div>
{this.renderMessage().bind(this)}
</div>
)
}
}
I am stumped on how I can render FeedBackMessage styling based on typeMessage prop value.
For instance, if I pass typeMessage with 'error', I like to have the FeedbackMessage component with a red border styling. Or if I pass confirm, I'd like to render with green border.
This all is very dependent on your styling solution.
If you want to use inline styles it might look something like this:
class FeedBackMessage extends Component {
renderMessage(){
const {shouldRenderMessage, typeMessage, message } = this.props;
if (shouldRenderMessage === true){
<div>
{message}
</div>
}
}
render(){
const componentStyle = {
error: { border: "1px solid red" },
confirm: { border: "1px solid green" }
}[this.props.typeMessage];
return (
<div style={componentStyle}>
{this.renderMessage().bind(this)}
</div>
)
}
}
If you want to style with stylesheets, you can use something like classnames to toggle classes based on some logic and then add the class your component.
class FeedBackMessage extends Component {
renderMessage(){
const {shouldRenderMessage, typeMessage, message } = this.props;
if (shouldRenderMessage === true){
<div>
{message}
</div>
}
}
render(){
const componentClass = classNames('FeedBackMessage', {
"error": this.props.typeName === 'error',
"confirm": this.props.typeName === 'confirm'
});
return (
<div className={componentClass}>
{this.renderMessage().bind(this)}
</div>
)
}
}
And have a stylesheet like so:
.FeedBackMessage .error {
border: 1px solid red;
}
.FeedbackMessage .confirm {
border: 1px solid green;
}
The official documentation will help you. Please check here
render() {
let className = 'menu';
if (this.props.isActive) {
className += ' menu-active';
}
return <span className={className}>Menu</span>
}
Got tired Firefox's ugly select and not being able to style it.
I thought I'd do one in React for learning purposes.
It seems to be easy to implement but I cannot figure out how to do onChange with custom component and how to get the value back with the event. If it is possible at all ...
The Select component looks like this:
type SelectProps = {
select: {
value: any
options: {
[k: string]: any
}
}
}
type SelectState = {
show: boolean
}
class Select extends Component<SelectProps, SelectState> {
constructor(props: SelectProps) {
super(props)
this.state = {
show: false
}
}
label = (v: any): string | undefined => {
for (var k in this.props.select.options) {
if (this.props.select.options[k] === v) return k
}
}
change = (i: number) => {
this.setState({ show: false })
this.props.select.value = this.props.select.options[this.keys[i]]
}
display = () => {
this.setState({ show: !this.state.show })
}
keys = Object.keys(this.props.select.options)
render() {
let { show } = this.state
let { options, value } = this.props.select
return (
<div className='select'>
<button onClick={this.display}>{this.label(value)}</button>
{!show ? null :
<ul>
{this.keys.map((e: string, i: number) => (
<li key={i} onClick={() => this.change(i)}>{e}</li>)
)}
</ul>
}
</div>
)
}
}
It works as expected. I can style it (hooray!).
I get the selected value from value parameter. I wondering though if I can get it with onChange event? So it behaves more like native select.
P.S.
This is styling of it (in stylus), in case it is needed
.select
display: inline-block
position: relative
background: white
button
border: .1rem solid black
min-width: 4rem
min-height: 1.3rem
ul
position: absolute
top: 100%
border: .1rem solid black
border-top: 0
z-index: 100
width: 100%
background: inherit
li
text-align: center
&:hover
cursor: pointer
background: grey
Thanks
As part of the props, pass in a change callback. In change, call the callback and pass in the new value:
type SelectProps = {
select: {
onChange: any, // change callback
value: any,
options: {
[k: string]: any
}
}
}
...
...
change = (i: number) => {
this.setState({ show: false })
this.props.select.value = this.props.select.options[this.keys[i]]
this.props.select.onChange(this.props.select.value); // call it
}
Then you can pass your change callback when outputting:
let s = {
value: '2',
options: {
'1' : 'one',
'2' : 'two'
},
onChange : function(val){
console.log("change to " + val);
}
};
return (
<Select select={s} />
);