NativeScript-vue custom switch not working - vue.js

I'm trying to make a re-usable custom Switch component, but my v-model is not working. Here's the situation:
My component correctly emits the tap event
My component updates its data correctly
However, the parent data doesn't get updated despite being hooked to the child with a v-model
Here are some snippets showing my setup:
// MY COMPONENT
<template>
<Switch
dock="right"
backgroundColor="red"
offBackgroundColor="yellow"
v-model="model"
/>
</template>
<script>
export default {
name: "SettingsSwitch",
props: {
value: {
type: Boolean,
required: true
}
},
data() {
return {
model: this.value
};
},
watch: {
model: function(value) {
this.$emit("tap", value);
}
}
};
</script>
In the example below, I have 2 Switches:
- A normal one that works and whose data gets updated
- The one linked to my child component and the data does not get updated
// My parent view
<template>
<ViewWrapper viewTitle="Change your settings" pageColor="tertiary">
<StackLayout class="content-wrapper">
<StackLayout class="category-wrapper">
<DockLayout class="field-wrapper" stretchLastChild="true">
<Switch v-model="myCheck" dock="right" #tap="test" />
<StackLayout>
<Label text="Mon label" class="field-label" />
<Label text="Ma valeur actuelle" class="field-value" />
</StackLayout>
</DockLayout>
<DockLayout class="field-wrapper" stretchLastChild="true">
<SettingsSwitch v-model="myCheck2" #tap="test2" />
<StackLayout>
<Label text="Mon label" class="field-label" />
<Label text="Ma valeur actuelle" class="field-value" />
</StackLayout>
</DockLayout>
</StackLayout>
</StackLayout>
</ViewWrapper>
</template>
<script>
import { ViewWrapper } from "#/components";
import SettingsSwitch from "./SettingsSwitch";
export default {
name: "Settings",
components: { ViewWrapper, SettingsSwitch },
data() {
return {
myCheck: false,
myCheck2: false
};
},
methods: {
test() {
console.log(this.myCheck);
},
test2(v) {
console.log("emit", v); // <--- Value changes each time
console.log("model", this.myCheck2); // <--- Value never changes
}
}
};
</script>
I've tried playing around with different setups, like removing the watch and directly calling a method that does the $emit but it doesn't seem to fix the issue
Any thoughts?

So I managed to fix my issue. My mistake was that I was emitting tap instead of input in my component. I feel stupid but I'm leaving this up instead someone struggles like I did
The bottom line is:
You can use v-model on any tag, but in order to update its value, it will need to receive an "input" event with some data. That's why my child component must perform this.$emit("input", value);

Related

Why can't apollo get data in first request?

I am using props to get the Id of birds.
When I first start the app and click to enter the second-page, data won't load.
It gives the following error;
JS: [Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
JS:
JS: found in
JS:
JS: ---> <Detail> at components/Detail.vue
JS: <Frame>
JS: <Root>
But after I click on button second time it loads properly.
I have created a repo showing the problem. Please check it here:
github repo
Working graphql playground
<template>
<Page>
<StackLayout>
<Label :text="bird.name" textWrap="true" />
<Button text="get data" #tap="getData" />
</StackLayout>
</Page>
</template>
<script>
import { gql } from "apollo-boost";
export default {
props: ["birdID"],
apollo: {
bird: {
query: gql`
query($id: ID!) {
bird(id: $id) {
id
name
}
}
`,
variables() {
return {
id: this.birdID,
};
},
},
},
methods: {
getData() {
console.log("bird data: ", this.bird);
},
},
mounted() {},
};
</script>
<style></style>
Is there a better way to get data?
Answer was correct
I have updated github repo with solution.
https://github.com/kaanguru/query-data-from-prop
The issue is that your template
<Label :text="bird.name" textWrap="true" />
attempts to display bird.name before your query has completed.
The Vue Apollo documentation shows a simple work-around by defining some initial defaults
You can initialize the property in your vue component's data hook
data: () => ({
bird: {
name: null
}
}),
Alternately, make use of the Loading state feature to conditionally render your components
<Label v-if="$apollo.loading" text="loading" textWrap="true" />
<StackLayout v-else>
<Label :text="bird.name" textWrap="true" />
<Button text="get data" #tap="getData" />
</StackLayout>

How to change vue data from outside?

I have a function outside of export default object. How can I make Stop button to change into play button after 1 second?
In Short: I want to change playingStatus into false out of scope.
Please check {N} PlayGround link for question
<template>
<Page>
<ActionBar title="Home" />
<ScrollView>
<StackLayout>
<Image v-show="!playingStatus" #tap="startFunc"
src="https://cdn3.iconfinder.com/data/icons/iconic-1/32/play_alt-512.png"
stretch="aspectFit" />
<Image v-show="playingStatus"
src="https://cdn3.iconfinder.com/data/icons/eightyshades/512/24_Stop-512.png"
stretch="aspectFit" />
</StackLayout>
</ScrollView>
</Page>
</template>
<script>
function playForAWhile() {
setTimeout(stop, 1000);
}
function stop() {
playingStatus =
false; // scope error... How to change vue data from outside?
}
export default {
data() {
return {
playingStatus: false
};
},
methods: {
startFunc() {
this.playingStatus = true;
playForAWhile();
}
}
};
</script>
<style scoped>
</style>
Or instead of changing variable from outside, how can I toggle images without vue object.
Thanks in advance.

Get input values from child components in Vue

I would like to retrieve all input values from my child components (client and advice, seen below), but not sure how to proceed.
client.vue
<template>
<div id="client">
<input type="text" v-model="client.name" />
<input type="text" v-model="client.code" />
</div>
</template>
<script>
export default {
data() {
return {
client: {
name: '',
code: '',
}
}
}
}
</script>
advice.vue
<template>
<div id="advice">
<input type="text" v-model="advice.foo" />
<input type="text" v-model="advice.bar" />
<div v-for="index in 2" :key="index">
<input type="text" v-model="advice.amount[index]" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
advice: {
foo: '',
bar: '',
amount:[]
}
}
}
}
</script>
Each component has more fields than the above example.
My home page (parent) looks as simple as:
<template>
<form id="app" #submit="printForm">
<clientInfo />
<advice />
<input type="submit" value="Print" class="btn" />
</form>
</template>
<script>
import clientInfo from "#/components/clientInfo.vue";
import advice from "#/components/advice.vue";
export default {
components: {
clientInfo,
advice
},
methods: {
printForm() {}
}
}
</script>
My first idea was to $emit, but not sure how to do that efficiently with more than 20 fields without attaching a #emitMethod="parentEmitMethod" to every single field.
My second idea was to have a Vuex store (as seen below), but I don't know how to save all the states at once and not sure if I should.
new Vuex.Store({
state: {
client: {
name:'',
code:''
},
advice: {
foo:'',
bar:'',
amount:[]
}
}
})
You could use FormData to get the values of the form's <input>s or <textarea>s that have a name attribute (anonymous ones are ignored). This works even if the form has nested Vue components that contain <input>s.
export default {
methods: {
printForm(e) {
const form = e.target
const formData = new FormData(form) // get all named inputs in form
for (const [inputName, value] of formData) {
console.log({ inputName, value })
}
}
}
}
demo
You could use v-model with your custom components. Let's say you want to use them like this:
<advice v-model="adviceData"/>
For this, you would need to watch for value changes on your input elements inside your advice component and then emit an input event with the values. This will update the adviceData binded property. One generic way to do this could be including a watcher inside your advice component, like this:
export default {
data() {
return {
advice: {
foo: '',
bar: '',
amount:[]
}
}
},
watch: {
advice: {
handler: function(newValue, oldValue) {
this.$emit('input', newValue);
},
deep: true,
}
},
}
This way you will not have to add a handler for each input field. The deep option must be included if we need to detect changes on nested data in the advice object.
I think you can achieve what you want is when the user writes something using#change this will trigger a method when the input value is changed, you could use a button instead or anything you want, like this:
The child component
<input type="text" v-model="advice.amount[index]" #change="emitCurrStateToParent ()"/>
You gotta add #change="emitCurrStateToParent ()" in every input you have.
emitCurrStateToParent () {
this.$emit("emitCurrStateToParent", this.advice)
}
Then in you parent component
<child-component v-on:emitCurrStateToParent="reciveDataFromChild ($event)"></child-component>
reciveDataFromChild (recivedData) {
// Do something with the data
}
I would use a button instead of #change, like a "Save" button idk, the same goes for vuex, you can use the #change event
saveDataAdviceInStore () {
this.$store.commit("saveAdvice", this.advice)
}
Then in the store
mutations: {
saveAdvice (state, advice) {
state.advice = advice
}
}

RadDataForm update example

I am looking for an example on the use of RadDataForm / Vue to update data in a vuex store. I have the data populating no problem, but I cannot find anywhere in the current documentation that explains how to trigger an update function when the data is updated. here is a simple example, how would I trigger the save() function when the data is updated and I click done in the form?
<StackLayout>
<StackLayout orientation="vertical" backgroundColor="lightgray">
<RadDataForm
:source="record"
/>
</StackLayout>
<script>
export default {
data() {}
},
methods:{
save(){
console.log('save')
},
}
computed:{
record (){
return this.$store.getters.record;
}
},
};
```
You just need to add a watcher to a local property, i recommend using mapState from Vuex.
You can have something like this:
computed: mapState({
record: this.$store.getters.record;
})
And also a watcher that sets a callback to a function, based on if a value has changed or not.
watch: {
record: function () {
// something to run when record changes
}
In summary, you have a local mapped variable from your store and a watcher that acts on whether the information has changed or not. Hope this helps
See: MapState Helper
See: Computed properties and watchers
Ok, I found the answer. This is not documented anywhere I can find in the nativescript/vue docs. I found: RadDataForm Commit Documentation. So, using that, I modified to (keeping mapState from Luis.) I added:
v-on:propertyCommitted="save" to RadDataForm
So, this works:
`<StackLayout>
<StackLayout orientation="vertical" backgroundColor="lightgray">
<RadDataForm
:source="record"
v-on:propertyCommitted="save"/>
</StackLayout>
<script>
export default {
data() {}
},
methods:{
save(){
console.log('save')
},
}
computed: mapState({
record(state){
return record = this.$store.getters.record;
}
};`
Here is a more detailed answer for this. obviously, I would set the store value in the save function.
<StackLayout>
<StackLayout orientation="vertical" backgroundColor="lightgray">
<RadDataForm
id="myDataForm"
:source="record"
v-on:propertyCommitted="save"/>
</StackLayout>
<script>
import { mapState } from 'vuex'
import { getViewById } from "tns-core-modules/ui/core/view";
export default {
data() {}
},
methods:{
save(){
console.log('save')
let data = args.object
var dataform = getViewById(data, "myDataForm");
console.log(dataform.editedObject)//<--updated Data Here
},
}
computed: mapState({
record(state){
return record = this.$store.getters.record;
}
};

How can I use dynamic components in a ListView

I am trying to populate a NativeScript-Vue ListView with templates that contain components of which their types are not known ahead of time. For example, this code does not work as NativeScript does not have a 'component' element but this suggests what I am trying to accomplish:
<ListView for="component in components">
<v-template>
<component :is="component" />
</v-template>
</ListView>
computed: {
components () {
return ['Label', 'Button'];
}
}
Yes, I know you can use if="" in a v-template, but in this case I do not know ahead of time what components need to be loaded in the ListView. In my case I am loading global components in a plugin, and these components will be referenced in the ListView.
Thanks #Manoj. Those wise words made me think, the template can't be dynamic but the contents of the v-template can be. Maybe not for everyone, but this code works for me:
// App.vue
<template>
<Page>
<ActionBar title="Welcome to NativeScript-Vue!"/>
<GridLayout columns="*" rows="400,*">
<ListView ref="lv" for="item in items">
<v-template>
<v-component :type="item.type" :options="item.options" />
</v-template>
</ListView>
</GridLayout>
</Page>
</template>
<script lang="ts">
import Vue from 'nativescript-vue'
import { Component } from 'vue-property-decorator'
import VComponent from './VComponent.vue'
#Component({
components: {
VComponent
}
})
export default class App extends Vue {
get items () {
return [
{type: 'Label', options: [{key: 'text', value: 'I am a Label'}, {key: 'color', value:'red'}] },
{type: 'Button', options: [{key: 'text', value:'I am a Button!'}]}
]
}
}
</script>
// VComponent.vue
<template>
<StackLayout ref="myLayout">
</StackLayout>
</template>
<script lang="ts">
import Vue from 'nativescript-vue'
import { Component, Prop } from 'vue-property-decorator'
import { StackLayout } from 'tns-core-modules/ui/layouts/stack-layout'
import { Label } from 'tns-core-modules/ui/label'
import { Button } from 'tns-core-modules/ui/button'
const classByClassName = {
'Label': Label,
'Button': Button
}
#Component
export default class CoolTemplate extends Vue {
#Prop() type!: string;
#Prop() options;
mounted () {
if (this.type) {
const myLayout = <StackLayout>((<Vue>this.$refs.myLayout).nativeView)
const component = new classByClassName[this.type]
for (var i = 0; i< this.options.length; i++) {
const option = this.options[i];
component[option.key] = option.value
}
myLayout.addChild(component)
}
}
}
</script>
Your template can not be dynamic, that's the whole point of using ListView - keeping them static, so they can be reused as needed. If you like to see different components based on data then you must use multiple templates.
Read more on docs.