I am attempting to build a polymer countdown widget.
In Javascript I would write a similar function to this and keep inserting the updated countdown time into the required ID element.
setInterval(function () {
//Work out time till event horizon
//Shove the new time into the associated element
countdown.innerHTML = days + "d, " + hours + "h, "+ minutes + "m, " + seconds + "s";
}, 1000);
In Polymer I am uncertain how to go about this as I am unsure how to go about using setInterval in Polymer... Here is some sudo code:
<polymer-element name="countdown-element">
<template>
<div id="countDown">{{counter}}</div>
</template>
<script>
Polymer('countdown-element', {
counter: function() {
//Do stuff
}
});
</script>
</polymer-element>
How do I go about repeatedly sending the count down time to the counter ID location?
I think what you want is a combination of the two-way data binding that comes with Polymer and the async method described in the "advanced topics" here: http://www.polymer-project.org/docs/polymer/polymer.html#additional-utilities
Here is a JSBin with a simple countdown.
Here's a simple countdown element in Polymer 3:
<html>
<body>
<script type="module">
import { PolymerElement, html } from 'https://unpkg.com/#polymer/polymer/polymer-element.js?module'
class CountdownElement extends PolymerElement {
static get properties () { return {counter: Number} }
ready () {
super.ready()
this.counter = 1000;
this.updateCounter()
}
updateCounter () {
this.counter -= 1;
setTimeout(() => this.updateCounter(), 1000)
}
static get template () { return html`<div>Countdown: [[counter]]</div>` }
}
customElements.define('countdown-element', CountdownElement)
</script>
<countdown-element></countdown-element>
</body>
</html>
This example works without polyfills in Chrome and Firefox, you can try this in CodePen.
(This is also useful for building an ordinary clock by using clock instead of counter and setting it in updateClock() with e.g. this.clock = new Date().toLocaleTimeString() – but you need a countdown clock instead and seem to have existing logic for calculating the remaining time until the event already.)
Related
I'm learning Svelte after having used Vue for a while, but am a bit confused by some strange reactivity issues with Svelte.
I have this simple code:
Svelte Snippet
<script>
let count = 0
$: tripleCount = count * 3
const increaseCount = () => count++
$: if (tripleCount > 6) count = 0
</script>
<button on:click={increaseCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>
Triple count is: {tripleCount}
</p>
But, when I try to run it on Svelte's playground, I get an error message alerting me that there is a Cyclical dependency detected: tripleCount → count → tripleCount.
I've found a few ways to fix that issue and get the Svelte component to work as intended. But, I'm curious about why I get that issue, given that there isn't any logical loop that'd be impossible to close. The equivalent code in Vue works perfectly fine.
Vue Equivalent Snippet
<script setup>
import { computed, ref, watchEffect } from 'vue'
const count = ref(0)
const tripleCount = computed(() => count.value * 3)
const incrementCount = () => count.value++
watchEffect(() => {
if (tripleCount.value > 6) count.value = 0
})
</script>
<template>
<button #click="incrementCount">
Clicked {{ count }} {{ count === 1 ? 'time' : 'times' }}
</button>
<p>Triple count is: {{ tripleCount }}</p>
</template>
Live Demo of Expected Behavior
You can ignore the code in the snippet below since it's not too relevant to my question and is already described more simply above.
Just run the snippet below to see what I'm trying to get my Svelte component to do.
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="app"></div>
<script>
const { createApp, ref, computed, watchEffect } = Vue
createApp({
setup() {
const count = ref(0)
const tripleCount = computed(() => count.value * 3)
const incrementCount = () => count.value++
watchEffect(() => {
if (tripleCount.value > 6) count.value = 0
})
return {
count,
tripleCount,
incrementCount,
}
},
template: `
<button #click="incrementCount">
Clicked {{ count }} {{ count === 1 ? 'time' : 'times' }}
</button>
<p>Triple count is: {{ tripleCount }}</p>
`,
}).mount('#app')
</script>
Possible Solutions and New Issues
Approach A: Indirect Update
An interesting workaround to avoid getting the Cyclical dependency error is to change count indirectly, through an intermediary resetCount function.
<script>
// $: if (tripleCount > 6) count = 0
const resetCount = () => count = 0
$: if ($tripleCount > 6) resetCount()
</script>
Logically, this implementation is no different from the original, but somehow the Cyclical dependency error goes away.
However, some new unexpected behavior arises. When tripleCount is greater than 6 and count is therefore reset to 0, tripleCount does not update. It retains its last value (9 in this case), and its reactivity doesn't reactivate until the next click of the button.
Why does tripleCount not react to the change of count when count is reset?
Approach B: Indirect Update + Delay
If I add a delay before the reset helper sets count to 0, the code will work as its Vue equivalent, with the correct behavior.
<script>
const resetCount = () => {
setTimeout(() => count = 0, 0) // zero ms delay
}
$: if ($tripleCount > 6) resetCount()
</script>
Why does this delay help tripleCount react to the change in count? I guess using setTimeout is helping Svelte exit that event loop and only then handle the reactivity of the change. But, it seems quite error-prone to me that I have to be mindful of not forgetting to add these delays for a supposedly computed value to be able to pick up on all the changes of the value it's dependent on.
Is there a better way of making sure tripleCount reacts to the resetting of count?
As can be seen in the Vue implementation, I didn't need to use any indirect resetCount auxiliary function nor setTimeout hack to get the expected behavior.
Approach C: Stores
After further experimentation, I managed to get my Svelte component to work as intended by using stores like this:
<script>
import { writable, derived } from 'svelte/store';
const count = writable(0);
const tripleCount = derived(count, $count => $count * 3);
function incrementCount() {
count.update(n => n + 1);
}
$: if ($tripleCount > 6) {
count.set(0);
}
</script>
<button on:click={incrementCount}>
Clicked {$count} {$count === 1 ? 'time' : 'times'}
</button>
<p>Triple count is: {$tripleCount}</p>
Would this be the recommended way to work with reactivity in Svelte? I am a bit sad because it takes away much of the simplicity that made me want to learn Svelte. I know all frameworks rely on stores for state management at some point, but it seems overkill that I need to use them to implement logic as basic as the one I was trying to implement.
I'd be extremely grateful for any guidance or feedback on the issues I presented here. Thank you so much for reading and for any help <3! Hoping I can understand how reactivity in Svelte works better ^^
PS: Summary of Questions
Why does the intuitive way ($: if (tripleCount > 6) count = 0) not work?
Why does the indirect reset trick ($: if (tripleCount > 6) resetCount() avoid the Cyclical dependency error?
Why does the immediate delay trick (setTimeout(() => count = 0, 0)) ensure tripleCount does update after count is reset?
Is there a way to get the expected behavior in Svelte without an auxiliary resetCount function, nor a setTimeout hack, nor by resorting to using Svelte Stores yet?
If at all possible write it like this 4️⃣:
<script>
let count = 0
$: tripleCount = count * 3
const increaseCount = () => {
count++;
if (count * 3 > 6) count = 0;
}
</script>
<button on:click={increaseCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>
Triple count is: {tripleCount}
</p>
The preferred and most performant way is to make all state changes synchronously and then let Svelte update the view in one go (not relying on derived data and calculating the tripleCount inline).
You don't have use an event and can react to a change in data (like in your workarounds) but using an event prevents the chance of creating an infinite loop.
If want to use a value based on a calculation that happens after a reactivity update, you can use the tick()
( Use sparingly because that might cause a trickle effect, where you'll need more and more ticks )
import { tick } from "svelte";
const increaseCount = async () => {
count++;
await tick();
if (tripleCount > 6) count = 0
}
or when reacting to data:
const resetCount = () => tick().then(() => count = 0)
How svelte works 1️⃣
Every assignment in a reactive block is augmented by the compiler with an invalide() call.
This invalidate marks the variable as dirty and schedules an update unless there is already an update planned.
The value of the variables are updated instantly, they are regular javascript variables.
After your code ran, then the update starts.
explained in pseudo code
$: tripleCount = count * 3
<div>{count === 1 ? 'time' : 'times'}</div>
becomes
function updateData(changes) {
if (hasCountChanged(changes)) {
tripleCount = count * 3
}
}
function updateView(changes) {
if (hasTripleCountChanged(changes)) {
updateTheText(count === 1 ? 'time' : 'times')
}
}
This updateData code only runs once per component.
Svelte looks at all the reactive blocks and sorts then based on their dependencies.
It a A changes B and B changes A you've got a cycle and Svelte can't sort them in the correct order. Using the resetCount() the compiler was unable to detect this cycle 2️⃣ and generated:
function resetCount() {
count = 0
invalidate('count')
}
function updateData(changes) {
if (hasCountChanged(changes)) {
tripleCount = count * 3
}
if (hasTripleCountChanged(changes)) {
if (tripleCount > 6) {
resetCount()
}
}
}
Because the ordering is incorrect the tripleCount and count are out of sync.
When using a setTimeout the invalidate runs after the "updateData" completed and the invalidate('count') wil trigger a new update cycle and that one will update the tripleCount 3️⃣
( PS. I'd like to see a reactivity system that runs more than once, I've written an Svelte RFC about it, but in day-to-day writing Svelte components, I rarely encounter this issue )
As Bob clearly pointed out in his answer, a single variable cannot be updated twice in a single update cycle (a tick in Svelte lingo).
That double update requirement becomes obvious when you try to pack your update code into the smallest possible function:
function processUpdate(_count) {
tripleCount = count * 3
if (tripleCount > 6) {
count = 0
tripleCount = 0 // this is the second time tripleCount is updated
}
}
In truth, the only way around your issue is to eliminate the cyclic dependency (as pointed out by Svelte's error message, incidentally) altogether.
This means reducing the reactive statements that cause this cyclical dependency to a single reactive statement based on one of the variables.
We can do this by reusing the minimal update function stated above:
<script>
let count = 0
let tripleCount = 0
$: processUpdate(count)
const processUpdate = (_count) => {
tripleCount = count * 3
if (tripleCount > 6) {
count = 0
tripleCount = 0
}
}
const increaseCount = () => {
count++;
}
</script>
<button on:click={increaseCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>
Triple count is: {tripleCount}
</p>
And this works as you would expect: REPL
I'm trying to throttle the execution of a function in Svelte. However, using throttle within an auto-subscription seems to break it. Consider the following example (REPL):
<script>
import throttle from 'lodash.throttle';
import { writable } from 'svelte/store';
const arr = writable(['A', 'B', 'C']);
function foo(val) {
console.log('foo: ', val);
}
</script>
{#each $arr as a}
<button on:click={throttle(() => { foo(a); }, 1000)}>
Button {a}
</button>
{/each}
The execution of foo(a) is not throttled at all. If you remove the {#each $arr as a} block and just pass a string to foo, you'll see that it works as intended.
I'm assuming this has to do with the event loop and how Svelte auto-subscriptions work but don't know the exact reason. Wondering if anyone knows a) why this is happening and b) what a possible solution could look like?
If you look at the code Svelte generates for this, you can see that it is regenerating the throttled function on every click when you pass a store value. This resets the throttle timer on every click.
dispose = listen(button, "click", function () {
if (is_function(throttle(click_handler, 1000)))
throttle(click_handler, 1000).apply(this, arguments);
});
For whatever reason, this does not happen when you pass a regular string.
dispose = listen(button, "click", throttle(click_handler, 1000));
This may be a bug in Svelte, but I'm not sure. It might be worth opening an issue on the GitHub repo.
I was able to work around it by generating the throttled functions ahead of time:
<script>
import throttle from 'lodash.throttle';
import { writable } from 'svelte/store';
const arr = writable(['A', 'B', 'C']);
function foo(val) {
console.log('foo: ', val);
}
$: throttledFns = $arr.map(val => getThrottled(val));
function getThrottled(val) {
console.log('getting throttled');
return throttle(() => { foo(val); }, 1000);
}
</script>
{#each $arr as a, idx}
<button on:click={throttledFns[idx]}>
Button {a}
</button>
{/each}
This will regenerate the throttled functions when the store array changes, but not on every click.
You can also generate a throttled version of foo once and use that, but that will throttle all clicks to the buttons (e.g. if you click A and then click B, the click to B will be throttled).
<script>
// as before
const throttledFoo = throttle(foo, 1000);
</script>
{#each $arr as a, idx}
<button on:click={() => throttledFoo(a)}, 1000)}>
Button {a}
</button>
{/each}
I'm trying to use Twitter's typeahead.js in a Vue component, but although I have it set up correctly as tested out outside any Vue component, when used within a component, no suggestions appear, and no errors are written to the console. It is simply as if it is not there. This is my typeahead setup code:
var codes = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('code'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
prefetch: contextPath + "/product/codes"
});
$('.typeahead').typeahead({
hint: true,
highlight: true,
minLength: 3
},
{
name: 'codes',
display: 'code',
source: codes,
templates: {
suggestion: (data)=> {
return '<div><strong>' + data.code + '</strong> - ' + data.name + '</div>';
}
}
});
I use it with this form input:
<form>
<input id="item" ref="ttinput" autocomplete="off" placeholder="Enter code" name="item" type="text" class="typeahead"/>
</form>
As mentioned, if I move this to a div outside Vue.js control, and put the Javascript in a document ready block, it works just fine, a properly formatted set of suggestions appears as soon as 3 characters are input in the field. If, however, I put the Javascript in the mounted() for the component (or alternatively in a watch, I've tried both), no typeahead functionality kicks in (i.e., nothing happens after typing in 3 characters), although the Bloodhound prefetch call is made. For the life of me I can't see what the difference is.
Any suggestions as to where to look would be appreciated.
LATER: I've managed to get it to appear by putting the typeahead initialization code in the updated event (instead of mounted or watch). It must have been some problem with the DOM not being in the right state. I have some formatting issues but at least I can move on now.
The correct place to initialize Twitter Typeahead/Bloodhound is in the mounted() hook since thats when the DOM is completely built. (Ref)
Find below the relevant snippet: (Source: https://digitalfortress.tech/js/using-twitter-typeahead-with-vuejs/)
mounted() {
// configure datasource for the suggestions (i.e. Bloodhound)
this.suggestions = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('title'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify: item => item.id,
remote: {
url: http://example.com/search + '/%QUERY',
wildcard: '%QUERY'
}
});
// get the input element and init typeahead on it
let inputEl = $('.globalSearchInput input');
inputEl.typeahead(
{
minLength: 1,
highlight: true,
},
{
name: 'suggestions',
source: this.suggestions,
limit: 5,
display: item => item.title,
templates: {
suggestion: data => `${data.title}`;
}
}
);
}
You can also find a working example: https://gospelmusic.io/
and a Reference Tutorial to integrate twitter typeahead with your VueJS app.
I use moment.js to display how many time has passed from some event.
I have a list (rendered using vue.js)
event 3, 5 seconds ago
event 2, 1 minute ago
event 1, 5 minutes ago
The problem is: list is not updated frequently (new items are added, for example, every 2 minutes).
I want to update n (seconds|minutes) ago strings.
Should I do simple loop using setInterval?
for(let i = 0; i < this.list.length; i++) {
let item = this.list[i];
item.created_at_diff = moment(item.created_at).fromNow();
this.$set(this.list, i, item);
}
or there is a better approach?
Here is how I would do such a thing, tell me if I am wrong:
First, I would create a component that will make the timer:
let Timer = Vue.extend({
template: '#timer',
props: ['timestamp'],
data () {
return {
formatted: ''
}
},
methods: {
format () {
let self = this
this.formatted = moment().to(moment(this.timestamp,'X'))
// Uncomment this line to see that reactivity works
// this.formatted = moment().format('X')
setTimeout(() => {
self.format()
},500)
}
},
created () {
this.format()
}
})
The timer takes one property, a UNIX timestamp in seconds. The component contains one method called format() that will update the only data formatted of the component. The method is recursive and calls itself every 500ms (with the setTimeout()), you can make this number bigger if you want.
Here is a jsFiddle I made if you want to test it:
https://jsfiddle.net/54qhk08h/
The Google Custom Search integration only includes numbered page links and I cannot find a way to include Next/Previous links like on a normal Google search. CSE used to include these links with their previous iframe integration method.
I stepped through the javascript and found the undocumented properties I was looking for.
<div id="cse" style="width: 100%;">Loading</div>
<script src="http://www.google.com/jsapi" type="text/javascript"></script>
<script type="text/javascript">
google.load('search', '1', {language : 'en'});
google.setOnLoadCallback(function() {
var customSearchControl = new google.search.CustomSearchControl('GOOGLEIDGOESHERE');
customSearchControl.setResultSetSize(google.search.Search.FILTERED_CSE_RESULTSET);
customSearchControl.setSearchCompleteCallback(null,
function() { searchCompleteCallback(customSearchControl) });
customSearchControl.draw('cse');
}, true);
function searchCompleteCallback(customSearchControl) {
var currentPageIndex = customSearchControl.e[0].g.cursor.currentPageIndex;
if (currentPageIndex < customSearchControl.e[0].g.cursor.pages.length - 1) {
$('#cse .gsc-cursor').append('<div class="gsc-cursor-page">Next</div>').click(function() {
customSearchControl.e[0].g.gotoPage(currentPageIndex + 1);
});
}
if (currentPageIndex > 0) {
$($('#cse .gsc-cursor').prepend('<div class="gsc-cursor-page">Previous</div>').children()[0]).click(function() {
customSearchControl.e[0].g.gotoPage(currentPageIndex - 1);
});
}
window.scrollTo(0, 0);
}
</script>
<link rel="stylesheet" href="http://www.google.com/cse/style/look/default.css" type="text/css" />
I've been using this to find the current page:
ctrl.setSearchCompleteCallback(null, function(gControl, gResults)
{
currentpage = 1+gResults.cursor.currentPageIndex;
// or, here is an alternate way
currentpage = $('.gsc-cursor-current-page').text();
});
And now it's customSearchControl.k[0].g.cursor ... (as of this weekend, it seems)
Next time it stops working just go to script debugging in IE, add customSearchControl as a watch, open the properties (+), under the Type column look for Object, (Array) and make sure there is a (+) there as well (i.e. contains elements), open[0], and look for Type Object, again with child elements. Open that and once you see "cursor" in the list, you've got it.