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} />
);
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.
Im trying to toggle the displaying of message using a button.
Below is my code.
class DisplayMessage extends PolymerElement {
// DO YOUR CHANGES HERE
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<h2>Hello [[prop1]]!</h2>
<button on-click="toggle">Toggle Message</button> <br />
<template is="dom-if" if="{{user.authorise }}">
<br />
<span>I should now display message.</span>
</template>
`;
}
toggle() {
// DO YOUR CHANGES HERE
// hint: use set method to do the required changes
//console.log(this.user);
//this.user = !this.user
}
static get properties() {
return {
prop1: {
type: String,
value: 'user',
},
user: {
type: Object,
value: function () {
return { authorise: false}; // HINT: USE BOOLEAN VALUES TO HIDE THE MESSAGE BY DEFAULT
},
notify: true,
},
};
}
}
window.customElements.define('display-message', DisplayMessage);
I tried thinking for like hours, but couldn't solve. The requirement her is on clicking the button, the click handler toggle should change the value of authorize in user property to true. And on clicking again to false and so on. I need to use set method within toggle method. I'm not getting how to do this. Please help me on this.
Thanks in advance.
Why use a library/dependency for such a small component, that can be done with native code
<display-message id=Message name=Cr5>You are authorized</display-message>
<script>
customElements.define("display-message", class extends HTMLElement {
static get observedAttributes() {
return ["name", "authorized"]
}
connectedCallback() {
this.innerHTML = `<h2>Hello <span>${this.getAttribute("name")}</span></h2><button>Toggle message</button><br><div style=display:none>${this.innerHTML}</div>`;
this.querySelector('button').onclick = () => this._toggle();
}
_toggle(state) {
let style = this.querySelector('div').style;
style.display = state || style.display == "none" ? "inherit" : "none";
this.toggleAttribute("authorized", state);
console.log(Message.name, Message.authorized);
}
get name() { return this.getAttribute("name") }
set name(value) {
this.querySelector('span').innerHTML = value;
this.setAttribute("name", value);
}
get authorized() { return this.hasAttribute("authorized") }
set authorized(value) { this._toggle(value) }
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue) this[name] = newValue;
}
})
Message.name = "Cr5";
Message.authorized = true;
</script>
class DisplayMessage extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<h2>Hello [[prop1]]!</h2>
<button on-click="toggle">Toggle Message</button> <br />
<template is="dom-if" if="{{user.authorise}}">
<br />
<span>I should now display message.</span>
</template>
`;
}
toggle() {
if(this.user.authorise==false)
{
this.set('user.authorise', true);
}
else
{
this.set('user.authorise', false);
}
}
static get properties() {
return {
prop1: {
type: String,
value: 'user',
},
user: {
type: Object,
value: function () {
return { authorise: false };
},
},
};
}
}
window.customElements.define('display-message', DisplayMessage);
I have a loop where I loop over an Array.
for each item in this array I render a new component. Now when a user clicks on a certain component I only want to add a class to that component to highlight it and remove it from others that have it. Think of it as a menu active item.
<step-icon
v-for="(step, currentStep) in steps"
/>
data() {
return {
steps: [{foo: 'bar'}, {foo2: 'bar2'}]
}
}
my step-icon.vue:
<template>
<div :class="{'selected': selected}" #click="clickStep()">
hello
</div>
</template>
data() {
return {
selected: false
}
},
methods: {
clickStep() {
this.selected = true;
}
}
This works only 1 way, I can only add the selected class but never remove it.
I created a simple example illustrating your use case since you didn't provided enough detail to go with. Below you can find the items selected and unselected. Firstly, we added a key isSelected and set it to false as default. This will act as a status for all items.
steps: [
{key:"0", tec:"foo", isSelected:false},
{key:"1", tec:"bar", isSelected:false},
{key:"2", tec:"foo2", isSelected:false},
{key:"3", tec:"bar2", isSelected:false},
]
Next, we looped over the array and displayed all the items.
<ul>
<li
v-for="l in steps"
id="l.key"
#click="select(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
Here you can se we have set our status property isSelected on v-bind directive which will add or remove the class based on the value of isSelected.
Next, once the item is clicked we will trigger select method.
methods: {
select(key) {
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].key !== key) {
this.steps[i].isSelected = false
}
}
this.toggleSelection(key)
},
toggleSelection(key) {
const stepsItem = this.steps.find(item => item.key === key)
if (stepsItem) {
stepsItem.isSelected = !stepsItem.isSelected
}
}
}
The select method will firstly unselect all those except the one which is selected and then call toggleSelection which will set the selected Item to true or false.
Complete Code:
new Vue({
el: '#app',
data: {
steps: [
{key:"0", tec:"foo", isSelected:false},
{key:"1", tec:"bar", isSelected:false},
{key:"2", tec:"foo2", isSelected:false},
{key:"3", tec:"bar2", isSelected:false},
]
},
methods: {
select(key) {
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].key !== key) {
this.steps[i].isSelected = false
}
}
this.toggleSelection(key)
},
toggleSelection(key) {
const stepsItem = this.steps.find(item => item.key === key)
if (stepsItem) {
stepsItem.isSelected = !stepsItem.isSelected
}
}
}
})
.selected {
background: grey;
}
.notselected {
background:transparent;
}
<script src="https://unpkg.com/vue#2.6.10"></script>
<div id="app">
<ul>
<li
v-for="l in steps"
id="l.key"
#click="select(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
You can keep all the step state in the parent component.
Now the parent component can listen for toggle_selected event from the nested one, and call toggle_selected(step) with the current step as a param.
Toggle_selected method should deselect all steps except the current one, and for the current one just toggle the selected prop.
If You would like to modify more props of the step in the nested component You could use .sync modifier (:step.sync="step") and then this.#emit('update:step', newStepState) in the nested component.
I've also made a snippet (my first). In this example I omitted clickStep and just put #click="$emit('toggle_selected') in the step-icon component.
new Vue({
el: '#app',
// for this example only defined component here
components: {
'step-icon': {
props: { step: Object }
}
},
data: {
steps: [
{ name: 'Alfa', selected: false},
{ name: 'Beta', selected: false},
{ name: 'Gamma', selected: false},
]
},
methods: {
toggle_selected(step) {
this.steps.filter(s => s != step).forEach(s => s.selected = false);
step.selected = true;
}
}
})
#app {
padding: 2rem;
font-family: sans-serif;
}
.step-icon {
border: 1px solid #ddd;
margin-bottom: -1px;
padding: 0.25rem 0.5rem;
cursor: pointer;
}
.step-icon.selected {
background: #07c;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<!-- Ive used inline-template for this example only -->
<div id="app">
<step-icon v-for="(step, currentStep) in steps" :key="currentStep"
:step.sync="step"
#toggle_selected="toggle_selected(step)" inline-template>
<div class="step-icon"
:class="{selected: step.selected}"
#click="$emit('toggle_selected')">
{{ step.name }}
</div>
</step-icon>
</div>
I'm looking to loop through an array of span tags and add is-active to the next one in line, every 3 seconds. I have it working but after the first one, it adds all the rest. How do I just pull that class from the active one and add it to the next array item?
I've read through the official documentation several times and there doesn't seem to be any mention of iterating individual items, just listing them all or pushing an item onto the list.
I'm not sure if 'index' comes in to play here, and how to grab the index of the span element to add/subtract is-active. what am I doing wrong?
var firstComponent = Vue.component('spans-show', {
template: `
<h1>
<span class="unset">Make</span>
<br>
<span class="unset">Something</span>
<br>
<span v-for="(span, index) of spans" :class="{ 'is-active': span.isActive, 'red': span.isRed, 'first': span.isFirst }" :key="index">{{ index }}: {{ span.name }}</span>
</h1>
`,
data() {
return {
spans: [
{
name: 'Magical.',
isActive: true,
isRed: true,
isFirst: true
},
{
name: 'Inspiring.',
isActive: false,
isRed: true,
isFirst: true
},
{
name: 'Awesome.',
isActive: false,
isRed: true,
isFirst: true
}
]
};
},
methods: {
showMe: function() {
setInterval(() => {
// forEach
this.spans.forEach(el => {
if (el.isActive) {
el.isActive = false;
} else {
el.isActive = true;
}
});
}, 3000);
}
},
created() {
window.addEventListener('load', this.showMe);
},
destroyed() {
window.removeEventListener('load', this.showMe);
}
});
var secondComponent = Vue.component('span-show', {
template: `
<span v-show="isActive"><slot></slot></span>
`,
props: {
name: {
required: true
}
},
data() {
return {
isActive: false
};
}
});
new Vue({
el: "#app",
components: {
"first-component": firstComponent,
"second-component": secondComponent
}
});
.container {
position: relative;
overflow: hidden;
width: 100%;
}
.wrapper {
position: relative;
margin: 0 auto;
width: 100%;
padding: 0 40px;
}
h1 {
font-size: 48px;
line-height: 105%;
color: #4c2c72;
letter-spacing: 0.06em;
text-transform: uppercase;
font-family: archia-semibold, serif;
font-weight: 400;
margin: 0;
height: 230px;
}
span {
position: absolute;
clip: rect(0, 0, 300px, 0);
}
span.unset {
clip: unset;
}
span.red {
color: #e43f6f;
}
span.is-active {
clip: rect(0, 900px, 300px, -300px);
}
<div id="app">
<div class="container">
<div class="wrapper">
<spans-show>
<span-show></span-show>
</spans-show>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
To achieve desired result, I'd suggest to change the approach a bit.
Instead of changing value of isActive for individual items, we can create a variable (e.g. activeSpan, that will be responsible for current active span and increment it over time.
setInterval(() => {
// Increment next active span, or reset if it is the one
if (this.activeSpan === this.spans.length - 1) {
this.activeSpan = 0
} else {
this.activeSpan++
}
}, 3000);
In component's template, we make class is-active conditional and dependent on activeSpan variable:
:class="{ 'is-active': index === activeSpan, 'red': span.isRed, 'first': span.isFirst }"
If you still need to update values inside spans array, it can be done in more simple way, via map for example. Also included such case as optional in solution below.
Working example:
JSFiddle
Sidenote: there is no need to add window listeners for load event, as application itself is loaded after DOM is ready. Instead, method can be invoked inside created hook. It is included in solution above.
I am trying to create custom <input type="file"> upload button with the name of the uploaded file visible on the button itself after the upload, in React. I am creating this as the component. I found it very difficult to create a codepen demo so I am just uploading the code here (sorry for that).
import React, { Component, PropTypes } from 'react';
import './InputFile.css';
export default class InputFile extends Component {
constructor(props: any)
{
super(props);
this.getUploadedFileName = this.getUploadedFileName.bind(this);
}
getUploadedFileName(selectorFiles: FileList, props) {
const { id } = this.props;
;( function ( document, window, index )
{
var inputs = document.querySelectorAll(`#${id}`);
Array.prototype.forEach.call( inputs, function( input )
{
var label = input.nextElementSibling,
labelVal = label.innerHTML;
input.addEventListener( 'change', function( e )
{
var fileName = '';
if( this.files && this.files.length > 1 )
fileName = ( this.getAttribute( 'data-multiple-caption' ) ||
'' ).replace( '{count}', this.files.length );
else
fileName = e.target.value.split( '\\' ).pop();
if( fileName )
label.querySelector( 'span' ).innerHTML = fileName;
else
label.innerHTML = labelVal;
});
// Firefox bug fix
input.addEventListener( 'focus', function(){ input.classList.add(
'has-focus' ); });
input.addEventListener( 'blur', function(){ input.classList.remove(
'has-focus' ); });
});
}( document, window, 0 ));
}
render () {
const { id, text, multiple } = this.props;
return(
<div>
<input id={id} type="file" className="km-btn-file" data-multiple-caption="{count} files selected" multiple={multiple} onChange={ (e, id) => this.getUploadedFileName(e.target.files, id)}></input>
<label htmlFor={id} className="km-button km-button--primary km-btn-file-label">
<span>{text}</span>
</label>
</div>
);
}
}
InputFile.propTypes = {
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
multiple: PropTypes.string,
};
I am importing this component in my other file <InputFile id={'input-file'} text={'Upload File'} multiple={'multiple'}/>
Here is the CSS code
.km-button--primary {
background-color: #5C5AA7;
color: #FFFFFF;
}
.km-button {
border-radius: 3px;
-webkit-appearance: none;
border: none;
outline: none;
background: transparent;
height: 36px;
padding: 0px 16px;
margin: 0;
font-size: 14px;
font-weight: 400;
text-align: center;
min-width: 70px;
transition: all 0.3s ease-out;
}
.km-btn-file {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.km-btn-file-label {
line-height: 36px;
cursor: pointer;
}
The problem I am facing is when I click on the button first time and choose a file to upload it selects the file but does not update the text "Upload File" with the name of the file. But after the click it the second time it works fine. I don't know why that is happening and for that I need help.
Thanks.
You can use the component 'state' to update your elements.
constructor(props: any)
{
super(props);
this.state = {message:'some initial message'};
}
and for the onChange event do:
getUploadedFileName = (e) => {
let files = e.target.files,
value = e.target.value,
message;
if( files && files.length > 1 ) message = `${files.length} files selected`;
else message = value.split( '\\' ).pop();
if(message) this.setState({...this.state,message});
}
and then in the element just bind the value to the state:
<div>
<input id={id} type="file" className="km-btn-file"
data-multiple-caption={this.state.message}
multiple={multiple}
onChange={this.getUploadedFileName}>
</input>
<label htmlFor={id} className="km-button km-button--primary km-btn-file-label">
<span>{text}</span>
</label>
</div>
You'll need to bind the text property from props to your state, so in your constructor you'll have to do;
this.state = {...props};
or
this.state = { text: props.text, id: props.id, multiple: props.multiple };
Then calling when you want to update the view value instead of manually setting the innerHtml on the label yourself;
this.setState({text : new value});
And in your render method;
const { id, text, multiple } = this.state;
What this does is when you call this.setState, it tells React to re-render your component which then takes the updated values from the state.