I wanna use svelte for a little app that Im making. The app was half finished using plain html/css/js when I stumbled upon Svelte.
I was using a lot of javascript classes and aimed for object oriented programming.
Now looking at svelte, it looks like its not made for OOP at all. Am I wrong? Properties of classes wont be tracked and updated by svelte.
Maybe my approach is wrong. I basicly used a View/Model pattern, where I have a model class object that Im feeding the svelte component. Using the object's properties in html wont update obviously. (This works great with angular i.e.)
<script lang="ts">
import type { Key } from "../key";
export let key: Key
const onTrigger = () => {
key.trigger()
}
const onRelease = () => {
key.release()
}
</script>
<div
class="key"
class:black={key.note[1] == '#' || key.note[1] === 'b'}
class:pressed={key.isPressed}
on:pointerdown={onTrigger}
on:pointerup={onRelease}
on:pointerleave={onRelease}
style={key.isPressed ? 'transform: scale(1.5); transform-origin: center;' : ''}>
<div class="key-mapping">{#html key.mapping.toLocaleUpperCase() + '<br/>' }</div>
<div class="key-note">{ key.note + (key.octave ? key.octave.toString() : '') }</div>
</div>
(Key represents a piano key sitting inside a piano component, things like key.isPressed or key.octave wont update, because they are changed in the model class)
Demo here
I really dont wanna use the store for ALL properties of my classes that I use in html, because I think this is not the purpose of the store. I was hoping to save some code by using Svelte and not make it weird and complex.
I saw the trick to reassign the whole object like this
object.property = 'some value'
object = object
to trigger reactivity, but this wont work when changing the properties outside of the component.
Also using the reactive marking $: { ... } I wasnt able to update any class' property (Only when changing it directly from a html event)
Also saw a decorator function to make classes reactive to svelte, but the decorator makes the class singleton too, which makes it useless to me.
So there are a few questions I wanna ask:
Is there any proper way to update class properties in Svelte?
If not, whats the prefered coding style? Functional?
Will there be OOP support in the future?
You don't need dummy assignments as soon as you assign to a property (rather than invoking a method) and there is no issue with using classes as long as you do not "hide" changes from Svelte's compiler.
E.g. this will work just fine (inside the component):
const onTrigger = () => {
key.isPressed = true;
}
const onRelease = () => {
key.isPressed = false;
}
In general your components should be fairly specialized so they do not have to deal with deeply nested data and complex modifications which makes it easy to lose reactivity.
Ideally you just have some very simple local state via component properties rather than objects. Here your Key component should just use properties for all its state e.g. isPressed should just be a property that then can be bound on the level of the parent component.
In my Vue 2 application I have a big object that is passed from the root component down several levels to some child components as a prop. When I change some property of the object the child components should update and rerender. In some cases they do, in other cases they don't. I need some help spotting why it does not work.
Here is a child component, which does not update:
<template>
<div class="upgradeBar">
{{level}}
<div
v-for="lvlNum in maxLevel + 1"
class="level"
v-bind:class="{reached: isLevelReached(lvlNum - 1)}"
></div>
<button
class="btnUpgrade"
#click="onLevelUp()"
v-if="!isLevelReached(maxLevel)"
>
+
</button>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import Upgradable from "../../../models/Upgradable";
#Component()
export default class UpgradeBar extends Vue {
name: 'UpgradeBar';
#Prop() entity: Upgradable;
get level(): number {
return this.entity.level;
}
get maxLevel(): number {
return this.entity.MAX_LEVEL;
}
onLevelUp() {
this.entity.levelUp();
}
isLevelReached(level: number): Boolean {
return this.entity.level >= level;
}
}
</script>
The component is called like this:
<UpgradeBar :entity="entity" />
All the code works. When I click the btnUpgrade button entity.level is indeed changed, but I need to close and reopen the component to see the changes. Also the browser dev tool does not show the change instantly. I need to click on the component to refresh the values in the debugger.
EDIT:
The entity class looks basicly like this (excerpt):
class Entity {
name: string = 'some name';
level: number = 1;
}
I searched deeper and it seems to boils down to this: Some properties of the object are reactive (they have getters / setters created by vue) and some don't. entity.name has a setter, so changing it updates the component. entity.level does not. Here's the question: Why are they treated differently? Here is a log:
Can't tell for sure without seeing the code for entity.levelUp, but it seems like a reactivity issue, that may be solved by using Vue.$set inside that function.
You can confirm this being the case by adding this.$forceUpdate(); after this.entity.levelUp();
update
this._level = this._level + 1;
can be changed to
Vue.$set(this, _level, this._level + 1);
You will need to import Vue in that component/file to access the $set function
You don't show (or I can't find) the code that changes the object, but are you using $set() or Vue.set() instead of simply changing the object's properties directly? Changing a property directly generally doesn't work because of reactivity limitations
Edited to add:
I see now. I think you want something like:
this.$set(this, '_level', this._level + 1);
I have created a custom element based on bootstrap-toggle that looks as follows:
toggle.ts:
import {bindingMode, bindable, customElement} from "aurelia-framework";
#customElement('toggle')
export class Toggle {
#bindable({ defaultBindingMode: bindingMode.twoWay }) checked;
input;
attached() {
$(this.input).bootstrapToggle();
}
}
toggle.html:
<template>
<require from="bootstrap-toggle/css/bootstrap2-toggle.min.css"></require>
<require from="bootstrap-toggle"></require>
<input ref="input" data-toggle="toggle" type="checkbox" checked.bind="checked">
</template>
The problem ist that the binding for the checked attribute is never updated when the switch is toggled via the UI. I am aware of the common pitfalls when using Aurelia with jQuery based components as described here. However, in my understanding this should not apply to bootstrap-toggle, as this component triggers a change event on the input element on toggle. I have verified that this change event bubbles up to my custom component.
The workaround I currently use is this:
toggle.ts:
import {bindingMode, bindable, customElement} from "aurelia-framework";
#customElement('toggle')
export class Toggle {
#bindable({ defaultBindingMode: bindingMode.twoWay }) checked;
input;
attached() {
$(this.input).bootstrapToggle().on('change', (event) => this.checked = event.target.checked);
}
}
However, I do not understand why this should be necessary.
I have created a test project based on Aurelia's navigation-skeleton that can be downloaded here.
I would appreciate some help in understanding this!
The event listener used by jquery uses the capture phase and presumably does preventDefault or something among those lines that prevents the event from bubbling back up.
The change event dispatched by this line: if (!silent) this.$element.change() does not actually propagate to the bubble phase. Aurelia uses the bubble phase and thus never receives the event.
You can see the difference if you dispatch one manually in your browser console like so:
document.querySelector("[data-toggle]").dispatchEvent(new CustomEvent("change"));
This will result in the appropriate listeners being invoked and updates the bound property.
What this line does: .on('change', (event) => this.checked = event.target.checked); is also add a listener on the capture phase which then of course works. But then you might as well remove the checked.bind='checked' because that effectively does nothing.
There isn't necessarily any straight-forward fix for this, these CSS frameworks tend to just have very intrusive javascript. I would generally recommend against including their scripts and look for (or implement) them natively in Aurelia. It's very easy to do so and just works a lot better.
i trying to build logic to translate only part of the page(module) with i18n lib.
i have set i18n globally to change language on page when i change language, but i would like to have one module on that page (like some kind of preview for email) on different language which i can change on that module via some dropdown field. Like some kind of scoped i18n.
I'm using aurelia-i18n 1.4.0 version.
is it possible to set <span t="messages.hello_message">hello<span> to watch for local module changes for language and not the global one, but again to use the same translation files as global does.
Did anyone have some similar problem or idea how can I do this? thanks. :)
You can't do that out of the box. When you change the active locale using setLocale the method fires an event and signals an update to binding behaviors https://github.com/aurelia/i18n/blob/master/src/i18n.js#L54.
The TCustomAttribute listens for those changes and automatically rerenders bindings. What you could do though is create your own custom attribute as seen here https://github.com/aurelia/i18n/blob/master/src/t.js and override the bind and unbind methods where you define the condition when the update of translations should happen.
--- Update with example ---
Ok so here's a small example what I was thinking about, might not be the nicest way but it should do it.
In your main.js add a new globalResources
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.plugin('aurelia-i18n', (instance) => {
...
})
.globalResources("./foo-custom-attribute") // <-- this here
now create a file foo-custom-attribute.js
import {TCustomAttribute} from 'aurelia-i18n';
import {customAttribute} from 'aurelia-framework';
#customAttribute('foo')
export class FooCustomAttribute extends TCustomAttribute {
constructor(element, i18n, ea, tparams) {
super(element, i18n, ea, tparams);
}
bind() {
this.params = this.lazyParams();
if (this.params) {
this.params.valueChanged = (newParams, oldParams) => {
this.paramsChanged(this.value, newParams, oldParams);
};
}
let p = this.params !== null ? this.params.value : undefined;
this.service.updateValue(this.element, this.value, p);
}
unbind() {}
}
This essentially creates a new attribute called foo, which extends the TCustomAttribute and overrides the bind/unbind methods to exclude the signaling and listening to language changed events.
In your view you can now use
<span t="demo"></span>
<span foo="demo"></span>
Toggling the language now will change the t attribute as usual but will keep the foo as it is.
I'd like to insert new vuejs components on the fly, at arbitrary points within a block of not-necessarily-predefined HTML.
Here's a slightly contrived example that demonstrates the sort of thing I'm trying to do:
Vue.component('child', {
// pretend I do something useful
template: '<span>--><slot></slot><--</span>'
})
Vue.component('parent', {
data() {
return {
input: 'lorem',
text: '<p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p>'
}
},
template: `<div>
Search: <input type='text' v-model="input"><br>
<hr>
This inserts the child component but doesn't render it
or the HTML:
<div>{{output}}</div>
<hr>
This renders the HTML but of course strips out the child component:
<div v-html="output"></div>
<hr>
(This is the child component, just to show that it's usable here:
<child>hello</child>)
<hr>
This is the goal: it renders both the input html
and the inserted child components:
TODO ¯\_(ツ)_/¯
</div>`,
computed: {
output() {
/* This is the wrong approach; what do I replace it with? */
var out = this.text;
if (this.input) {
this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
var regex = new RegExp(this.input, "gi");
out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
}
return out;
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
<parent></parent>
</div>
In the above snippet, assume data.text is sanitized HTML. <child> is some sub-component that does something useful, which I want to wrap around chunks of data.text that aren't known ahead of time. (input is just for demo here. This MCVE doesn't really resemble the code I'm building, it's just an example that shows the sort of situation I'm stuck on.)
So: how would I change either the output function or the parent component's template, such that both the HTML from input and the inserted <child> templates are rendered properly?
What I've tried
In Vue 1, the answer to this would be a straightforward $compile. I'm using vuejs2 which removed $compile (out of justifiable concern that it made it too easy to naively introduce XSS vulnerabilities.)
v-html sanitizes what you feed it, which strips the child component out. Obviously this is not the way to do this. (That page suggests using partials instead, but I'm not sure how that could be applied to this situation; in any case partials have also been removed from vue2.)
I've tried passing the results of output() into another component which would then use it as its template. This seems like a promising approach, but I can't figure out how to change that secondary component's template. template only accepts a string, not a function like many of the other component properties, so I can't pass the template html in, say, a prop. Something like rewriting this.template inside beforeMount() or bind() would have been nice, but no joy there either. Is there some other way to replace a component's template string before it's mounted?
Unlike template, I can pass data to a component's render() function... but then I'm still stuck having to parse that html string into nested createElement functions. Which is exactly what Vue is doing internally in the first place; is there some way to hook into that here short of reinventing it myself?
Vue.component('foo', {
props: ['myInput'],
render(createElement) {
console.log(this.myInput); // this works...
// ...but how to parse the html in this.myInput into a usable render function?
// return createElement('div', this.myInput);
},
})
I wasn't able to cheat my around this with inline-template, either: <foo inline-template>{{$parent.output}}</foo> does exactly the same thing as a plain old {{output}}. In retrospect that should have been obvious, but it was worth a shot.
Maybe constructing an async component on the fly is the answer? This could clearly generate a component with an arbitrary template, but how would I reasonably call that from the parent component, and feed output to the constructor? (It would need to be reusable with different input, with multiple instances potentially visible simultaneously; no globals or singletons.)
I've even considered ridiculous stuff like having output() split the input into an array at the points where it would have inserted <child>, and then doing something like this in the main template:
...
<template v-for="chunk in output">
<span v-html="chunk"></span>
<child>...</child>
</template>
....
That would be doable, if laborious -- I'd have to split out what goes in the child's slot into a separate array too and get it by index during the v-for, but that could be done... if input were plain text instead of HTML. In splitting HTML I'll often wind up with unbalanced tags in each chunk, which can mess up the formatting when v-html rebalances it for me. And anyway this whole strategy feels like a bad hack; there must be a better way.
Maybe I just drop the whole input into a v-html and then (somehow) insert the child components at the proper positions through after-the-fact DOM manipulation? I haven't explored this option too deeply because it, too, feels like a hack, and the reverse of the data-driven strategy, but maybe it's a way to go if all else fails?
A couple of pre-emptive disclaimers
I'm very well aware of the XSS risks involved in $compile-like operations. Please be assured that none of what I'm doing involves unsanitized user input in any way; the user isn't inserting arbitrary component code, instead a component needs to insert child components at user-defined positions.
I'm reasonably confident that this is not an XY problem, that I really do need to insert components on the fly. (I hope it's obvious from the number of failed attempts and blind alleys I've run down that I've put more than a little thought into this one!) That said, if there's a different approach that leads to similar results, I'm all ears. The salient point is that I know which component I need to add, but I can't know ahead of time where to add it; that decision happens at run time.
If it's relevant, in real life I'm using the single-file component structure from vue-cli webpack template, not Vue.component() as in the samples above. Answers that don't stray too far from that structure are preferred, though anything that works will work.
Progress!
#BertEvans points out in comments that Vue.compile() is a thing that exists, which is an I-can't-believe-I-missed-that if ever there was one.
But I'm still having trouble using it without resorting to global variables as in that documentation. This renders, but hardcodes the template in a global:
var precompiled = Vue.compile('<span><child>test</child></span>');
Vue.component('test', {
render: precompiled.render,
staticRenderFns: precompiled.staticRenderFns
});
But various attempts to rejigger that into something that can accept an input property have been unsuccessful (the following for example throws "Error in render function: ReferenceError: _c is not defined", I assume because the staticRenderFns aren't ready to go when render needs them?
Vue.component('test', {
props: ['input'],
render() { return Vue.compile(this.input).render()},
staticRenderFns() {return Vue.compile(this.input).staticRenderFns()}
});
(It's not because there are two separate compile()s -- doing the precompile inside beforeMount() and then returning its render and staticRenderFns throws the same error.)
This really feels like it's on the right track but I'm just stuck on a dumb syntax error or the like...
As mentioned in the my comment above, $compile was removed, but Vue.compile is available in certain builds. Using that below works as I believe you intend except in a couple cases.
Vue.component('child', {
// pretend I do something useful
template: '<span>--><slot></slot><--</span>'
})
Vue.component('parent', {
data() {
return {
input: 'lorem',
text: '<div><p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p></div>'
}
},
template: `<div>
Search: <input type='text' v-model="input"><br>
<hr>
<div><component :is="output"></component></div>
</div>`,
computed: {
output() {
if (!this.input)
return Vue.compile(this.text)
/* This is the wrong approach; what do I replace it with? */
var out = this.text;
if (this.input) {
this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
var regex = new RegExp(this.input, "gi");
out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
out = Vue.compile(out)
}
return out;
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
<parent></parent>
</div>
You mentioned you are building with webpack and I believe the default for that build is Vue without the compiler, so you would need to modify it to use a different build.
I added a dynamic component to accept the results of the compiled output.
The sample text is not a valid template because it has more than one root. I added a wrapping div to make it a valid template.
One note: this will fail if the search term matches all or part of any of the HTML tags in the text. For example, if you enter "i", or "di" or "p" the results will not be what you expect and certain combinations will throw an error on compilation.
I'm posting this as a supplement to Bert Evans's answer, for the benefit of vue-cli webpack users who want to use .vue files instead of Vue.component(). (Which is to say, I'm mostly posting this so I'll be able to find this information when I inevitably forget it...)
Getting the right Vue build
In vue-cli 2 (and possibly 1?), to ensure Vue.compile will be available in the distribution build, confirm webpack.base.conf.js contains this line:
'vue$': 'vue/dist/vue.esm.js' // or vue/dist/vue.common.js for webpack1
instead of 'vue/dist/vue.runtime.esm.js'. (If you accepted the defaults when running vue init webpack you will already have the full standalone build. The "webpack-simple" template also sets the full standalone build.)
Vue-cli 3 works somewhat differently, and does not have Vue.compile available by default; here you'll need to add the runtimeCompiler rule to vue.config.js:
module.exports = {
/* (other config here) */
runtimeCompiler: true
};
The component
The "child" component can be a normal .vue file, nothing special about that.
A bare-bones version of the "parent" component would be:
<template>
<component :is="output"></component>
</template>
<script>
import Vue from 'vue';
import Child from './Child'; // normal .vue component import
export default {
name: 'Parent',
computed: {
output() {
var input = "<span>Arbitrary single-root HTML string that depends on <child></child>. This can come from anywhere; don't use unsanitized user input though...</span>";
var ret = Vue.compile(input);
ret.components = { Child }; // add any other necessary properties similarly
ret.methods = { /* ... */ } // like so
return ret;
}
}
};
</script>
(The only significant difference between this and the non-webpack version is importing the child, then declaring the component dependencies as ret.components: {Child} before returning it.)