select value binding and svelte-i18n integration problem - internationalization

I'm dealing with something that seems to be a bit beyond my ken here ...
It's about select value binding. Here below, it's a simple peace of code that is working perfectly in a classic svelte SPA.
<script>
let countrySelected = {
code: 'BE',
name: 'Belgium',
};
const countries = [
{
code: 'FR',
name: 'France',
},
{
code: 'BE',
name: 'Belgium',
},
{
code: 'GA',
name: 'Gabon',
},
];
</script>
<select bind:value={countrySelected}>
{#each countries as country}
<option value={country} selected={country.code === countrySelected.code}>
{country.name}
</option>
{/each}
</select>
But for whatever reason, it doesn't work anymore when it's working in a sveltekit app which uses the svelte-i18n npm package.
/src/routes/__layout.svelte
<script>
import { setupI18n, isLocaleLoaded } from '$lib/services/i18n.js';
$: if (!$isLocaleLoaded) {
setupI18n({ withLocale: 'fr-FR' });
}
</script>
{#if !$isLocaleLoaded}
Please wait...
{:else}
<main>
<slot />
</main>
{/if}
/src/lib/services/i18n.js
The code of /src/lib/services/i18n.js comes from:
https://phrase.com/blog/posts/how-to-localize-a-svelte-app-with-svelte-i18n/
&
https://lokalise.com/blog/svelte-i18n/
import { derived } from 'svelte/store';
import { dictionary, locale, _, date, time, number } from 'svelte-i18n';
const MESSAGE_FILE_URL_TEMPLATE = 'http://localhost:3000/lang/{locale}.json';
let cachedLocale;
async function setupI18n({ withLocale: _locale } = { withLocale: 'en-GB' }) {
const messsagesFileUrl = MESSAGE_FILE_URL_TEMPLATE.replace(
'{locale}',
_locale
);
const res = await fetch(messsagesFileUrl);
const messages = await res.json();
dictionary.set({ [_locale]: messages });
cachedLocale = _locale;
locale.set(_locale);
}
// Before any locale is set, svelte-i18n will give locale an object type.
// Once it is correctly set, the libray will set locale
// to the code of the active locale, e.g. "en", a string type.
// We check for this in our devired store, and make sure that isLocaleLoadedā€˜s value
// is true only after i18n initialization is successful.
const isLocaleLoaded = derived(locale, $locale => typeof $locale === 'string');
export { _, locale, setupI18n, isLocaleLoaded, date, time, number };
Problem:
The <select> element in the index.svelte file should show "Belgium" because:
countrySelected = { code: 'BE', name: 'Belgium', }
The problem is that in __layout.svelte, svelte-i18n makes a kind of refresh just after the item has been selected so it looks nothing is selected.
It's probably because of the way I've integrated svelte-i18n in my project as I only understood the broad strokes but once again ... the devil is in the detail :D
Thank you so much for your help. You can clone this repos, it gonna be easier to understand:
git clone https://github.com/BigBoulard/sveltekit-sveltei18n
npm i
npm run dev

With your example, if you inspect the select element in a browser, the selected attribute is not set on any option.
Svelte has a very easy to use and straight forward way to set the initial selected value.
It automatically handles the required attribute on the option element when binding a value to the select element.
The issue in your example is, that countrySelected looks the same as the object/dictionary inside your countries array, but is actually a new object and therefore Svelte can not select it.
This example should work:
<script>
const countries = [
{
code: 'FR',
name: 'France',
},
{
code: 'BE',
name: 'Belgium',
},
{
code: 'GA',
name: 'Gabon',
},
];
let countrySelected = countries.find(x => x.code === 'BE');
</script>
<select bind:value={countrySelected}>
{#each countries as country}
<option value={country}>
{country.name}
</option>
{/each}
</select>

Related

What is the correct way to call a method property inside an object in a store?

I am working on my first Sveltekit app project, converting from a React based project. I got stuck with a bit of a tricky method inside an object inside a store. Before I refactor the entire data structure to something perhaps more intuitive ( this is still a data structure derived from the React way of doing things ) , I wanted to understand how to do this properly in case I need it again.
An object stores some meta data about a dataset.
const dataSetsIndex = [
{id: ':sample:alphabet',
permanent: true,
metadata: {
title: 'Alphabetic Letter Frequency',
source: 'https://observablehq.com/#d3/bar-chart-transitions'
},
attachments: {
data: import ('/src/dataSets/alphabet.json'),
}
}
];
export default dataSetsIndex;
There would be more objects with the { id: permanent: metadata: { title: source: } attachments: { data: ()=> } } structure in this dataSetsIndex component prop.
But when my program eventually tries to access the data from an external JSON ti display on a route in Sveltekit , I can't seem to find a way to make that Promise from the import('/src/dataSets/alphabet.json') method return.
Following the docs, I tried an interface that destructures the data and stores it in a writable - the data in the JSON file is fields:[] , rows:[]
import DataSets from "../dataSets/dataSetsIndex.js";
import {writable} from "svelte/store";
export const dataSetsStore = writable([]);
let destructedDataSets = () => {
const dataSets = DataSets.map( ( dataset, index ) =>
{
return {
id: index,
title: dataset.metadata.title,
source: dataset.metadata.source,
fields: dataset.attachments.data().then(
(success) => { return success.fields},
(fail) => {return fail})
}
}
)
dataSetsStore.set(dataSets);
};
destructedDataSets();
then bringing that in to a route which is reactive
<script>
import {dataSetsStore} from "../stores/dataSetsStore.js"
</script>
{#each $dataSetsStore as metadataObject}
<div>
{metadataObject.title.toUpperCase()}
{metadataObject.fields}
</div>
{/each}
only displays ALPHABETIC LETTER FREQUENCY [object Promise]
What am I missing here?
OK, so I figured it out and this is working. I looked at this related post to help me understand the role of {#await} in the context of this particular structure... my code excerpt below uses Bulma to draw up a table for the results
<script>
import {dataSetsStore} from "../stores/dataSetsStore.js"
</script>
{#each $dataSetsStore as metadataObject}
{#await metadataObject.importDataFrom()}
<p>loading...</p>
{:then theFields}
<table class="table">
<thead>{metadataObject.title.toUpperCase()}</thead>
{#each theFields.fields as f}
<th>
<abbr class="has-background-success is-size-5 p-1" title={f.name}>
{f.name.trim().slice(0,10)}
</abbr>
<span class="has-text-info is-size-8">
{f.type}
</span>
</th>
{/each}
</table>
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
{/each}

Refreshing data after-the-fact in AlpineJS

I'm using Alpine to display a list of items that will change. But I can't figure out how to tell Alpine to refresh the list of items once a new one comes back from the server:
<div x-data=" items() ">
<template x-for=" item in items " :key=" item ">
<div x-text=" item.name "></div>
</template>
</div>
The first "batch" of items is fine, because they're hard-coded in the items() function:
function items(){
return {
items: [
{ name: 'aaron' },
{ name: 'becky' },
{ name: 'claude' },
{ name: 'david' }
]
};
}
Some code outside of Alpine fetches and receives a completely new list of items, that I want to display instead of the original set. I can't figure out how, or if it's even currently possible. Thanks for any pointer.
There are 3 ways to solve this.
Move the fetch into the Alpine.js context so that it can update this.items
function items(){
return {
items: [
{ name: 'aaron' },
{ name: 'becky' },
{ name: 'claude' },
{ name: 'david' }
],
updateItems() {
// something, likely using fetch('/your-data-url').then((res) => )
this.items = newItems;
}
};
}
(Not recommended) From your JavaScript code, access rootElement.__x.$data and set __x.$data.items = someValue
<script>
// some other script on the page
// using querySelector assumes there's only 1 Alpine component
document.querySelector('[x-data]').__x.$data.items = [];
</script>
Trigger an event from your JavaScript and listen to it from your Alpine.js component.
Update to the Alpine.js component, note x-on:items-load.window="items = $event.detail.items":
<div x-data=" items() " x-on:items-load.window="items = $event.detail.items">
<template x-for=" item in items " :key=" item ">
<div x-text=" item.name "></div>
</template>
</div>
Code to trigger a custom event, you'll need to fill in the payload.
<script>
let event = new CustomEvent("items-load", {
detail: {
items: []
}
});
window.dispatchEvent(event);
</script>
Expanding on Hugo's great answer I've implemented a simple patch method that lets you update your app's state from the outside while keeping it reactive:
<div x-data="app()" x-on:patch.window="patch">
<h1 x-text="headline"></h1>
</div>
function app(){
window.model = {
headline: "some initial value",
patch(payloadOrEvent){
if(payloadOrEvent instanceof CustomEvent){
for(const key in payloadOrEvent.detail){
this[key] = payloadOrEvent.detail[key];
}
}else{
window.dispatchEvent(new CustomEvent("patch", {
detail: payloadOrEvent
}));
}
}
};
return window.model;
}
In your other, non-related script you can then call
window.model.patch({headline : 'a new value!'});
or, if you don't want assign alpine's data model to the window, you can simply fire the event, as in Hugo's answer above:
window.dispatchEvent(new CustomEvent("patch", {
detail: {headline : 'headline directly set by event!'}
}));

vue-18n - how to force reload in computed function when changing language

I am using vue-i18n but I also have some content which is stored in database. I would like my text to be updated when the user changes the language.
I am using laravel and vuejs2.
Thanks in advance, I am not super familiar with vuejs yet. I hope it's clear enough.
in ContenuComponent.vue
<template>
<div>
{{$i18n.locale}} <== this changes well
<div v-html="textcontent"></div>
<div v-html="textcontent($i18n.locale)"></div> <== this won't work, I am wondering how to put params here (more like a general quetsion)
</div>
</template>
<script>
export default {
name:'contenu',
props: {
content: {
type: String,
default: '<div></div>'
}
},
computed: {
textcontent: function () {
console.log(navigator.language); <== this gives me the language as well, so i could use it if I can make it reload
var parsed = JSON.parse(this.content);
parsed.forEach(element => {
if(navigator.language == element['lang']){
return element['text'];
}
});
}
}
}
</script>
in ContentController
public function getcontent(){
$content = DB::connection('mysql')->select( DB::connection('mysql')->raw("
SELECT text, lang from content
"));
return view('myvue', ['content' => json_encode($content)]);
}
in content.blade.php
<div id="app">
<contenu content="{{ $content }}"></contenu>
</div>
You SHOULD NOT pass parameters to computed props! They are not methods and you should create method instead:
methods: {
textcontent () {
var parsed = JSON.parse(this.content)
parsed.forEach(element => {
if (navigator.language == element['lang']){
return element['text']
}
})
}
}
Also you should consider ES6 syntax:
methods: {
textcontent () {
var parsed = JSON.parse(this.content)
const content = parsed.find(element => navigator.language == element['lang'])
return content['text']
}
}
Much cleaner!
Please make sure to read about computed props and how they are different than methods or watchers: docs

set watcher on props of vuejs component

I have created datepicker vuejs component.
I have set two date picker in page to select start date and end date. So startDate and endDate of one datepicker is depend on value of another. So for that, I need to set watcher on selected value.
It was working fine with VueJS (< 2.3.0) using this.$emit('input', value). But in VueJS (2.4.4), it is not working.
I'm very poor in front-end development. So I got nothing even after spending two days on vuejs. I just come to know that it is just because of vue version > 2.2.6.
If anyone know the solution, it will be realy very much helpful for me.
Here is my code.
DatePicker.vue
<template>
<div class="input-append date form_datetime">
<input size="16" type="text" readonly>
<span class="add-on"><i class="icon-th fa fa-calendar"></i></span>
</div>
</template>
<script>
export default {
twoWay: true,
props: ['slctd', 'startDate', 'endDate'],
mounted: function () {
var self = this;
$(this.$el).datetimepicker({
format: "mm/dd/yyyy",
autoclose: true,
minView: 2,
maxView: 3,
daysShort: true,
initialDate: this.slctd,
startDate: this.startDate,
endDate: this.endDate,
}).on('changeDate', function (ev) {
var value = new Date(ev.date.valueOf()).toString('MM/dd/yyyy');
self.$emit('input', value);
});
},
watch: {
slctd: function (value) {
console.log('watch:value ' + value);
$(this.$el).find("input").val(value);
},
startDate: function (value) {
console.log('watch:start ' + value);
$(this.$el).datetimepicker('setStartDate', value);
},
endDate: function (value) {
console.log('watch:end ' + value);
$(this.$el).datetimepicker('setEndDate', value);
}
},
}
</script>
JS
window.Vue = require('vue');
Vue.component('component-date-picker', require('./components/DatePicker.vue'));
window.app = new Vue({
el: '#app',
data: {
queries: {
start_date: '10/04/2017',
end_date: '10/05/2017',
}
},
});
HTML
<component-date-picker v-bind:end-date="queries.end_date" v-bind:slctd="queries.start_date" v-model="queries.start_date"></component-date-picker>
<component-date-picker v-bind:start-date="queries.start_date" v-bind:slctd="queries.end_date" v-model="queries.end_date"></component-date-picker>
VueJS - 2.2.6 (working)
VueJS - 2.4.4 (not working)
I have also tried this too. But didn't get success
HTML
<component-date-picker v-bind:end-date="queries.end_date" v-bind:slctd="queries.start_date" v-model="queries.start_date" v-on:input="set_start_date"></component-date-picker>
<component-date-picker v-bind:start-date="queries.start_date" v-bind:slctd="queries.end_date" v-model="queries.end_date" v-on:input="set_end_date"></component-date-picker>
JS
methods: {
set_start_date: function(value){
console.log('called');
this.queries.start_date = value;
},
set_end_date: function(value){
this.queries.end_date = value;
},
}
In this code, I method was called and I was getting 'called' in console. But watcher of component not working. It should work as startDate/endDate is changed in these methods. I don't know what is the new working concept of VueJS 2.4.4.
Solved
It was my stupid mistake due to lack of knowledge in front-end. I was re-creating queries object in create method of vue root instance. And in previous code, there was nothing in create so it was working fine.
Here is my mistake
JS
created: function () {
/* my mistake starts */
this.queries = queryString.parse(location.search, {arrayFormat: 'bracket'});
/* my mistake ends */
this.queries.start_date = (this.queries.start_date) ? this.queries.start_date : null;
this.queries.end_date = (this.queries.end_date) ? this.queries.end_date : null;
},

Vue.js Retrieving Remote Data for Options in Select2

I'm working on a project that is using Vue.js and Vue Router as the frontend javascript framework that will need to use a select box of users many places throughout the app. I would like to use select2 for the select box. To try to make my code the cleanest I can, I've implemented a custom filter to format the data the way select2 will accept it, and then I've implemented a custom directive similar to the one found on the Vue.js website.
When the app starts up, it queries the api for the list of users and then stores the list for later use. I can then reference the users list throughout the rest of the application and from any route without querying the backend again. I can successfully retrieve the list of users, pass it through the user list filter to format it the way that select2 wants, and then create a select2 with the list of users set as the options.
But this works only if the route that has the select2 is not the first page to load with the app. For example, if I got to the Home page (without any select2 list of users) and then go to the Users page (with a select2), it works great. But if I go directly to the Users page, the select2 will not have any options. I imagine this is because as Vue is loading up, it sends a GET request back to the server for the list of users and before it gets a response back, it will continues with its async execution and creates the select2 without any options, but then once the list of users comes back from the server, Vue doesn't know how to update the select2 with the list of options.
Here is my question: How can I retrieve the options from an AJAX call (which should be made only once for the entire app, no matter how many times a user select box is shown) and then load them into the select2 even if the one goes directly to the page with the select2 on it?
Thank you in advance! If you notice anything else I should be doing, please tell me as I would like this code to use best practices.
Here is what I have so far:
Simplified app.js
var App = Vue.extend({
ready: function() {
this.fetchUsers();
},
data: function() {
return {
globals: {
users: {
data: []
},
}
};
},
methods: {
fetchUsers: function() {
this.$http.get('./api/v1/users/list', function(data, status, response) {
this.globals.users = data;
});
},
}
});
Sample response from API
{
"data": [
{
"id": 1,
"first_name": "John",
"last_name": "Smith",
"active": 1
},
{
"id": 2,
"first_name": "Emily",
"last_name": "Johnson",
"active": 1
}
]
}
User List Filter
Vue.filter('userList', function (users) {
if (users.length == 0) {
return [];
}
var userList = [
{
text : "Active Users",
children : [
// { id : 0, text : "Item One" }, // example
]
},
{
text : "Inactive Users",
children : []
}
];
$.each( users, function( key, user ) {
var option = { id : user.id, text : user.first_name + ' ' + user.last_name };
if (user.active == 1) {
userList[0].children.push(option);
}
else {
userList[1].children.push(option);
}
});
return userList;
});
Custom Select2 Directive (Similar to this)
Vue.directive('select', {
twoWay: true,
bind: function () {
},
update: function (value) {
var optionsData
// retrive the value of the options attribute
var optionsExpression = this.el.getAttribute('options')
if (optionsExpression) {
// if the value is present, evaluate the dynamic data
// using vm.$eval here so that it supports filters too
optionsData = this.vm.$eval(optionsExpression)
}
var self = this
var select2 = $(this.el)
.select2({
data: optionsData
})
.on('change', function () {
// sync the data to the vm on change.
// `self` is the directive instance
// `this` points to the <select> element
self.set(select2.val());
console.log('emitting "select2-change"');
self.vm.$emit('select2-change');
})
// sync vm data change to select2
$(this.el).val(value).trigger('change')
},
unbind: function () {
// don't forget to teardown listeners and stuff.
$(this.el).off().select2('destroy')
}
})
Sample Implementation of Select2 From Template
<select
multiple="multiple"
style="width: 100%"
v-select="criteria.user_ids"
options="globals.users.data | userList"
>
</select>
I may have found something that works alright, although I'm not sure it's the best way to go about it. Here is my updated code:
Implementation of Select2 From Template
<select
multiple="multiple"
style="width: 100%"
v-select="criteria.reporting_type_ids"
options="globals.types.data | typeList 'reporttoauthorities'"
class="select2-users"
>
</select>
Excerpt from app.js
fetchUsers: function() {
this.$http.get('./api/v1/users/list', function(data, status, response) {
this.globals.users = data;
this.$nextTick(function () {
var optionsData = this.$eval('globals.users.data | userList');
console.log('optionsData', optionsData);
$('.select2-users').select2({
data: optionsData
});
});
});
},
This way works for me, but it still kinda feels hackish. If anybody has any other advice on how to do this, I would greatly appreciate it!
Thanks but I'm working on company legacy project, due to low version of select2, I encountered this issue. And I am not sure about the v-select syntax is from vue standard or not(maybe from the vue-select libaray?). So here's my implementation based on yours. Using input tag instead of select tag, and v-model for v-select. It works like a charm, thanks again #bakerstreetsystems
<input type="text"
multiple="multiple"
style="width: 300px"
v-model="supplier_id"
options="suppliers"
id="select2-suppliers"
>
</input>
<script>
$('#app').ready(function() {
var app = new Vue({
el: "#app",
data: {
supplier_id: '<%= #supplier_id %>', // We are using server rendering(ruby on rails)
suppliers: [],
},
ready: function() {
this.fetchSuppliers();
},
methods: {
fetchSuppliers: function() {
var self = this;
$.ajax({
url: '/admin_sales/suppliers',
method: 'GET',
success: function(res) {
self.suppliers = res.data;
self.$nextTick(function () {
var optionsData = self.suppliers;
$('#select2-suppliers').select2({
placeholder: "Select a supplier",
allowClear: true,
data: optionsData,
});
});
}
});
},
},
});
})
</script>

Resources