Is there a way to stop per character updates in Ractive? - ractivejs

I would like to prevent Ractive from updating the DOM until after an iput field blur event. By default Ractive updates the DOM any time a character in typed into or removed from a field. Changing the field in question results in a metered API call, so reducing the number of calls is critical. I know I could display a field that is not part of the Ractive config.data, but that brinings in other issues.
Is this possible? I can't find anything in the Ractive docs.

It can be controlled via the lazy option, docs are here, which limits to change and blur events:
var ractive = new Ractive({
template: '<input value="{{foo}}">',
data: { foo: 'bar' },
lazy: true
});
// will not fire as user is typing
ractive.on('change', function(){
// only happens on exiting <input> or return if submit
console.log('changed!')
})
The lazy option can also be specified per element, not just at the component level.
<input value="{{foo}}" lazy="true" />
You can also specify a debouncing timeout treshhold:
<input value="{{foo}}" lazy="1000" />

Related

use the same form for create and read operation

I have a master and a child component. The child component persists the data for the create mode as well as the edit mode. The child has a data section as follows which is being used when the component is in create mode
data() {
return {
title: '',
description: '',
organizer: '',
startdate: '',
enddate: '',
email: '',
phone: ''
}
},
and my inputs in create mode are as follows
<input type="text" placeholder="enter event title here" class="form-control" v-model="title">
In the edit mode, I am updating a prop value on the client as follows, which is
props:['currentevent']
The value of the currentevent is being passed from the master component to the child component and is also the value that is currently being edited.
so, the complete code for handling an input value looks like as follows
<input type="text" placeholder="enter event title here" class="form-control" v-if="currentevent" :value="currentevent.title">
<input type="text" placeholder="enter event title here" class="form-control" v-else v-model="title">
and in my save method (in the child component), I am checking if currentevent is empty or not. If it is empty then I trigger the add code otherwise, I trigger the update code.
Question : This works , but I have a large form and having to do this for each and every component is not a clean design . Can you please let me know what should I be doing ?
I totally appreciate your predicament. The best way to handle form data is to make it create/update agnostic. Here's what I'd recommend you try:
Instead of maintaining all the data fields as disparate properties, contain them in a single object, in this case I'm calling it eventObj for clarity:
data () {
return {
eventObj: {}
}
}
Then in your markup you'd reference them via the object:
<input type="text" placeholder="..." class="form-control" v-model="eventObj.title">
You'd then need to define a prop for passing in the data (as an object) from the parent component if you are editing:
props: {
currentevent: Object
}
And then you'd just need to map the incoming prop to the child component's data:
created() {
Object.assign(this.eventObj, this.currentevent || {})
}
Now when your inputs like <input v-model="eventObj.title"> are processed, if there is a saved title (that was passed in with currentevent) the field will be prepopulated with it, otherwise it will be blank.
I think this should help you in the right direction toward solving the complexity you're trying to figure out. There are other logistical issues involved with this kind of stuff in general, but I won't drone on. :)
The issue I see is you want to remove the v-if/else in the form. I will recommend here is keep your local data of child to be in sync with the props passed and only use local variable in the form.
One way to do this can be put a watcher on props and whenever props changes, update local variables and only use those variables in form.
watch: {
currentevent: function(newVal){
title = newVal.title,\
description = newVal.description
...
}
}

VueJs child component props not updating instantly

I have a parent/child component setup where the parent is loading data from the server and passing it down to children via props. In the child I would like to instantiate a jQuery calendar with some of the data it receives from the parent.
In order to wait for the data before setting up the calendar, I broadcast an event in the parent that I have an event listener setup for in the child.
The listener is being fired in the child but if I this.$log('theProp'), it's undefined. However, if I inspect the components with the VueJs devtools, the parent/child relationship is there and the child has received the prop in the meantime.
The prop is defined on the child as a dynamic prop :the-prop="theProp". Since the child does receive the prop in the end, I'm assuming my setup is correct but there seems to be some sort of delay. The parent sets the props in the return function of the ajax call and again: it's working, just with a slight delay it seems.
I also tried registering a watch listener on the prop in the child so I could setup the calendar then and be sure that the prop is there. However, the watch listener fires, but this.$log('theProp') is still undefined.
If I pass the data along with the the broadcast call, like this.$broadcast('dataLoaded', theData) the child receives it just fine. But it seems wrong to do it that way as I'm basically building my own prop handler.
I'm not posting any code because the components are rather large and the VueJs devtools are telling me the parent/child situation is working.
Am I missing some information? Is there a slight delay between setting a value in the parent and the child receiving it? What would be the proper way to wait for parent data in the child?
Normally, when you're just rendering the data out into the template, the timing doesn't matter so much since the data is bound to the template. But in this case, I really need the data to be there to setup the calendar or it will be wrong.
Thanks.
edit 1: here's a jsfiddle: https://jsfiddle.net/dr3djo0u/1/
It seems to confirm that the data is not available immediately after the broadcast. However, the watcher does work, though I could almost swear that sometimes this.$log('someData') returned undefined when I setup that testcase.
But I guess my problem might be somewhere else, I'll have a look tonight, don't have the project with me right now.
edit 2: did some more tests. My problem was that a) event listeners do not seem to receive the data instantly and b) I was also trying to init the calendar in the route.data callback if someData was already around (e.g. when coming from parent), but that route callback is called before the component is ready, so it wasn't working there either.
My solution is now this:
// works when the child route is loaded directly and parent finishes loading someData
watch: {
someData() {
this.initCalendar();
}
},
// works when navigating from parent (data already loaded)
ready() {
if (this.someData && this.someData.length) {
this.initCalendar()
}
}
As far as I know, you should not need events to pass data from parent to child.
All you need is, in the child component: props: ['theProp']
And when using the child component in the parent: <child :theProp="someData"></child>
Now, wherever in the parent you change someData, the child component will react accordingly.
You don't need events, you don't need "watch", you don't need "ready".
For example: after an AJAX call, in the parent's "ready", you load some data:
// at the parent component
data: function () {
return {
someData: {}
}
},
ready: function () {
var vm = this;
$.get(url, function(response) {
vm.someData = response;
});
}
Now, you do not need anything else to pass the data to the child. It is already in the child as theProp!
What you really need to do is to have, in the child, something which reacts to data changes on its own theProp property.
Either in the interface:
<div v-if="theProp.id > 0">
Loaded!
</div>
Or in JavaScript code:
// at the child component
computed: {
// using a computed property based on theProp's value
awesomeDate: function() {
if (!this.theProp || (this.theProp.length === 0)) {
return false;
}
if (!this.initialized) {
this.initCalendar();
}
return this.theProp.someThing;
}
}
Update 1
You can also, in the parent, render the child conditionally:
<child v-if="dataLoaded" :theProp="someData"></child>
Only set dataLoaded to true when the data is available.
Update 2
Or maybe your issue is related to a change detection caveat
Maybe you're creating a new property in an object...
vm.someObject.someProperty = someValue
...when you should do...
vm.$set('someObject.someProperty', someValue)
...among other "caveats".
Update 3
In VueJS 2 you are not restricted to templates. You can use a render function and code the most complex rendering logic you want.
Update 4 (regarding OP's edit 2)
Maybe you can drop ready and use immediate option, so your initialization is in a single place:
watch: {
someData: {
handler: function (someData) {
// check someData and eventually call
this.initCalendar();
},
immediate: true
}
}
It's because tricky behavior in Vue Parent and Child lifecycle hooks.
Usually parent component fire created() hook and then mount() hook, but when there are child components it's not exactly that way: Parent fires created() and then his childs fire created(), then mount() and only after child's mount() hooks are loaded, parent loads his mount() as explained here. And that's why the prop in child component isn't loaded.
Use mounted() hook instead created()
like that https://jsfiddle.net/stanimirsp5/xnwcvL59/1/
Vue 3
Ok so I've spent like 1.5h trying to find out how to pass prop from parent to child:
Child
<!-- Template -->
<template>
<input type="hidden" name="_csrf_token" :value="csrfToken">
<span>
{{ csrfToken }}
</span>
</template>
<!-- Script -->
<script>
export default {
props: [
"csrfToken"
]
}
</script>
Parent
<!-- Template -->
<template>
<form #submit.prevent="submitTestMailForm" v-bind:action="formActionUrl" ref="form" method="POST">
...
<CsrfTokenInputComponent :csrf-token="csrfToken"/>
...
</form>
</template>
<!-- Script -->
<script>
...
export default {
data(){
return {
...
csrfToken : "",
}
},
methods: {
/**
* #description will handle submission of the form
*/
submitTestMailForm(){
let csrfRequestPromise = this.getCsrfToken();
let ajaxFormData = {
receiver : this.emailInput,
messageTitle : this.titleInput,
messageBody : this.bodyTextArea,
_csrf_token : this.csrfToken,
};
csrfRequestPromise.then( (response) => {
let csrfTokenResponseDto = CsrfTokenResponseDto.fromAxiosResponse(response);
this.csrfToken = csrfTokenResponseDto.csrToken;
this.axios({
method : "POST",
url : SymfonyRoutes.SEND_TEST_MAIL,
data : ajaxFormData,
}).then( (response) => {
// handle with some popover
})
});
},
/**
* #description will return the csrf token which is required upon submitting the form (Internal Symfony Validation Logic)
*/
getCsrfToken(){
...
return promise;
}
},
components: {
CsrfTokenInputComponent
}
}
</script>
Long story short
This is how You need to pass down the prop to child
<CsrfTokenInputComponent :csrf-token="csrfToken"/>
NOT like this
<CsrfTokenInputComponent csrf-token="csrfToken"/>
Even if my IDE keep me telling me yeap i can navigate with that prop to child - vue could not bind it.
solution (testing ok)
In child component just using the props data, no need to re-assignment props's values to data, it will be cause update bug!
vue child component props update bug & solution
https://forum.vuejs.org/t/child-component-is-not-updated-when-parent-component-model-changes/18283?u=xgqfrms
The problem is not how to pass data with props, but rather how to do two things at almost the same time.
I have an user account component that can edit users (with an user id) and add users (without id).
A child component shows checkboxes for user<->company assignments, and needs the user id to prepare API calls when the user account is saved.
It is important that the child component shows before saving the user account, so that things can be selected before the user is saved and gets an id.
So it has no user id at first: the id is passed to the child component as 'null'.
It updates when the user is stored and gets an id.
But at this point, it takes a very short time for the child to get the new id into its model.
If you call a function in the child component that relies on data that was just changing, it might happen that the function executes before the data is updated.
For cases like this, nextTick() is your friend.
import { nextTick } from 'vue';
...
saveAccount() {
axios.post(URL, this.userModel).then((result)) {
// our model gets an id when persisted
this.userModel.id=result.data.id;
nextTick( () => {
this.$refs.childComponent.doSomething();
});
}
}

How to deal with checkboxes in meteor

Following from this question it appears I have to go around the houses with Meteor just to flag a checkbox as checked - but I've been Googling for an hour now and I can't find any examples of how to actually do it.
I've created the base of a helper:
Template.Settings.helpers({
isChecked: function () {
var id = this.id;
console.log(id);
}
});
And here's the HTML:
<input type="checkbox" name="imaname" id="imanid" {{isChecked}} />
However I'm clearly misunderstanding the context of this as literally whatever I try returns undefined (this.target etc. all nada). No docs I can find elaborate on how to actually create the isChecked helper that I'm supposed to use, so I'm now lost and frustrated.
Where do I go from here?
Edit: OK so I have it working, however I'd still like to know what the context of this is (maybe a duh moment, I guess the context is the helper itself… how do I access the element that triggers the helper?) - so I can make it a little more dynamic:
Template.Settings.helpers({
isChecked: function (value) {
return (value == 'on' ? 'checked' : false);
}
});
Along with:
<input type="checkbox" checked={{ isChecked currentUser.profile.services.bananaExports }} />
I'd still like to know what the context of this is
In a Blaze template helper this refers to the data context where the helper was called, which is the data context of the template unless you've set the context to be something else. To learn about data contexts see Discover Meteor's Guide on the topic.
how do I access the element that triggers the helper
Blaze doesn't have any way of telling you where in the DOM a helper was called, it only has access to the data context and any arguments you pass to the helper.
If you really have to access the DOM node you need to use Template.instance().find('some query') or Template.instance().$('some query') for a JQuery object.
An alternative way of marking a checkbox as checked is to define it like this
<input type="checkbox" name="imaname" id="imanid" checked={{isChecked}} />
And then setting isChecked to true or false in your helper.
If I understand your question, here's my approach, instead of using helper, you better use blaze template event handler and jquery like below:
Template.Setting.event({
'click input'(event){
if($('#imanid').is(":checked") == true){
return true;
}else{
return false;
}
}
});
hope this helps

Force rerendering a component

What's the best way to completely teardown and reinstantiate a component in the old one's place, preferably from a template?
Our use case is we have a bunch of Backbone models/collections that are used in our views. In init we might listen to some of those model or collection events (that are sometimes deep), or we may do some sort of setup work relative to that model. It seems we have two options: listen for if the entire model property changes on the view and then unbind any events and bind them to the new model and redo any setup work, or force the view to teardown and put a new one in its place with the new model, since the template may change significantly or even completely. We chose the latter route due to the significance of the change and to ensure we start with a clean slate in the view.
Up to this point we've been wrapping the component in a conditional and changing a boolean to force the old component to teardown and a new one to rerender:
HTML
<p>Some stuff that isn't bound to the model: {{prop1}}, {{prop2}}</p>
{{#if isRenderable}}
<myComponent model="{{model}}" />
{{/if}}
JS
component.set('isRenderable', false); // force `myComponent` to teardown
component.set('model', aDifferentModel); // this often happens in/via template
component.set('isRenderable', true); // force a new `myComponent` to render
Is this a decent approach or are we looking at this all wrong? It seems like there has to be a better option, especially since is necessary in a few places in our app.
One way to do this would be to use the reset() method of the component to change the data, and include a dynamic template function to choose the appropriate template. One of the advantages is that it will not need to re-render the template unless data.type changes. (btw - the design behind the default for components to not re-render is that if the data is updating is more efficient to update the DOM values than to re-render everything. The falsey-block trick works to force a refresh - but that may not always be needed).
There are a lot of details that are specific to your implementation, but this example will give you some ideas:
var Page = Ractive.extend({
template: function(data, t){
return data.type ? t.fromId(data.type) : 'loading...'
}
})
var r = new Ractive({
el: '#container',
template: '#template',
data: { model: datas.person1 },
components: {
page: Page
},
oninit: function(){
var page = this.findComponent('page')
this.observe('model', function(n){
page.reset(n)
})
},
load: function(load){
this.set('model', datas[load])
}
})
This works if there is shared-logic, or no logic, in the component that is rendering the various models.
Often though, you want to use a different component for each model type because there are observers and event handlers specific to that view for that particular model. In that case, this example up-levels the dynamism to the parent and uses an option function for the component:
var r = new Ractive({
el: '#container',
template: '#template',
data: datas.person1,
components: {
page: function(data){
return components[data.type]
}
},
load: function(load){
this.reset( datas[load] )
}
})

Reactive updates not received in other browsertabs

Meteor promises reactive updates, so that views are auto-updated when data changes. The included leaderboard example demonstrates this. It runs fine when I test it: data is updated across several browsertabs in different browsers, as expected.
All set and go, I started coding with meteor and progress was being made, but when I tested for reactive updates across browertabs, I noticed that only after a short while the updates across tabs stopped.
I boiled down the problem to the following code, based on a new empty meteor project:
updatebug.html
<head>
<title>updatebug</title>
</head>
<body>
{{> form}}
</body>
<template name="form">
<form onsubmit="return false;">
{{#each items}}
{{> form_item }}
{{/each}}
</form>
</template>
<template name="form_item">
<div>
<label>{{name}}
<input type="text" name="{{name}}" value="{{value}}">
</label>
</div>
</template>
updatebug.js:
Items = new Meteor.Collection("items");
if (Meteor.is_client) {
Template.form.items = function () {
return Items.find();
};
Template.form_item.events = {
'blur input': function(e) {
var newValue = $(e.target).val();
console.log('update', this.name, this.value, newValue);
Items.update({_id: this._id}, {$set: {value: newValue}});
},
};
}
if (Meteor.is_server) {
Meteor.startup(function () {
if (Items.find().count() === 0) {
Items.insert({name: 'item1', value: 'something'});
}
});
}
Run in multiple browsertabs, start changing the value of the input in one tab. The other tabs will reflect the change. Goto the next tab and change the value. Repeat a couple of times.
After a while, no more updates are received by any other tabs. It seems that once a tab has changed the value, it does not receive/show any more updates.
Differences compared to the leaderboard example (since it's very similar):
The leaderboard uses no form controls
The leaderboard example does an increment operation on update, not a set
I am about to file a bug report, but want to be sure I am not doing anything stupid here, or missing an essential part of the Meteor Collection mechanics (yes, autopublish package is installed).
The issue here is input element preservation. Meteor will preserve the input state of any form field with an id or name attribute across a template redraw. The redraw is preserving the old text in your form element, because you wouldn't want to interrupt another user typing in the same field. If you remove the name attribute from the text box, each tab will update on blur.
In fact, I'm not sure why the first update works in your example. That may actually be the bug!
You can see it's not a data problem by opening the console in each browser. On each blur event you will get an updated document in every open tab. (Type Items.find().fetch())

Resources