I'm new to programming in Svelte. I would like to be able to use a method on an ES6 class instance in order to dynamically change values being used on my SPA. (Using svelte-spa-router is not an option, unfortunately.)
To get to the crux of the problem in a simplified from:
This is router.js:
import { writable } from 'svelte/store';
export class Router {
constructor(pageMap) {
this.pageMap = pageMap;
this.current = {};
this.currentName = '';
}
get name() {
return this.currentName;
}
set name(pageName) {
this.current = this.pageMap[pageName];
this.currentName = this.current.name;
}
navigate(target) {
this.name = target;
console.log(this.currentName);
}
}
and this is App.js:
<script>
import { Router } from './router';
const pageMap = {
start: {
title: 'start',
name: 'world!',
},
end: {
title: 'end',
name: '-- it works!!!',
},
}
const page = new Router(pageMap);
page.name = 'start';
</script>
<h1>Hello {page.currentName}</h1>
<button on:click={() => page.navigate('end')}>
change to "it works"
</button>
<button on:click={() => page.navigate('start')}>
change back to "world!"
</button>
The desired behavior is that the page.currentName value changes with the button presses. The output to the console on button presses is correct: "--it works!!!", or "world!". However, the text remains "Hello world!", so the value change is not traveling outside the class instance. If I had some way of saying "this = this" upon invoking the navigate method, that would probably solve the problem...
I suspect the correct answer involves writable stores, but I haven't quite been able to figure it out.
I suspect the correct answer involves writable stores
That is correct and trying to use classes like this is not helpful, at least with how Svelte operates right now.
Stores have to be declared at the top level of a component to be usable with $ syntax, putting them inside properties of classes and hiding them behind getters or setters just gets in the way.
I would just use a function that returns an object containing the stores and API you actually need, which then can be destructured right away and used in the markup. E.g.
import { writable, derived } from 'svelte/store';
export function router(pageMap) {
const current = writable({});
const currentName = derived(current, $current => $current.name ?? '');
function navigate(target) {
current.set(pageMap[target]);
}
return {
navigate,
currentName,
};
}
<script>
import { router } from './router';
const pageMap = {
start: {
title: 'start',
name: 'world!',
},
end: {
title: 'end',
name: '-- it works!!!',
},
}
const { navigate, currentName } = router(pageMap);
navigate('start');
</script>
<h1>Hello {$currentName}</h1>
<button on:click={() => navigate('end')}>
change to "it works"
</button>
<button on:click={() => navigate('start')}>
change back to "world!"
</button>
REPL example
You can do something similar with a class, but if you destructure it, the this binding will be broken, so all functions have to be bound manually or you have to pull out the store on its own and keep accessing the functions via the instance.
REPL example
Related
What ways to change language in React can you suggest without using external libraries? My way is to use the ternary operator {language === 'en'? 'title': 'titre'}. If language is en, displaytitle if not, display titre. What other way can you recommend. For example, that the translations should be placed in a separate json file.
Code here: https://stackblitz.com/edit/react-eu9myn
class App extends Component {
constructor() {
super();
this.state = {
language: 'en'
};
}
changeLanguage = () => {
this.setState({
language: 'fr'
})
}
render() {
const {language} = this.state;
return (
<div>
<p>
{language === 'en' ? 'title' : 'titre'}
</p>
<button onClick={this.changeLanguage}>change language</button>
</div>
);
}
}
Internationalization (i18n) is a hard problem with a few existing, standard solutions designed by expert translators and linguists to account for the breadth of language quirks across the world. You shouldn't generally try to come up with your own solution, even when you are fluent in all target languages.
That doesn't mean you need a library (you could implement one of those standards yourself) but writing the i18n logic inline will not scale and probably won't work well.
The easiest case of i18n is if you're translating strings that do not depend on context and are complete sentences with no interpolation. You could get away with a very basic approach there, like using a big dictionary of translations and just looking up each string in it. It would look sort of like your ternary but at least it would scale for many languages, and it would be reasonable to do that with no library:
l10n = {
'title': {en: 'title', fr: 'titre'}
}
<p>
{l10n['title'][lang]}
</p>
However, if there is going to be string interpolation in your website/application/whatever, please consider a library that implements, say, ICU.
Now, let me show you why it would be a bad idea. Suppose you have the string "you can see (n) rocks" where you want to replace (n) with an actual number, and you want the sentence to be grammatically correct so you need to compute number agreement, right ? so, "0 rocks", "1 rock", "2+ rocks"… looks like English plural is just adding an "s" (not true, but let's assume for now), you could implement that with ternaries. I see you used French in your example so, how about that ? "0 cailloux", "1 caillou", "2+ cailloux". Right, there are multiple plural forms in French. How do you write your code to account for that ? And what if you need a German translation ? maybe the translator will decide that the object should go first in the sentence, rather than last. How does your code handle word order based on language ?
All these problems should be delegated to the translator who encodes them into an ICU string, which is then evaluated by some code given a context to get a correct translation. Whether you use a library or implement it yourself, what you want in the end is some function — let's call it localize(string, context) that is pretty much independent from React and that you use in your components like this:
import localize from './somewhere'
<p>
{localize('title')}
</p>
If you really want to, you can pass the locale as an argument and have it stored in React's state somehow. This library decided it wasn't necessary because real users rarely switch language and it's OK to reload the whole application when that happens.
I just implemented a simple language component for work that uses a Localisation context/provider and a dictionary (e.g JSON). I'll go through the steps, and there's a workable codesandbox example at the end. This is a very basic approach, but it works well for us at the moment.
The example has:
1) A simple "dictionary" that contains the tokens you want to translate in each language defined by a short code
{ EN: { welcome: 'Welcome' }, FR: { welcome: 'Bienvenue' }, IT: { welcome: 'Benvenuto' } };
2) An initial state and reducer that you can update when the language changes
export const initialState = {
defaultLanguage: 'EN',
selectedLanguage: 'IT'
}
export function reducer(state, action) {
const { type, payload } = action;
switch (type) {
case 'LANGUAGE_UPDATE': {
return { ...state, selectedLanguage: payload };
}
default: return state;
}
}
3) A Localisation Context/Provider. You can wrap your code in the provider and every child component can get access to the state through the context. We import the dictionary and state/reducer, create the new context and then set up the provider into which we pass the state and dictionary.
import dictionary from './dictionary';
import { initialState, reducer } from './localisationReducer';
export const LocalisationContext = React.createContext();
export function LocalisationProvider({ children }) {
const localisationStore = useReducer(reducer, initialState);
return (
<LocalisationContext.Provider value={{ localisationStore, dictionary }}>
{children}
</LocalisationContext.Provider>
);
}
4) An example app. You can see the LocalisationProvider wrapping the other elements, but also a dropdown, and a component called Translate. I'll describe those next.
<LocalisationProvider>
<Dropdown />
<div className="App">
<h1>
<Translate token="welcome" />
</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
</LocalisationProvider>
5) The dropdown accesses the Localisation context and builds a dropdown with the languages. The key part is the handleSelected function which uses the dispatch from the localisation store to change the state (update the language):
import { LocalisationContext } from './localisation';
const langs = [
{ shortCode: 'EN', label: 'English' },
{ shortCode: 'FR', label: 'Français' },
{ shortCode: 'IT', label: 'Italiano' }
];
export function Dropdown() {
const {
localisationStore: [ state, dispatch ]
} = useContext(LocalisationContext);
const { selectedLanguage } = state;
const handleSelected = (e) => {
const { target: { value } } = e;
dispatch({ type: 'LANGUAGE_UPDATE', payload: value });
}
function getOptions(langs, selectedLanguage) {
return langs.map(({ shortCode, label }) => {
return <option value={shortCode}>{label}</option>
});
}
return (
<select onChange={handleSelected}>
{getOptions(langs, selectedLanguage)}
</select>
);
}
6) The Translate component which also accesses the state and dictionary through the context, and performs the translation based on the selected language.
import { LocalisationContext } from './localisation';
export default function Translator({ token }) {
const {
localisationStore: [state], dictionary
} = useContext(LocalisationContext);
const {
selectedLanguage, defaultLanguage
} = state;
const translatedToken = dictionary[selectedLanguage][token] || dictionary[defaultLanguage][token];
return (
<Fragment>
{translatedToken}
</Fragment>
);
}
Here's the codesandbox example for you to explore. Just select a new language from the dropdown to see the main "welcome" text change.
Ok so I have the following prop that I get from the parent component
props: {
selectedExchange: {
default: 'acx',
}
},
And i try to use it in the following method
methods: {
getMarkets() {
const ccxt = require('ccxt')
const exchanges = ccxt.exchanges;
let marketPair = new ccxt[this.selectedExchange]()
let markets = marketPair.load_markets()
return markets
}
},
The expected result should be an array of markets for my project but i get an error in the console
[Vue warn]: Error in mounted hook: "TypeError: ccxt[this.selectedExchange] is not a constructor"
Now i thought it might be a problem with ccxt but it's not! I have tried the following code
methods: {
getMarkets() {
const ccxt = require('ccxt')
const exchanges = ccxt.exchanges;
let acx = 'acx'
let marketPair = new ccxt[acx]()
let markets = marketPair.load_markets()
return markets
}
},
If you don't see the change I have made a variable that contains 'acx' inside, exactly the same like the prop but this time it's created inside the method, and with this code I get the expected result, It has been bugging me for days and I can't seem to find an answer to it, did I initialize the default value wrong? When i look inside vue dev tools the value for my prop is array[0], only after I pass a value to that prop it gets updated, shouldn't i see the default value of acx in devtools? Any help is much appreciated!
Edit 1: Added parent component code
This is how i use the methods inside the parent and how my components are related to each other,
<div id="exchange">
<exchange v-on:returnExchange="updateExchange($event)"></exchange>
</div>
<div id="pair">
<pair :selectedExchange="this.selectedExchange"></pair>
</div>
And this is the code inside the script tags, i didn't include the import tag cause i don't think it would be useful
export default {
name: 'App',
components: { exchange, pair, trades },
data(){
return{
selectedExchange: ''
}
},
methods: {
updateExchange(updatedExchange){
this.selectedExchange = updatedExchange
}
},
};
In this case you will inherit the default value:
<pair></pair>
In this case you will always inherit the value of selectedExchange, even if it's null or undefined:
<pair :selectedExchange="this.selectedExchange"></pair>
So, in your case, you have to handle the default value on parent component.
This should work:
export default {
name: 'App',
components: { exchange, pair, trades },
data(){
return{
selectedExchange: 'acx' // default value
}
},
methods: {
updateExchange(updatedExchange){
this.selectedExchange = updatedExchange
}
},
};
I have just started looking into switching to Serenity/JS and wanted to know if it's best practice to have base Questions/Tasks?
There are many times I will want to check if a field is blank or has an error, so I created a 'base Question' to achieve this:
Base Question
import { Is, See, Target, Task, Wait, Value, Attribute } from 'serenity-js/lib/screenplay-protractor';
import { equals, contains } from '../../../support/chai-wrapper';
import { blank } from '../../../data/blanks';
export class InputFieldQuestion {
constructor(private inputField: Target) { }
isBlank = () => Task.where(`{0} ensures the ${this.inputField} is blank`,
Wait.until(this.inputField, Is.visible()),
See.if(Value.of(this.inputField), equals(blank))
)
hasAnError = () => Task.where(`{0} ensures the ${this.inputField} has an error`,
See.if(Attribute.of(this.inputField).called('class'), contains('ng-invalid'))
)
}
I then create classes specific to the scenario but simply extend the base question:
import { LoginForm } from '../scenery/login_form';
import { InputFieldQuestion } from './common';
class CheckIfTheUsernameFieldQuestion extends InputFieldQuestion {
constructor() { super(LoginForm.Username_Field) }
}
export let CheckIfTheUsernameField = new CheckIfTheUsernameFieldQuestion();
The beauty of node exports allow me to export an instantiated question class for use in my spec.
I just wanted to know if I am abusing the Serenity/JS framework or if this is okay? I want to establish a good framework and wanted to ensure I am doing everything to best practice. Any feedback is appreciated!
Sure, you could do it this way, although I'd personally favour composition over inheritance.
From what I can tell, you're designing standardised verification Tasks, which you want to use as follows:
actor.attemptsTo(
CheckIfTheUsernameField.isBlank()
)
You could accomplish the same result with a slightly more flexible design where a task can be parametrised with the field to be checked:
actor.attemptsTo(
EnsureBlank(LoginForm.Username_Field)
)
Where:
const EnsureBlank = (field: Target) => Task.where(`#actor ensures that the ${field} is blank`,
Wait.until(field, Is.visible()),
See.if(Value.of(field), equals(blank)),
);
Or, to follow the DSL you wanted:
const EnsureThat = (field: Target) => ({
isBlank: () => Task.where(`#actor ensures that the ${field} is empty`,
Wait.until(field, Is.visible()),
See.if(Value.of(field), equals(blank)),
),
hasError: () => Task.where(`#actor ensures that the ${field} has an error`,
See.if(Attribute.of(this.inputField).called('class'), contains('ng-invalid')),
),
});
Which can be used as follows:
actor.attemptsTo(
EnsureThat(LoginForm.Username_Field).isBlank()
);
Hope this helps!
Jan
I'm trying to create a Quill.js editor instance once component is loaded using mounted() hook. However, I need to set the Quill's content using Quill.setContents() on the same mounted() hook with the data I received from vuex.store.state .
My trouble here is that the component returns empty value for the state data whenever I try to access it, irrespective of being on mounted() or created() hooks. I have tried with getters and computed properties too. Nothing seems to work.
I have included my entry.js file, concatenated all the components to make things simpler for you to help me.
Vue.component('test', {
template:
`
<div>
<ul>
<li v-for="note in this.$store.state.notes">
{{ note.title }}
</li>
</ul>
{{ localnote }}
<div id="testDiv"></div>
</div>
`,
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
}
},
created() {
this.$store.dispatch('fetchNotes')
},
mounted() {
// Dispatch action from store
var quill = new Quill('#testDiv', {
theme: 'snow'
});
// quill.setContents(JSON.parse(this.localnote.body));
},
methods: {
setLocalCurrentNote(note) {
console.log(note.title)
return this.note = note;
}
}
});
const store = new Vuex.Store({
state: {
message: "",
notes: [],
currentNote: {}
},
mutations: {
setNotes(state,data) {
state.notes = data;
// state.currentNote = state.notes[1];
},
setCurrentNote(state,note) {
state.currentNote = note;
}
},
actions: {
fetchNotes(context) {
axios.get('http://localhost/centaur/public/api/notes?notebook_id=1')
.then( function(res) {
context.commit('setNotes', res.data);
context.commit('setCurrentNote', res.data[0]);
});
}
},
getters: {
getCurrentNote(state) {
return state.currentNote;
}
}
});
const app = new Vue({
store
}).$mount('#app');
And here is the index.html file where I'm rendering the component:
<div id="app">
<h1>Test</h1>
<test :localnote="$store.state.currentNote"></test>
</div>
Btw, I have tried the props option as last resort. However, it didn't help me in anyway. Sorry if this question is too long. Thank you for taking your time to read this. Have a nice day ;)
I copied your code and tested it ( of-course I created my own dummy notes so I could remove the get request ) and I was able to get the notes display on a page.
A couple of things that I realized from your code, you may need to add a store property as there are places in your component ( test ) where you are referencing it, yet you only define it on the 'app' component. So in this section of your code modify as shown below:
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
store : store
}
},
The key difference is the definition of the 'store' property. Please note that, what you have done, defining a "store" property in your app component, is correct, but the very same needs to be defined in "test" component as I have shown in the above code snippet above.
Second thing is, you are using $store and I guess that gives you undefined, unless as you said, in the libraries that you included this resolves accordingly, but on my side I had to remove all references of "$store" and replace it with just "store" (without the dollar sign).
Lastly for testing purposes, I would advise you to also
I am trying to create a global event bus so that two sibling components can communicate with each other. I have searched around; however, I cannot find any examples of how to implement one. This is what I have so far:
var bus = new Vue();
Vue.component('Increment', {
template: "#inc",
data: function() {
return ({count: 0})
},
methods: {
increment: function(){
var increment = this.count++
bus.$emit('inc', increment)
}
}
})
Vue.component('Display', {
template: "#display",
data: function(){
return({count: 0})
},
created: function(){
bus.$on('inc', function(num){
alert(num)
this.count = num;
});
}
})
vm = new Vue({
el: "#example",
})
I created my templates like so: http://codepen.io/p-adams/pen/PzpZBg
I'd like the Increment component to communicate the count to the Display component. I am not sure what I am doing wrong in bus.$on().
The problem is that within your bus.$on function, this refers to the bus. You just need to bind the current Vue instance to that function using .bind():
bus.$on('inc', function(num){
alert(num)
this.count = num;
}.bind(this));
You should also check out https://github.com/vuejs/vuex if you want to manage global application states.
EDIT: Since this page seems to get a lot of clicks I want to edit and add another method, per ChristopheMarois in the comments:
EDIT: In effort to make this answer a little clearer, and so future readers don't need to read comments here's what's happening:
Using a fat arrow like below binds the lexical scope of 'this' to the component instead of to the event bus.
bus.$on('inc', (num) => {
alert(num);
this.count = num;
});
Or removing the alert:
bus.$on('inc', (num) => this.count = num);
As you write ES5 JavaScript you have to be aware of the fact that what you refer to by using the this keyword might change, according to the scope, it is called from.
A useful metaphor to get your head around the this concept is to think of the curly braces in ES5 as fences, that contain/bind its own this.
When you use this in the callback function of your event bus, this does not refer to your Vue component, but the bus object, which has no count data, so the data you expect to update doesn't.
If you have/want to write ES5 syntax a common workaround (besides binding this as suggested by the accepted answer) is to assign the this keyword to a variable like so:
created: function(){
var self = this;
bus.$on('inc', function(num){
alert(num)
self.count = num;
});
}
If you can write ES6, do so whenever possible. You can always compile/transpile down to ES5 with Babel. The accepted answer shows you how by using arrow functions.
Arrow functions work in that case because they do not bind their own this.
To stick with the fence metaphor: imagine the ES6 arrow poking a hole in your function fence, so the outer this can pass through and you can call this as intended.
To learn more about ES6 arrow functions visit:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
This is answered long back, here is my solution using in vue.js-2
main.js
import Vue from 'vue'
import App from './App'
export const eventBus = new Vue({
methods:{
counter(num) {
this.$emit('addNum', num);
}
}
});
new Vue({
el: '#app',
template: '<App/>',
components: { App }
});
comp1.vue
//Calling my named export
import { eventBus } from '../../main'
<template>
<div>
<h1>{{ count }}</h1>
<button #click="counterFn">Counter</button>
</div>
</template>
<script>
import { eventBus } from '../../main'
export default {
name: 'comp-one',
data() {
return {
count: 0
}
},
methods: {
counterFn() {
eventBus.counter(this.count);
}
},
created() {
eventBus.$on('addNum', () => {
this.count++;
})
}
}
</script>
How about this? Assume Vue.js 2.
Create a reusable Event-Bus component and attach it to Vue via plugin pattern:
// ./components/EventBus.vue
import Vue from 'vue'
export const EventBus = new Vue()
// ./plugins/EventBus.js
export default {
install(Vue) {
const { EventBus } = require('../components/EventBus')
Vue.prototype.$bus = EventBus
}
}
// ./main.js
import EventBus from './plugins/EventBus'
Vue.use(EventBus)
Then, you can do anywhere in your code:
this.$bus.$emit('some-event', payload)
As a side note, try to utilize the Event-Bus pattern as last resort.