Component $emit seems to stop event propagation in Bootstrap Vue table - bootstrap-vue

I'm trying to do something like this in a BV table to send the result of a row click to a parent component using the row-clicked event:
<template>
<div>
<b-table :items='totals' selectable select-mode='single' hover #row-clicked='rowClicked'></b-table>
</div>
</template>
<script>
export default {
data: function() {
return { }
},
props: ['totals'],
methods: {
rowClicked(rowData) {
this.$emit("filter-total", rowData.total)
}
}
}
</script>
The $emit works fine, but seems to prevent the row-selected event, as the row never gets selected (which is confusing for the user). If I replace the $emit with something else, the row is selected correctly.
Why is this happening and how can I prevent it?

I implement your case in the code snippet, I think your problem is because of the bootstrap-vue version that you are using. Upgrade your bootstrap-vue package to the latest version and your problem should be solved.
Vue.component("customComponent", {
template:
"<b-table :items='items' selectable select-mode='single' hover #row-clicked='rowClicked' />",
data() {
return {
items: [
{
isActive: true,
age: 40,
first_name: "Dickerson",
last_name: "Macdonald"
},
{ isActive: false, age: 21, first_name: "Larsen", last_name: "Shaw" },
{ isActive: false, age: 89, first_name: "Geneva", last_name: "Wilson" },
{ isActive: true, age: 38, first_name: "Jami", last_name: "Carney" }
]
};
},
methods: {
rowClicked(items) {
this.$emit("filter-total", items.age);
}
}
});
new Vue({
el: "#app",
methods: {
do_sth(value) {
console.log(value);
}
}
});
<link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap#4.5.3/dist/css/bootstrap.min.css" />
<script src="https://unpkg.com/vue#2.6.12/dist/vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.min.js"></script>
<div id="app">
<custom-component #filter-total="do_sth" />
</div>

Related

How to handle multiple input forms in Vuex 4.x?

I have a Vue component with 5 input elements. As a exercise to learn VueX I wanted to manage the user input in a Vuex store. Let's assume each input represents a line in a poem. My state, mutation and actions look like that
state: {
poem: {
line1: '',
line2: '',
line3: '',
line4: '',
line5: '',
}
},
mutations: {
setPoem(state, line) {
state.poem = {...state.poem, ...line}
},
resetPoem(state) {
state.poem = {
line1: '',
line2: '',
line3: '',
line4: '',
line5: '',
}
}
},
actions: {
setPoem({commit}, line) {
commit('setPoem', line)
},
resetPoem({commit}) {
commit('resetPoem')
},
},
Looking the documentation I found that I could use v-model as usual but with a two-way computed property: https://next.vuex.vuejs.org/guide/forms.html#two-way-computed-property
But it seems not very DRY to create a computed property for each input element like to:
computed: {
line1: {
get() {
return this.$store.state.poem.line1;
},
set(value) {
this.$store.dispatch('setPoem', {line1: value})
}
},
line2: {
get() {
return this.$store.state.poem.line2;
},
set(value) {
this.$store.dispatch('setPoem', {line2: value})
}
},
line3: {
get() {
return this.$store.state.poem.line3;
},
set(value) {
this.$store.dispatch('setPoem', {line3: value})
}
},
line4: {
get() {
return this.$store.state.poem.line4;
},
set(value) {
this.$store.dispatch('setPoem', {line4: value})
}
},
line5: {
get() {
return this.$store.state.poem.line5;
},
set(value) {
this.$store.dispatch('setPoem', {line5: value})
}
}
},
My template looks like this:
<form class="form-group" v-on:submit.prevent="addDocument">
<input v-model="line1" type="text" />
<p class="error">{{errorMsg1}}</p>
<input v-model="line2" type="text" />
<p class="error">{{errorMsg2}}</p>
<input v-model="line3" type="text" />
<p class="error">{{errorMsg3}}</p>
<input v-model="line4" type="text" />
<p class="error">{{errorMsg4}}</p>
<input v-model="line5" type="text" />
<p class="error">{{errorMsg5}}</p>
<button type="submit">Send Poem</button>
</form>
How can I refactor this? Is there a best practice to manage state of multiple forms?
You can use vuex-map-fields
<script>
import { mapFields } from 'vuex-map-fields';
export default {
computed: {
...mapFields([
'poem.line1',
'poem.line2',
'poem.line3',
// ...
]),
},
};
</script>
and in your store, you can import the getField and updateField to fetch and mutate data
...
getters: {
getField,
},
mutations: {
updateField,
}

How to dynamically bind items of sap.m.Select

In a table, there is a drop-down for each row.
And every row has unique ID and based on it the values should be populated on UI
ID Country
1 India/Malasia/UK
2 Paris/spain/USA
3 Canada/Chile/China
So, I am trying to send the path of ObjectID.
The below code doesn't work. Not sure how to achieve this.
oEditTemplate = new Select({
forceSelection: false,
selectedKey: sPath,
items: {
path: {
path: "tempModel>ObjectId",
formatter: this._editableFormatter.bind(this, sName)
},
templateShareable: false,
template: new ListItem({
key: "{tempModel>value}",
text: "{tempModel>value}"
})
}
});
You can achieve it by using custom data and onAfterRendering. You can refer this JSBIN example
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv='Content-Type' content='text/html;charset=UTF-8'/>
<script src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
id="sap-ui-bootstrap"
data-sap-ui-libs="sap.ui.table,sap.m"
data-sap-ui-xx-bindingSyntax="complex"
data-sap-ui-theme="sap_bluecrystal"></script>
<script>
var ITEMS = {
"1": ["India", "Malasia", "UK"],
"2": ["Paris", "Spain", "USA"],
"3": ["Canada", "Chile", "China"]
};
sap.m.Select.extend("CustomSelect", {
metadata: {
properties: {
countryId: "string"
}
},
renderer: {}
});
var oSelect = new sap.m.Select({
customData: {
key: "countryId",
value: "{ID}"
}
});
oSelect.addEventDelegate({
onAfterRendering: function(oEvent) {
var src = oEvent.srcControl;
var countryId = src.data("countryId");
if (!!countryId && src.getItems().length === 0) {
ITEMS[countryId].forEach(function(i) {
src.addItem(new sap.ui.core.Item({
text: i,
value: i
}));
});
}
}
});
var oTable = new sap.ui.table.Table({
rows: '{/d/results}',
columns: [
new sap.ui.table.Column({
label: new sap.m.Label({text: "ID"}),
template: new sap.m.Text({text:"{ID}"}),
filterProperty: 'District'
}),
new sap.ui.table.Column({
label: new sap.m.Label({text: "Country"}),
template: oSelect
})]
});
var model = new sap.ui.model.json.JSONModel({
d: {
results: [
{ ID: "1"},
{ ID: "2"}
]
}
});
oTable.setModel(model);
oTable.placeAt('content');
</script>
</head>
<body id="content" class="sapUiBody sapUiSizeCompact">
</body>
</html>

vue managing form editing state, boilerplate code

I have a decent number of forms in my app, in which the user can choose to edit, revert, or save changes to the object, which are eventually saved to the backend.
Very similar to this: (code found in another question)
https://jsfiddle.net/k5j6zj9t/22/
var app = new Vue({
el: '#app',
data: {
isEditing: false,
user: {
firstName: 'John',
lastName: 'Smith',
}
},
mounted() {
this.cachedUser = Object.assign({}, this.user);
},
methods: {
save() {
this.cachedUser = Object.assign({}, this.user);
this.isEditing = false;
},
cancel() {
this.user = Object.assign({}, this.cachedUser);
this.isEditing = false;
}
}
})
Since v-model binding immediately changes the underlying object, I have to first create a clone of the object. Also I need to save a data member whether the object is in editing state.
Multiply this code for more forms and fields, and I end up with too much data members and a lot of boilerplate code.
In server frameworks like django, a model is in 'temporary state' until it is saved, so I can edit like this
user.first_name = 'aa' # temporary object in memory
user.save() # saved to the db
My question, is there a model component/pattern for vue to handle this task better?
Something that will hold the model state - i.e isEditing, automatically clone the object for form editing, revert the changes, etc.
So I won't have to write such code for so many objects?
Uses Scoped Slots may meet your requirements.
My solution:
Create one component with one slot
Then this slot will bind values with clonedValues (if closeMode is false, clondedValues = values)
finally, in parent component, generate your template with the properties of scoped slot, then pass it to the slot.
Like below demo:
Vue.component('child', {
template: `
<div>
<div>
<slot v-bind:values="clonedValues"></slot>
</div>
<p>
<button #click="saveAction(clonedValues)">Save</button>
<button #click="resetAction()">Reset</button>
</p>
</div>`,
props: {
'cloneMode': {
type: Boolean,
default: true
},
'values': {
type: Object,
default: () => { return new Object() }
},
'saveAction': {
type: Function,
default: function (newValues) {
this.$emit('save', newValues)
}
},
'resetAction': {
type: Function,
default: function () {
this.syncValues(this.values)
}
}
},
data() {
return {
clonedValues: {}
}
},
created: function () {
this.syncValues(this.values)
},
watch: {
values: {
handler: function (newVal) {
this.syncValues(newVal)
},
deep: true
},
cloneMode: function () {
this.syncValues(this.values)
}
},
methods: {
syncValues: function (newVal) {
this.clonedValues = this.cloneMode ? Object.assign({}, newVal) : newVal // if you'd like to support nested object, you have to deep clone
}
}
})
Vue.config.productionTip = false
app = new Vue({
el: "#app",
data: {
mode: true,
labels: ['id', 'name'],
childForm: {
'id': 1,
'name': 'test'
}
},
methods: {
saveForm: function (ev) {
Object.keys(this.childForm).forEach((item) => {
this.childForm[item] = ev[item]
})
// call backend to update the data
},
changeCurrentValue: function () {
this.childForm.id += '#'
this.childForm.name += '#'
}
}
})
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<p><button #click="mode=!mode">Mode: {{mode}}</button></p>
<p>Current: {{childForm}} --<button #click="changeCurrentValue()">Change Current</button></p>
<child :values="childForm" #save="saveForm($event)" :clone-mode="mode">
<template slot-scope="slotProps">
<p>ID: <input v-model="slotProps.values['id']"/></p>
<p>Name: <input v-model="slotProps.values['name']"/></p>
</template>
</child>
</div>
Edit for OP requested:
change default slot to named slot=edit, then create one slot=view
added data property=editing, if true, show 'Edit' slot, if false, show 'View' slot.
in parent component, design the template for slot=view.
Like below demo:
Vue.component('child', {
template: `
<div>
<div v-show="editing">
<slot name="edit" v-bind:values="clonedValues"></slot>
<button #click="saveForm(clonedValues)">Save</button>
<button #click="resetAction()">Reset</button>
</div>
<div v-show="!editing">
<slot name="view"></slot>
<button #click="editing = true">Edit</button>
</div>
</div>`,
props: {
'values': {
type: Object,
default: () => { return new Object() }
},
'saveAction': {
type: Function,
default: function (newValues) {
this.$emit('save', newValues)
}
},
'resetAction': {
type: Function,
default: function () {
this.syncValues(this.values)
}
}
},
data() {
return {
editing: false,
clonedValues: {}
}
},
created: function () {
this.syncValues(this.values)
},
watch: {
editing: function (newVal) {
if(newVal) this.syncValues(this.values)
},
values: {
handler: function (newVal) {
if(this.editing) this.syncValues(newVal) //comment out this if don't want to sync latest props=values
},
deep:true
}
},
methods: {
syncValues: function (newVal) {
this.clonedValues = Object.assign({}, newVal) // if you'd like to support nested object, you have to deep clone
},
saveForm: function (values) {
this.saveAction(values)
this.editing = false
}
}
})
Vue.config.productionTip = false
app = new Vue({
el: "#app",
data: {
childForm: {
'id': 1,
'name': 'test'
}
},
methods: {
saveForm: function (ev) {
Object.keys(this.childForm).forEach((item) => {
this.childForm[item] = ev[item]
})
// call backend to update the data
},
changeCurrentValue: function () {
this.childForm.id += '#'
this.childForm.name += '#'
}
}
})
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<p>Current: {{childForm}} --<button #click="changeCurrentValue()">Change Current</button></p>
<child :values="childForm" #save="saveForm($event)">
<template slot-scope="slotProps" slot="edit">
<h3>---Edit---</h3>
<p>ID: <input v-model="slotProps.values['id']"/></p>
<p>Name: <input v-model="slotProps.values['name']"/></p>
</template>
<template slot="view">
<h3>---View---</h3>
<p>ID: <span>{{childForm['id']}}</span></p>
<p>Name: <span>{{childForm['name']}}</span></p>
</template>
</child>
</div>

How to pass input value to form onSubmit without using state in component that renders multiple forms?

This is a bit of a longwinded problem and giving me a ton of headache to solve.
I'm making a voting app. On the page there will be a list of polls on which you can vote. Each poll is a form consisting of input radio buttons representing the different options available for that poll.
What I was doing previously was saving the option you choose to component state in this.state.value and then passing it as an argument to an action creator when the form is submitted.
Problem with this approach is that if I click an option of one poll, and then click submit on another poll, I've actually submitted the wrong option to the wrong poll.
Is there a way to pass input value to form onSubmit without storing it in component state?
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Loading from '../Loading';
class MyPolls extends Component {
constructor(props) {
super(props);
this.state = {
skip: 0,
isLoading: true,
isLoadingMore: false,
value: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
this.props.fetchMyPolls(this.state.skip)
.then(() => {
setTimeout(() => {
this.setState({
skip: this.state.skip + 4,
isLoading: false
});
}, 1000);
});
}
sumVotes(acc, cur) {
return acc.votes + cur.votes
}
loadMore(skip) {
this.setState({ isLoadingMore: true });
setTimeout(() => {
this.props.fetchMyPolls(skip)
.then(() => {
const nextSkip = this.state.skip + 4;
this.setState({
skip: nextSkip,
isLoadingMore: false
});
});
}, 1000);
}
handleSubmit(title, e) {
// console.log(e.target);
e.preventDefault();
const vote = {
title,
option: this.state.value
};
console.log(vote)
}
handleChange(event) {
this.setState({ value: event.target.value });
}
renderPolls() {
return this.props.polls.map(poll => {
return (
<div
className='card'
key={poll._id}
style={{ width: '350px', height: '400px' }}>
<div className='card-content'>
<span className='card-title'>{poll.title}</span>
<p>
Total votes: {poll.options.reduce((acc, cur) => { return acc + cur.votes }, 0)}
</p>
<form onSubmit={e => this.handleSubmit(poll.title, e)}>
{poll.options.map(option => {
return (
<p key={option._id}>
<input
name={poll.title}
className='with-gap'
type='radio'
id={option._id}
value={option.option}
onChange={this.handleChange}
/>
<label htmlFor={option._id}>
{option.option}
</label>
</p>
)
})}
<button
type='text'
className='activator teal btn waves-effect waves-light'
style={{
position: 'absolute',
bottom: '10%',
transform: 'translateX(-50%)'
}}
>
Submit
<i className='material-icons right'>
send
</i>
</button>
</form>
</div>
<div className='card-reveal'>
<span className='card-title'>{poll.title}
<i className='material-icons right'>close</i>
</span>
<p>
dsfasfasdf
</p>
</div>
</div>
)
})
}
render() {
return (
<div className='center-align container'>
<h2>My Polls</h2>
{this.state.isLoading ? <Loading size='big' /> :
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-evenly',
alignItems: 'center',
alignContent: 'center'
}}>
{this.renderPolls()}
</div>}
<div className='row'>
{this.state.isLoadingMore ? <Loading size='small' /> :
<button
className='btn red lighten-2 wave-effect waves-light' onClick={() => this.loadMore(this.state.skip)}>
Load More
</button>}
</div>
</div>
);
}
}
function mapStateToProps({ polls }) {
return { polls }
}
export default connect(mapStateToProps, actions)(MyPolls);
App demo: https://voting-app-drhectapus.herokuapp.com/
(use riverfish#gmail.com and password 123 to login)
Github repo: https://github.com/drhectapus/voting-app
I'm open to any suggestions. Thanks!
The more "React'ish" pattern would be to break it down to more components.
a Poll is a component, a PollOption could be a component as well.
Where each can handle the state internally.
This will allow you to keep global state in your App or some other state manager like redux that will hold all of your polls and each can reference to the selected option (id).
Another thing worth pointing, is that you tend to pass a new function reference on each render call.
For example:
onSubmit={e => this.handleSubmit(poll.title, e)}
This is considered as bad practice because you can interfere with the Reconciliation and The Diffing Algorithm of react.
When you break it down to components that each can fire back a callback with its
props, then you don't need to pass the handler this way.
Here is a small example with your data:
const pollsFromServer = [
{
_id: "5a0d308a70f4b10014994490",
title: "Cat or Dog",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-16T06:30:34.855Z",
options: [
{ option: "Cat", _id: "5a0d308a70f4b10014994492", votes: 0 },
{ option: "Dog", _id: "5a0d308a70f4b10014994491", votes: 0 }
]
},
{
_id: "5a0c7941e655c22b8cce43d7",
title: "Blonde or Brunette?",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:28:33.909Z",
options: [
{ option: "Blonde", _id: "5a0c7941e655c22b8cce43d9", votes: 0 },
{ option: "Brunette", _id: "5a0c7941e655c22b8cce43d8", votes: 0 }
]
},
{
_id: "5a0c7924e655c22b8cce43d4",
title: "Coke or Pepsi",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:28:04.119Z",
options: [
{ option: "Coke", _id: "5a0c7924e655c22b8cce43d6", votes: 0 },
{ option: "Pepsi", _id: "5a0c7924e655c22b8cce43d5", votes: 0 }
]
},
{
_id: "5a0c78c2e655c22b8cce43d0",
title: "Favourite german car?",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:26:26.724Z",
options: [
{ option: "BMW", _id: "5a0c78c2e655c22b8cce43d3", votes: 0 },
{ option: "Mercedes", _id: "5a0c78c2e655c22b8cce43d2", votes: 0 },
{ option: "Audi", _id: "5a0c78c2e655c22b8cce43d1", votes: 0 }
]
}
];
class Poll extends React.Component {
onSubmit = optionId => {
const { pollId, onSubmit } = this.props;
onSubmit(pollId, optionId);
};
render() {
const { title, options, selectedOption } = this.props;
return (
<div>
<h3>{title}</h3>
<ul>
{options.map((o, i) => {
return (
<PollOption
isSelected={selectedOption === o._id}
onClick={this.onSubmit}
name={o.option}
optionId={o._id}
/>
);
})}
</ul>
</div>
);
}
}
class PollOption extends React.Component {
onClick = () => {
const { optionId, onClick } = this.props;
onClick(optionId);
};
render() {
const { name, isSelected } = this.props;
const selectedClass = isSelected ? "selected" : '';
return (
<li
className={`poll-option ${selectedClass}`}
onClick={this.onClick}
>
{name}
</li>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
polls: pollsFromServer,
submittedPolls: []
};
}
onPollSubmit = (pollId, optionId) => {
this.setState({
submittedPolls: {
...this.state.submittedPolls,
[pollId]: optionId
}
});
};
render() {
const { polls, submittedPolls } = this.state;
return (
<div>
{polls.map((p, i) => {
const selectedPoll = submittedPolls[p._id];
return (
<Poll
selectedOption={selectedPoll}
pollId={p._id}
onSubmit={this.onPollSubmit}
title={p.title}
options={p.options}
/>
);
})}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.poll-option{
cursor: pointer;
display: inline-block;
box-shadow: 0 0 1px 1px #333;
padding: 15px;
}
.selected{
background-color: green;
color: #fff;
}
<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>

How can I bind kendo ui grid to its datasource in a this MVC SPA?

Using the following source as a template: https://github.com/Simba-Mupfunya/Kendo-UI-SPA-Template-vs2013-MVC5
I am trying to bind other Kendo UI controls like the grid and scheduler to the model in the javascript "code behind". What I have tried...
HTML:
<div data-role="grid"
date-scrollable="true"
data-editable="true"
data-toolbar="['create', 'save']"
data-columns="[
{ 'field': 'Name', 'width': 50 }
, { 'field': 'Phone' }
, { 'field': 'Email' }
]"
data-bind="source: contacts,
visible: isVisible,
events: {
save: onSave
}"
style="height: 200px">
</div>
js:
define([
'text!views/contacts/contacts.html'
], function (html) {
var contactDataSource = new kendo.data.DataSource({
data: [
{ Name: "Jim Dandy", Phone: "555-1234", Email: "jim.dandy#gmail.com" }
, { Name: "Joe Coffee", Phone: "555-1234", Email: "joe.coffee#gmail.com" }
, { Name: "Ham Son", Phone: "555-1234", Email: "ham.son#gmail.com" }
, { Name: "Dan Fooey", Phone: "555-1234", Email: "dan.foo#gmail.com" }
],
schema: {
model: {
fields: {
Name: { type: "string" }
, Phone: { type: "string" }
, Email: { type: "string" }
}
}
}
});
//contactDataSource.read();
var viewModel = kendo.observable({
title: 'Contacts'
, contacts: contactDataSource
});
kendo.bind(html, viewModel);
var view = new kendo.View(html, {
model: viewModel,
show: function (e) {
kendo.fx(this.element).fade('in').duration(500).play();
}
});
return view;
});
This doesn't answer your specific problem, however in your 'real' implementation, I think you'll need to get the data from a real datasource? In this case you probably want the data to come via an MVC or Web Api controller. To do this, you need to manually configure an AJAX call in your datasource. See this (you'll need to use the 'read' property of the 'transport' object that will automatically employ AJAX to make the call)
http://docs.telerik.com/kendo-ui/framework/datasource/basic-usage#creating-a-datasource-for-remote-data