I have a dynamic template for buttons in Blaze, looks like this (simplified):
button.html
<template name="Button">
<button {{attributes}}>
<span class="button__text">{{> UI.contentBlock}}</span>
</button>
</template>
button.js
import {Template} from 'meteor/templating';
import cx from 'classnames';
import './button.html';
Template.Button.helpers({
attributes() {
const instance = Template.instance();
let {data} = instance;
return {
disabled: data.disabled,
'class': cx('button', data.class)
};
}
});
Attempt to set dynamic data attribute:
{{#Button class="js-add-contact" data-phase-index={{index}}}}Add Contact{{/Button}}
This insertion of index (let's assume it's just a simple, dynamic string) into data-phase-index throws an error: the content block was not expecting the {{. I am unsure of another way to get that dynamic data into the template. There's also the issue of getting the data- attributes recognized by Button in the attributes() helper. Can anyone clear this up?
Simply data-phase-index=index should work.
Since you are already within double curly braces for your Button template call, Meteor knows it will get interpreted values. For example, see that you have to use quotes around your string in class="js-add-contact".
As usual, Meteor will try to interpret index from a helper, or from data context.
Related
I am using vue3 and wonder how to pass data the correct way.
My Component structure is one table (items loaded via pinia store): XTableComponent
The XTableComponent has a child: XModalComponent. In the rendered table I have a button in each row. #click on that stores the current item in a data item
XTableComponent:
<template>
...that mentioned table in each line a button with #click and the item in the iteration as param
<x-model-component v-if="currentItem" :item="currentItem ref="x-modal"></x-modal-component>
</template>
<script>
export default {
data: () => {
return {
currentItem: {},
itemListStore: useItemListStore()
}
},
computed: {
itemList() {
return this.itemListStore.list
}
methods: {
showModal(item){
this.currentItem = item
this.$refs['x-modal'].show()
}
}
}
</script>
My Child component looks a bit like this:
XModalComponent:
<template>
....
<input v-model:value="innerItem.something" type="text">
<button #click="save">save</button>
</template>
<script>
export default {
props: {
item: Object
},
data: () => {
return {
innerItem: {}
}
}
mounted() {
this.innerItem = item
},
methods: {
save() {
console.log(this.innerItem) //this does not show the manipulated value of `something`
}
}
}
</script>
Now, if I manipulate the input in my child component, and trigger a click event, the value does not get changed on my data item ...
what did I get wrong in vue3 with reactiveness, proxeis and passing props?
p.s. my code is kind of pseude code here, so please be fair with me on typos, or obvious parts
that are missing
p.p.s. I am used to vue2 quite well, so maybe I mix concepts. please tell me that too.
p.p.p.s. my table renders correctly, the modal looks fine. i double checked all names and typos.
So, as we figured out, the problem came from the way innerItem.something was bound to the input, and some confusion around the v-model directive.
As a recap, the v-model directive is short-hand for setting a prop on a component and listening to an event which updates the value.
In Vue 2, that was:
<child-component
:value="myValue"
#input="(nevValue) => myValue = newValue"
/>
which is equivalent to
<child-component v-model="myValue"/>
and it allows a variable to be changed by parent as well as child ("two-way binding"). Note that property name and event matches that of a HTML input element (the "value" attribute and the "input" event), probably because it represents the most familiar case, where a value is bound to an input:
<input type="text" v-model="myText"/>
However, to allow for multiple two-way bindings on a component, Vue 2 also introduced a second way, which allows to bind to any of the child components props, not just "value". This is the .sync modifier:
<child-component :childComponentProp.sync="myVar"/>
which is equivalent to:
<child-component
:childComponentProp="myVar"
#update:childComponentProp="(newValue) => myVar = newValue"
/>
In Vue 3, they decided to unify the two, dropping .sync and instead allowing to pass a prop name to v-model similar to how slot names are passed to the v-slot directive, i.e. v-model:childComponentProp="myVar", and similar as v-slot alone is equivalent to v-slot:default, v-model alone is equivalent to v-model:modelValue. So it is equivalent to:
<my-component
:modelValue="myValue"
#update:modelValue="(nevValue) => myValue = newValue"
/>
But the above only applies for Vue components. When using v-model on an HTML input element, it sill behaves like in Vue 2 and binds to the "value" attribute and the "input" event. It is still equivalent to:
<input :value="myValue" #input="(nevValue) => myValue = nevValue"/>
However, that behavior is a special case of plain v-model (i.e. without a prop name). And I think this is where the confusion comes from.
Using v-model:value explicitly binds to the #update:value event, i.e. this
<input v-model:value="innerItem.something" type="text">
is equivalent to:
<input type="text"
:value="innerItem.something"
#update:value="(newValue) => innerItem.something = newValue"
/>
but that event is not sent by a plain HTML element.
So long long story short, you have to use v-model= instead of v-model:value= when binding to a native input element.
Does that make sense? Hope it helps.
I'm using an external library rendered using Vue3. It has the following component from a third part library [Edit: I realize the GitHub repo for that library is out of date, so updating with the actual code in my node_modules.]
<template>
<div class="socket" :class="className" :title="socket.name"></div>
</template>
<script>
import { defineComponent, computed } from "vue";
import { kebab } from "./utils";
export default defineComponent({
props: ["type", "socket"],
setup(props) {
const className = computed(() => {
return kebab([props.type, props.socket.name]);
});
return {
className
};
}
});
</script>
It renders based on a Socket object passed as a prop. When I updated the name property of the Socket, I see the title updated accordingly. However, the CSS/class does not update. I've tried $forceRefresh() on its parent, but this changes nothing.
Update: I was able to move the rendering code to my own repo, so I can now edit this component if needed.
Based on this updated code, it seems the issue is that the class is computed. Is there any way to force this to refresh?
The only time it does is when I reload the code (without refreshing the page) during vue-cli-service serve.
For reference, the | kebab filter is defined here:
Vue.filter('kebab', (str) => {
const replace = s => s.toLowerCase().replace(/ /g, '-');
return Array.isArray(str) ? str.map(replace) : replace(str);
});
Do filtered attributes update differently? I wouldn't think so.
I was also wondering if it could be a reactivity issue, and whether I needed to set the value using Vue.set, but as I understand it that's not necessary in Vue3, and it's also not consistent with the title properly updating.
Computed properties are reactive, however Vue does not expect you to mutate a prop object.
From the documentation:
Warning
Note that objects and arrays in JavaScript are passed by reference, so
if the prop is an array or object, mutating the object or array itself
inside the child component will affect the parent state and Vue is
unable to warn you against this. As a general rule, you should avoid
mutating any prop, including objects and arrays as doing so ignores
one-way data binding and may cause undesired results.
https://v3.vuejs.org/guide/component-props.html#one-way-data-flow
I know that this says, that you should not mutate it in the child, but the general rule is, that you should not mutate properties at all, but instead create new object with the modified data.
In your case the computed function will look for changes in the properties itself, but not the members of the properties, that is why it is not updating.
My objective is to let end-users build some customization in my app. Can I do something like this? I know this is sometimes also referred to as liquid templates, similar to how handlebars.js works.
app.svelte
<script>
let name = 'world';
const template = '<h1> Hello {name} </h1>'
</script>
{#html template}
I'm sorry if this is already answered, but I could not find it.
Link to REPL.
This is a little bit hacky but it will do the trick:
<script>
let name = 'world';
let template;
</script>
<div class="d-none" bind:this={template}>
<h1>Hello {name}</h1>
</div>
{#html template?.innerHTML}
<style>
.d-none {
display: none;
}
</style>
If the template should be a string one solution might be to simply replace the {variable} with the values before displaying via a {#html} element >> REPL
Notice the warning in the SVELTE docs
Svelte does not sanitize expressions before injecting HTML. If the data comes from an untrusted source, you must sanitize it, or you are exposing your users to an XSS vulnerability.
(Inside a component $$props could be used to get the passed values in one object if they were handled seperately)
<script>
const props = {
greeting: 'Hello',
name: 'world'
}
let template = '<h1> {greeting} {name} </h1>'
let filledTemplate = Object.entries(props).reduce((template, [key,value]) => {
return template.replaceAll(`{${key}}`, value)
},template)
</script>
{#html filledTemplate}
Previous solution without string
To achieve this I would build a Component for every template and use a <svelte:component> element and a switch to display the selected one > REPL
[App.svelte]
<script>
import Template1 from './Template1.svelte'
import Template2 from './Template2.svelte'
let selectedTemplate = 'template1'
const stringToComponent = (str) => {
switch(str) {
case 'template1':
return Template1
case 'template2':
return Template2
}
}
</script>
<button on:click={() => selectedTemplate = 'template1'}>Template1</button>
<button on:click={() => selectedTemplate = 'template2'}>Template2</button>
<svelte:component this={stringToComponent(selectedTemplate)} adjective={'nice'}/>
[Template.svelte]
<script>
export let adjective
</script>
<hr>
<h1>This is a {adjective} template</h1>
<hr>
Well, you could do it, but that not what Svelte was designed for.
Svelte was designed to compile the template at build time.
I'd recommend using a template engine (like handlebars) for your use case.
A. Using Handlebars inside Svelte REPL:
<script>
import Handlebars from 'handlebars';
let name = 'world';
const template = "<h1> Hello {{name}} </h1>";
$: renderTemplate = Handlebars.compile(template);
</script>
{#html renderTemplate({ name })}
This of course limits the available syntax to handlebars, and you can't use svelte components inside a handlebar template.
B. Dynamic Svelte syntax templates inside a Svelte app
To be able to use svelte syntax you'll need to run the svelte compiler inside the frontend.
The output the compiler generates is not directly usable so you'll also need to run a bundler or transformer that is able to import the svelte runtime dependencies. Note that this is a separate runtime so using <svelte:component> wouldn't behave as expected, and you need to mount the component as a new svelte app.
In short, you could, but unless you're building a REPL tool you shouldn't.
C. Honourable mentions
Allow user to write markdown, this gives some flexibility (including using html) and use marked in the frontend to convert it to html.
Write the string replacements manually {#html template.replace(/\{name\}/, name)}
I have set up a bare bones vue project to show the problem. The only thing I added was the axios package. The problem is when I try to set the property of child component after an asynchronous call I cant read that property in the component. If you look at the code you can see I console log several times to show when I can get the data and when I cant. Please help me figure out what im missing here.
Parent
<template>
<div id="app">
<HelloWorld :test_prop="testData" :test_prop2="testData2" :test_prop3="testData3" test_prop4="I work also"/>
<div>{{testData5}}</div>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import axios from 'axios';
export default {
name: 'app',
components: {
HelloWorld
},
data() {
return {
testData: '',
testData2: 'I work just fine',
testData3: '',
testData5: ''
}
},
created: function(){
var self = this;
this.testDate3 = 'I dont work';
axios.get('https://jsonplaceholder.typicode.com/posts/42').then(function(response){
//I need this one to work
self.testData = 'I dont work either';
self.testData5 = 'I work also';
});
}
}
</script>
Child
<template>
</template>
<script>
export default {
name: 'HelloWorld',
props: ['test_prop', 'test_prop2', 'test_prop3', 'test_prop4'],
data() {
return {
comp_data: this.test_prop,
comp_data2: this.test_prop2,
comp_data3: this.test_prop3,
comp_data4: this.test_prop4
}
},
created: function(){
console.log(this.test_prop);
console.log(this.test_prop2);
console.log(this.test_prop3);
console.log(this.test_prop4);
}
}
</script>
Your console.log inside created hook will show you the initial state of this variables in Parent component. That's because Parent's created hook and Child's created hook will run at the same time.
So, when you solve your promise, Child component was already created. To understand this behavior, put your props in your template using {{ this.test_prop }}.
To solve it, depending on what you want, you can either define some default value to your props (see) or render your child component with a v-if condition. That's it, hope it helps!
On Vue created hook only the initial values of properties passed from main component. Therefore later updates (like your example "after ajax call") in main component will not effect to child component data variables because of that already child created hook take place.
If you want to update data later one way you can do like this:
watch: {
test_prop: function(newOne){
this.comp_data = newOne;
}
}
Adding watcher to property changes will update the last value of property from main component.
And also edit the typo this.testDate3. I guess it must be this.testData3
I'm building an app with Meteor using the react-komposer package. It is very simple: There's a top-level component (App) containing a search form and a list of results. The list gets its entries through the props, provided by the komposer container (AppContainer). It works perfectly well, until I try to implement the search, to narrow down the results displayed in the list.
This is the code I've started with (AppContainer.jsx):
import { Meteor } from 'meteor/meteor';
import { composeWithTracker } from 'react-komposer';
import React, { Component } from 'react';
import Entries from '../api/entries.js';
import App from '../ui/App.jsx';
function composer(props, onData) {
if (Meteor.subscribe('entries').ready()) {
const entries = Entries.find({}).fetch();
onData(null, {entries});
};
};
export default composeWithTracker(composer)(App);
App simply renders out the whole list of entries.
What I'd like to achieve, is to pass query parameters to Entries.find({}).fetch(); with data coming from the App component (captured via a text input e.g.).
In other words: How can I feed a parameter into the AppContainer from the App (child) component, in order to search for specific entries and ultimately re-render the corresponding results?
To further clarify, here is the code for App.jsx:
import React, { Component } from 'react';
export default class App extends Component {
render() {
return (
<div>
<form>
<input type="text" placeholder="Search" />
</form>
<ul>
{this.props.entries.map((entry) => (
<li key={entry._id}>{entry.name}</li>
))}
</ul>
</div>
);
}
}
Thanks in advance!
I was going to write a comment for this to clarify on nupac's answer, but the amount of characters was too restrictive.
The sample code you're looking for is in the search tutorial link provided by nupac. Here is the composer function with the corresponding changes:
function composer(props, onData) {
if (Meteor.subscribe('entries', Session.get("searchValues")).ready()) {
const entries = Entries.find({}).fetch();
onData(null, {entries});
};
};
The solution is the session package. You may need to add it to your packages file and it should be available without having to import it. Otherwise try with import { Session } from 'meteor/session';
You just need to set the session when submitting the search form. Like this for instance:
Session.set("searchValues", {
key: value
});
The subscription will fetch the data automatically every time the specific session value changes.
Finally, you'll be able to access the values in the publish method on the server side:
Meteor.publish('entries', (query) => {
if (query) {
return Entries.find(query);
} else {
return Entries.find();
}
});
Hope this helps. If that's not the case, just let me know.
There are 2 approaches that you can take.
The Subscription way,
The Meteor.call way,
The Subscription way
It involves you setting a property that you fetch from the url. So you setup your routes to send a query property to you Component.Your component uses that property as a param to send to your publication and only subscribe to stuff that fits the search criteria. Then you put your query in your fetch statement and render the result.
The Meteor.call way
Forget subscription and do it the old way. Send your query to an endpoint, in this case a Meteor method, and render the results. I prefer this method for one reason, $text. Minimongo does not support $text so you cannot use $text to search for stuff on the client. Instead you can set up your server's mongo with text indexes and meteor method to handle the search and render the results.
See what suits your priorities. The meteor.call way requires you to do a bit more work to make a "Search result" shareable through url but you get richer search results. The subscription way is easier to implement.
Here is a link to a search tutorial for meteor and read about $text if you are interested