Vue - Toggle class through multiple buttons - toggle

I have a Vue component, which displays a Vote button. When the user clicks on the Vote button an .active class gets added. Now I need to make sure only one Vote button can have this .active class at any given time.
What do I need to change in my code:
Vue.component('moustache', {
name: 'moustache',
props: ['type', 'img'],
template: `<li>
<p><strong>#{{ type }}</strong></p>
<img width="300" height="200" :src="img">
<button class="btn btn-primary" v-bind:class="{ active: isActive }" :data-type="type" #click="toggleClass">
Vote
</button>
</li>`,
data: function(){
return{
isActive: false
}
},
methods: {
toggleClass(){
this.isActive = !this.isActive;
}
}
});
new Vue({
el: '#app'
});

Each moustache component should only be in control of its own state. In the situation you describe, it would be best to let a parent component handle updating all the buttons when one is clicked.
In general:
Button is clicked in moustache component
Moustache component emits a "activated" event
The parent component listens for the "activated" event, and updates the state of all the buttons
Here is an example in code:
Vue.component('moustache', {
name: 'moustache',
props: ['type', 'img', 'isActive'],
template: `<li>
<p><strong>#{{ type }}</strong></p>
<img width="300" height="20" :src="img">
<button class="btn btn-primary" v-bind:class="{ active: isActive }" :data-type="type" #click="toggleClass">
Vote
</button>
</li>`,
methods: {
toggleClass() {
this.$emit('activate')
}
}
});
new Vue({
el: '#app',
data: {
buttons: [{
isActive: false,
type: 'Button 1',
img: null
}, {
isActive: false,
type: 'Button 2',
img: null
}, {
isActive: false,
type: 'Button 3',
img: null
}, ]
},
methods: {
activateButton: function(activatedButton) {
for (let button of this.buttons) {
button.isActive = button === activatedButton
}
}
}
});
.active { background-color: red; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.js"></script>
<div id="app">
<moustache v-for="button in buttons"
:is-active="button.isActive"
:type="button.type"
:src="button.src"
v-on:activate="activateButton(button)"></moustache>
</div>
Of course, this approach only works if all of Vote buttons can be controlled by the same parent component. If you require more complex behaviour, then it might be worth looking into Vuex. Then the state of your buttons could be managed by a vuex store.

Related

Ionic4 - ion-select does not correctly update after dynamically added option

The issue is that ion-select multi selection drop-down does not update (selection and as a consequence its control name) after dynamically updating underlying values list.
I have tried triggering change detection manually to pickup the changes but doesn't have an effect.
The following is the minimalistic snippet that illustrates the issue:
#Component({
selector: 'app-root',
template: `
<ion-button color="primary" (click)="AddItem()">Add Item</ion-button>
<ion-item>
<ion-label>Broken dropdown</ion-label>
<ion-select
multiple="true"
placeholder="Broken dropdown"
[compareWith]="compareWith"
>
<ion-select-option *ngFor="let item of stuff" [value]="item" [selected]="item.selected">
{{item.name}}
</ion-select-option>
</ion-select>
</ion-item>
`
})
export class AppComponent {
public stuff = [{ name: 'item1', selected: false }, { name: 'item2', selected: true }];
constructor(private platform: Platform, private splashScreen: SplashScreen, private statusBar: StatusBar) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
});
}
AddItem() {
this.stuff.push({ name: 'item3', selected: true });
}
compareWith(e1, e2) {
return e1 && e2 ? e1.name === e2.name : e1 === e2;
}
}
After clicking "Add Item" button I would expect the control name to be updated as well as after opening the drop-down the item2 and item3 would be selected.
I did workaround the issue by manually manipulating control but I would like to understand where the issue is that it doesn't update itself out of the box.
Thanks!

Reactjs how to get value from selected element

so I have this code for posting to my backend API. Normal form perfectly fine; I managed to post to my database. So I add a Cascader from Ant Design CSS Framework, and every time I selected the value, it produced an error
TypeError: Cannot read property 'value' of undefined
Here is the code:
import React from 'react';
import axios from 'axios';
import { Button, Cascader, Form, Input, Modal } from 'antd';
const FormProduct = Form.Item;
const computerType = [
{
value: 'computer',
label: 'Computer',
},
{
value: 'laptop',
label: 'Laptop',
}
]
export default class FormInventory extends React.Component {
state = {
category: '',
productname: '',
};
handleCategoryChange = event => { this.setState({ category: event.target.value }) }
handleProductNameChange = event => { this.setState({ productname: event.target.value }) }
handleSubmit = event => {
event.preventDefault();
axios.post('myapi',
{
category: this.state.category,
productname: this.state.productname,
})
.then(
function success() {
const modal = Modal.success({
title: 'Success',
content: 'Data successfully add',
});
setTimeout(() => modal.destroy(), 2000);
}
)
}
render() {
return (
<Form onSubmit={this.handleSubmit}>
<FormProduct {...formProductLayout} label="Computer Category">
<Cascader options={computerType} category={this.state.value} onChange={this.handleCategoryChange} />
</FormProduct>
<FormProduct {...formProductLayout} label="Product Name">
<Input type="text" productname={this.state.productname} onChange={this.handleProductNameChange} />
</FormProduct>
<FormProduct wrapperCol={{ span: 12, offset: 2 }}>
<Button type="primary" htmlType="submit">
Add Item
</Button>
</FormProduct>
</Form>
)
}
}
You need to either bind your event handlers in the constructor or use arrow function.
Option 1: Bind
constructor(props) {
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this);
}
Option 2: Arrow function
<Input onChange={(e) => this.handleChange(e)} />
According to antd docs you don't need event.target.
https://ant.design/components/cascader/
handleCategoryChange = category => { this.setState({ category }) }
The code above will work fine.

React setState from multiple input boxes

I'm working on a Recipe Box project and I have a program that allows the user to click a button which then displays input boxes that allows the user to add a new recipe to a list.
I have two inputs in the form. One for the name of the recipe being added, and one for the ingredients. The input for the name of the recipe works and allows the user to add a name, but the second input box is not updating the ingredients in state as it should.
Why isn't ingredientVal being updated in state? Why can't I enter text in the second input box?
import React, { Component } from 'react';
import RecipeList from './RecipeList';
import './App.css';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
items: ["Pumpkin Pie", "Spaghetti", "Onion Pie"],
ingredients:[
["Pumpkin Puree", "Sweetened Condensed Milk", "Eggs", "Pumpkin Pie Spice", "Pie Crust"],
["Noodles", "Tomato Sauce", "(Optional) Meatballs"],
["Onion", "Pie Crust"]
],
inputVal: '',
ingredientVal: '',
showRecipe: false
};
}
// Get text user inputs for recipe
handleChange = (event) => {
this.setState({inputVal: event.target.value});
};
handleIngredientChange = (event) => {
this.setState({ingredientVal: event.target.value});
}
// When user submits recipe this adds it
onSubmit = (event) => {
event.preventDefault()
this.setState({
inputVal: '',
items: [...this.state.items, this.state.inputVal],
ingredientVal: '',
ingredients: [...this.state.ingredients, this.state.ingredientVal]
});
}
onIngredientSubmit = (event) => {
event.preventDefault()
this.setState({
ingredientVal: '',
ingredients: [...this.state.ingredients, this.state.ingredientVal]
});
}
// Shows recipe
AddRecipe = (bool) => {
this.setState({
showRecipe: bool
});
}
render() {
return (
<div className="App">
<h3>Recipe List</h3>
<RecipeList items={this.state.items} ingredients={this.state.ingredients} />
<button onClick={this.AddRecipe}>Add New Recipe</button>
{ this.state.showRecipe ?
<div>
<form className="Recipe-List" onSubmit={this.onSubmit}>
<div className="Recipe-Item">
<label>Recipe Name</label>
<input
value={this.state.inputVal}
onChange={this.handleChange} />
</div>
<div className="Recipe-Item">
<label>Ingredients</label>
<input
value={this.state.ingredientVal}
onChange={this.state.handleIngredientChange} />
</div>
<button>Submit</button>
</form>
</div>
:null
}
</div>
);
}
}
You're calling this.state.handleIngredientChange as your onChange when it should be this.handleIngredientChange

Form fields lose focus when input value changes

I'm trying to build a form with conditional fields from a JSON schema using react-jsonschema-form and react-jsonschem-form-conditionals.
The components I'm rendering are a FormWithConditionals and a FormModelInspector. The latter is a very simple component that shows the form model.
The relevant source code is:
import React from 'react';
import PropTypes from 'prop-types';
import Engine from "json-rules-engine-simplified";
import Form from "react-jsonschema-form";
import applyRules from "react-jsonschema-form-conditionals";
function FormModelInspector (props) {
return (
<div>
<div className="checkbox">
<label>
<input type="checkbox" onChange={props.onChange} checked={props.showModel}/>
Show Form Model
</label>
</div>
{
props.showModel && <pre>{JSON.stringify(props.formData, null, 2)}</pre>
}
</div>
)
}
class ConditionalForm extends React.Component {
constructor (props) {
super(props);
this.state = {
formData: {},
showModel: true
};
this.handleFormDataChange = this.handleFormDataChange.bind(this);
this.handleShowModelChange = this.handleShowModelChange.bind(this);
}
handleShowModelChange (event) {
this.setState({showModel: event.target.checked});
}
handleFormDataChange ({formData}) {
this.setState({formData});
}
render () {
const schema = {
type: "object",
title: "User form",
properties: {
nameHider: {
type: 'boolean',
title: 'Hide name'
},
name: {
type: 'string',
title: 'Name'
}
}
};
const uiSchema = {};
const rules = [{
conditions: {
nameHider: {is: true}
},
event: {
type: "remove",
params: {
field: "name"
}
}
}];
const FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);
return (
<div className="row">
<div className="col-md-6">
<FormWithConditionals schema={schema}
uiSchema={uiSchema}
formData={this.state.formData}
onChange={this.handleFormDataChange}
noHtml5Validate={true}>
</FormWithConditionals>
</div>
<div className="col-md-6">
<FormModelInspector formData={this.state.formData}
showModel={this.state.showModel}
onChange={this.handleShowModelChange}/>
</div>
</div>
);
}
}
ConditionalForm.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.object.isRequired,
rules: PropTypes.array.isRequired
};
ConditionalForm.defaultProps = {
uiSchema: {},
rules: []
};
However, every time I change a field's value, the field loses focus. I suspect the cause of the problem is something in the react-jsonschema-form-conditionals library, because if I replace <FormWithConditionals> with <Form>, the problem does not occur.
If I remove the handler onChange={this.handleFormDataChange} the input field no longer loses focus when it's value changes (but removing this handler breaks the FormModelInspector).
Aside
In the code above, if I remove the handler onChange={this.handleFormDataChange}, the <FormModelInspector> is not updated when the form data changes. I don't understand why this handler is necessary because the <FormModelInspector> is passed a reference to the form data via the formData attribute. Perhaps it's because every change to the form data causes a new object to be constructed, rather than a modification of the same object?
The problem is pretty straightforward, you are creating a FormWithConditionals component in your render method and in your onChange handler you setState which triggers a re-render and thus a new instance of FormWithConditionals is created and hence it loses focus. You need to move this instance out of render method and perhaps out of the component itself since it uses static values.
As schema, uiSchema and rules are passed as props to the ConditionalForm, you can create an instance of FormWithConditionals in constructor function and use it in render like this
import React from 'react';
import PropTypes from 'prop-types';
import Engine from "json-rules-engine-simplified";
import Form from "react-jsonschema-form";
import applyRules from "react-jsonschema-form-conditionals";
function FormModelInspector (props) {
return (
<div>
<div className="checkbox">
<label>
<input type="checkbox" onChange={props.onChange} checked={props.showModel}/>
Show Form Model
</label>
</div>
{
props.showModel && <pre>{JSON.stringify(props.formData, null, 2)}</pre>
}
</div>
)
}
class ConditionalForm extends React.Component {
constructor (props) {
super(props);
this.state = {
formData: {},
showModel: true
};
const { schema, uiSchema, rules } = props;
this.FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);
this.handleFormDataChange = this.handleFormDataChange.bind(this);
this.handleShowModelChange = this.handleShowModelChange.bind(this);
}
handleShowModelChange (event) {
this.setState({showModel: event.target.checked});
}
handleFormDataChange ({formData}) {
this.setState({formData});
}
render () {
const FormWithConditionals = this.FormWithConditionals;
return (
<div className="row">
<div className="col-md-6">
<FormWithConditionals schema={schema}
uiSchema={uiSchema}
formData={this.state.formData}
onChange={this.handleFormDataChange}
noHtml5Validate={true}>
</FormWithConditionals>
</div>
<div className="col-md-6">
<FormModelInspector formData={this.state.formData}
showModel={this.state.showModel}
onChange={this.handleShowModelChange}/>
</div>
</div>
);
}
}
ConditionalForm.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.object.isRequired,
rules: PropTypes.array.isRequired
};
ConditionalForm.defaultProps = {
uiSchema: {},
rules: []
};
For anyone bumping into the same problem but using Hooks, here's how without a class :
Just use a variable declared outside the component and initialize it inside useEffect. (don't forget to pass [] as second parameter to tell react that we do not depend on any variable, replicating the componentWillMount effect)
// import ...
import Engine from 'json-rules-engine-simplified'
import Form from 'react-jsonschema-form'
let FormWithConditionals = () => null
const MyComponent = (props) => {
const {
formData,
schema,
uischema,
rules,
} = props;
useEffect(() => {
FormWithConditionals = applyRules(schema, uischema, rules, Engine)(Form)
}, [])
return (
<FormWithConditionals>
<div></div>
</FormWithConditionals>
);
}
export default MyComponent
Have you tried declaring function FormModelInspector as an arrow func :
const FormModelInspector = props => (
<div>
<div className="checkbox">
<label>
<input type="checkbox" onChange={props.onChange} checked={props.showModel}/>
Show Form Model
</label>
</div>
{
props.showModel && <pre>{JSON.stringify(props.formData, null, 2)}</pre>
}
</div>
)

kendo mvvm not updating after ajax call

I have a page (relevant code below) which carries out the following :
User enters a value into an auto-complete text box
2, Upon selecting an auto complete option, an ajax call is made in order to fill 2 dropdownlists
User is required to select a value from each dropdownlist
Once a value has been selected on both, they click on the add button and my bound table is updated
User can remove rows added to the table
The rows added in step 4 are contained in an array in the observable object.
The first time the page loads points 1 to 5 work as expected.....
However, if the user enters a new search into the auto-complete box and fires the select event, the second time the ajax call is made, the relationship between my viewmodel and UI objects are broken.
The code which is executing is identical so please could someone shed some light on why the second time around this breaks.
<input type="text" id="txtBox" style="width:300px;" />
<div id="fixturesBindable" style="padding:0 !Important;">
<table>
<thead>
<tr>
<th>Col1</th>
<th>Col2</th>
</tr>
</thead>
<tbody data-template="row-template" data-bind="source: Fixtures"></tbody>
</table>
<select id="a_teamsdropdown" data-role="dropdownlist" data-text-field="TeamFullName" data-value-field="Id" data-bind="source: Teams" style="width:200px;"></select>
<select id="a_oppteamsdropdown" data-role="dropdownlist" data-text-field="TeamFullName" data-value-field="Id" data-bind="source:
OpponentTeams" style="width:200px;"></select>
<button type="button" data-bind="click: addFixture">Add Fixture</button>
<script id="row-template" type="text/x-kendo-template">
<tr>
<td><input type="hidden" id="team" data-bind="attr: { name: TeamModelName, value: TeamId }" /></td>
<td><input type="hidden" id="oppteam" data-bind="attr: { name: OppModelName, value: OppTeamId }" /></td>
</tr>
</script>
</div>
<script>
$(document).ready(function () {
var viewModel = kendo.observable({
Teams: <%= Model.Teams %>,
OpponentTeams: [],
Fixtures: [],
addFixture: function (e) {
var Fixtures = this.get("Fixtures");
var teamId = $("#a_teamsdropdown").val();
var teamName = $("#a_teamsdropdown>option:selected").text();
var oppteamId = $("#a_oppteamsdropdown").val();
var oppteamName = $("#a_oppteamsdropdown>option:selected").text();
this.get("Fixtures").push({
TeamFullName: teamName,
TeamId: teamId,
OppTeamFullName: oppteamName,
OppTeamId: oppteamId,
OrderIndex: this.get("Fixtures").length,
TeamModelName: 'Fixtures[' + this.get("Fixtures").length + '].TeamId',
OppModelName: 'Fixtures[' + this.get("Fixtures").length + '].OpponentTeamId'
});
},
resetFixture: function(){
var Fixtures = this.get("Fixtures");
$.each(Fixtures, function (key, fixture) {
Fixtures.splice(0, 1);
});
}
});
opponents = $("#txtBox").kendoAutoComplete({
minLength: 3,
dataTextField: "Name",
filter: "contains",
dataSource: new kendo.data.DataSource({
transport: {
read: {
url: "/url/Ajax",
type: "POST",
data: function () { return { searchText: $("#txtBox").val()}
},
complete: function (data) {
opponents.list.width(400);
}
}
},
pageSize: 10,
serverPaging: true,
serverSorting: true,
schema: {
total: "count",
data: "data",
model: {
id: "Id",
fields: {
Id: { editable: false }
}
}
}
}),
change: function () {
this.dataSource.read();
},
select: function (e) {
$.each(opponents.dataSource.data(), function (index, value) {
if (e.item.text() == value.Name) {
selectedOpponent = value;
$('#Fixture_OpponentTeam_Id').val(selectedOpponent.Id);
$('#OpponentName').val(selectedOpponent.Name);
$.ajax({
url: 'GetOpponentTeams',
data: { schoolId: selectedOpponent.Id, seasonId: seasonId, sportId: sportsId },
type: 'GET',
success: function (data) {
viewModel.OpponentTeams = data;
kendo.bind($("#fixturesBindable"), viewModel);
},
error: function (xhr, ajaxOptions, thrownError) {
//alert('Error during process: \n' + xhr.responseText);
alert(thrownError);
}
});
return;
}
});
}
}).data("kendoAutoComplete");
</script>
Not sure if this will fix your issue or not, but in general I would advise against re-binding everything in your ajax success callback. If you just .set("OpponentTeams") instead of assigning the value directly, does that help?
success: function (data) {
viewModel.set("OpponentTeams", data);
},
The call to .set() should trigger a refresh of the #a_oppteamsdropdown element.