Problem with template implementation (VUE) using .NET (MVC) - javascript

I'm trying to implement an external template (creating an HTML page), but I can not succeed. This page is a ASP.NET MVC page that contains a Vue app.
I want to move the template section of this component to an external file, but whenever I do this it doesn't work.
The following (below) does work, but it's not easy to maintain or build upon due to loss of text editing features.
Vue.component('my-component', {
template: '#my-component'
}
This is the current code and it works perfectly:
var foo = Vue.component('foo', {
template:'
<table class="table">
<template v-for="(foo, ColName, index) in foos">
<template v-if="index % 3 == 0">
<tr>
</tr>
</template>
<th>{{ColName}}</th>
<td v-if="foo">{{foo}}</td>
<td v-else> -- </td>
</template>
</table>',
data: function () {
return {
foos: null,
NumColuns: 3
}
},
mounted() {
axios
.get('/api/account/foo/')
.then(response => {
this.foos = response.data
})
.catch(error => {
console.log(error1)
this.errored = true
})
.finally(() => this.loading = false)
}
});
var foo = new Vue({
el: '#foo-vue'
});

To fetch the HTML template from a file you need a async component.
Documentation: https://v2.vuejs.org/v2/guide/components-dynamic-async.html
In your example fetch the HTML and return the promise in the Vue.component factory.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<foo></foo>
</div>
<script>
var foo = Vue.component('foo', () => {
return fetch('/template.html')
.then((response) => response.text())
.then((html) => {
return {
template: html,
data: function () {
return { foos: null, NumColuns: 3 }
},
mounted() {
this.foos = [{ foo: 1, ColName: "A" }];
// axios.get('/api/account/foo/').then(response => {
// this.foos = response.data
// })
// .finally(() => this.loading = false)
}
};
});
});
var foo = new Vue({
el: '#app'
});
</script>
</body>
</html>
template.html
<table class="table">
<template v-for="(foo, ColName, index) in foos">
<template v-if="index % 3 == 0">
<tr>
</tr>
</template>
<th>{{ColName}}</th>
<td v-if="foo">{{foo}}</td>
<td v-else> -- </td>
</template>
</table>

The best thing you can do is configure an entire Vue project, so you can use .vue template files. I recently found an article that explains in detail how to achieve this, but it's specifically aimed at browser extensions. The steps you have to undertake to achieve it, in this case, are identical for the most part, so I'll just link the article here anyway: https://medium.com/javascript-in-plain-english/how-i-built-a-browser-extension-with-vue-part-2-2c4ab2dd752d.
Basically what's happening is, you'll configure the Vue project yourself with Webpack to compile the Vue files into one app.js file, which you'll then reference in your file (in the tutorial this file is his app.html). It will then inject its components into the #app element and handle all virtual DOM manipulations.

If your view is a razor .cshtml template then you can just extract your template to a html file and load that file to your ASP.NET MVC view as raw html code since Vue template is valid html:
<!-- in cshtml view -->
<script id="my-component" type="text/x-template">
#Html.Raw(File.ReadAllText(Server.MapPath("~/Path/To/Template/my-component-template.html")))
</script>
And in your Vue component it should work with template: '#my-component' as you mentioned.
Docs: Vue: Edge Cases
P.S. You can prepare a partial view for your components or create some extension method to make code more readable and add some caching.

Related

Vue not rendering fetched html

I am using vue in shopify and am working on a collection page. When I click on a filter, it‘s an href and it updates the url and reloads the page.
So I have a product grid
<div class="grid-wrapper">
<template v-for="(product, index) in collection.products">
<div class="product-item"></div>
</template>
</div>
And my idea was to just use the same url with fetch so the page doesn‘t reload.
I did this
fetch('/collections?variant=black')
.then(res => res.text())
.then(html => new DOMParser().parseFromText(html, 'text, html'))
.then(data => {
document.querySelector('.grid-wrapper').innerHTML = data.querySelector('.grid-wrapper').innerHTML
})
This does not work because I get back the actual <template v-for…> as the new innerHTML and vue isnt taking over. How can I solve this
In shopify I converted the object like so
const collection = (() => {
const collection = {{ collection | json }}
const products = {{ collection.products | json }}
collection.products = products
return collection
})();
Then in my vue instance
new Vue.createApp({
data() {
collection: collection
}
}).mount('#app')
You're approaching this in the traditional JavaScript way of manipulating the DOM directly. In Vue, we set state which can then be rendered by your template.
Instead:
Create a data attribute to store your state
Under methods, write a function to fetch your data, then update the components data
Call your function in the components created hook
In you're template render the results
Don't forget to check it's present first, with v-if
You can use v-for to iterate over, and render lists
Here's a working demo
I don't have access to your API endpoint, so for demo purposes, am just using the GitHub API, to fetch and render a list of all repos in the Vue.js organization.
Here's what it looks like:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
name: 'dbzx10299-demo',
data() {
return {
loaded: false,
response: null,
}
},
methods: {
fetchData() {
const demoEndpoint = 'https://api.github.com/orgs/vuejs/repos';
fetch(demoEndpoint)
.then(response => response.json())
.then(data => {
this.response = data;
this.loaded = true;
})
},
},
mounted() {
this.fetchData();
},
})
<script src="https://unpkg.com/vue#2.x/dist/vue.js"></script>
<div id="app">
<div class="hello">
<h2>Vue Repo List - Data fetching example</h2>
<div v-if="!loaded">Loading...</div>
<ul v-else>
<li v-for="(repo, index) in response" :key="index">
<a :href="repo.html_url" :title="repo.description" target="_blank">
{{ repo.name }}
</a>
<i>★ {{ repo.stargazers_count }}</i>
</li>
</ul>
</div>
</div>

How can I get HTML element when importing Vue via CDN?

I'm using Vue 3.0.2 in my project, loaded via CDN as follows:
<head>
...
<script src="https://unpkg.com/vue#3.0.2"></script>
</head>
How can I access an HTML element in my Vue component when importing the library via CDN? I want to do the equivelant of what would be document.getElementById('element') in plain JS.
Related posts on here suggest that calling $this.el.querySelector should work for this, however I don't have access to that function. I suspect due to importing Vue via CDN?
My component looks as follows...
const app = Vue.createApp({
data() {
return {
// data members.
}
},
methods: {
// method definitions.
} });
app.mount('#app')
Thanks for any advice!
Try something like this
<script src="https://unpkg.com/vue#3.0.2"></script>
</head>
<body>
<div id="app">
<h1>{{ data }}</h1>
<input type="text" ref="input">
<button #click="log">LOG</button>
</div>
<script>
const app = Vue.createApp({
data() {
return {
data: 'any'
}
},
methods: {
log() {
console.log(this.$refs.input.value)
}
}
});
app.mount('#app')
</script>

How do I display two components with vue.js?

this is my first post on here! Just need some small help with a problem. Thanks!
I just started learning vue.js and I'm trying to get my second components template to display on the page. For some reason, vue isn't rendering my second component, only the first. I'm thinking something is wrong within the index file with <education :data='data' />. Do I need to create a different <div id='app'> and put the second component within it?
app.js:
// Fetch handler
function fetchData(url) {
return fetch(url)
.then(checkStatus)
.then(res => res.json())
.catch(error => console.log('Error retrieving data', error))
}
// Response error handler
function checkStatus(res) {
if(res.ok) {
return Promise.resolve(res);
} else {
return Promise.reject(new Error(res.statusText));
}
}
(() => {
// Define components.
Vue.component('contact-info', {
props: ['data'],
template: `
<div>
<img v-bind:src='data.photo'>
<h1>{{ data.name }}</h1>
<p>{{ data.email }}</p><p>{{ data.phone }}</p>
<p v-if='data'>{{ data.links[0] }}</p><p v-if='data'>{{ data.links[1] }}</p>
</div>
`
});
Vue.component('education', {
props: ['data'],
template: `
<div>
<h3>Education</h3>
<div>
<h2></h2>
<p></p>
<p></p>
<div>
<div>
`
});
// Instantiate the app.
const app = new Vue({
el: '#app',
data() {
return {
data: ''
}
},
mounted() {
fetchData('https://wip.journeygroup.com/intern-api/joshbryant.json')
.then(jsonData => (this.data = jsonData))
}
});
})();
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Resume • Josh Bryant</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:wght#400;600;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./css/main.css">
</head>
<body>
<div id="app">
<contact-info :data='data' />
<education :data='data' />
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>
<script src="./app/main.js"></script>
</body>
</html>
Your post shows app.js though the HTML looks for main.js, but I'll assume that's just a posting error since you've gotten some of it working.
The reason it doesn't work is because custom HTML components require a closing tag and cannot be self-closing. If you were using Vue CLI this would work anyway because the compiler would fix it, but not in a CDN implementation of Vue. The first component will work but everything that follows will be invalid HTML since the first component isn't closed properly.
This will work:
<div id="app">
<contact-info :data="data"></contact-info>
<education :data="data"></education>
</div>
A couple of other suggestions
As #tao mentioned in comments, it's confusing to call your variable data in the root component, and similarly to call your props data. These won't cause a problem but probably best to rename them all for clarity (so as not to be confused with the component's data option.)
There's an invalid bullet character in the HTML <title> tag which you can replace with &bullet;
It's best to use double quotes for your HTML attributes (the props use single quotes in your code)

How to pass a variable from twig to vueJS (symfony 2.8 and VueJS 2)

I have a symfony 2.8 application and I recently integrated VueJs 2 as my front-end framework, because it gives a lot of flexibility.
My application is not single page and I use the symfony controllers to render views. All the views are wrapped in a base twig layout:
<!DOCTYPE html>
<html lang="{{ app.request.locale|split('_')[0] }}">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
</head>
<body>
<div id="app">
{% block body %} {% endblock %}
</div>
<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
<script src="/js/fos_js_routes.js"></script>
<script type="text/javascript" src="{{ asset('build/vendor-bundle.js') }}"></script>
<script type="text/javascript" src="{{ asset('build/vue-bundle.js') }}"></script>
</body>
</html>
I load most of the JS with webpack, all my vue components and JS dependencies are compiled in vendor-bundle.js and vue-bundle.js. My VueJs instance looks like this:
import './components-dir/component.vue'
import './components-dir/component2.vue'
Vue.component('component', Component);
Vue.component('component2', Component2);
window.onload = function () {
new Vue({
el: '#app',
components: {}
});
};
I want to pass some php variables from the controller to the vuejs componets, but I can't manage to make it work.
A very simple example of a controller looks like this:
/**
* #Route("/contract", name="contract")
* #Method("GET")
*/
public function indexAction()
{
$paymentMethods = PaymentMethod::getChoices();
return $this->render('contracts/index.html.twig', [
'paymentMethods' => $serializer->normalize($paymentMethods, 'json'),
]);
}
All the html, css and js are handled by vueJs. The twig view looks like this:
{% extends 'vue-base.html.twig' %}
{% block body %}
<contracts :paymentMethods="{{paymentMethods | raw}}"></contracts>
{% endblock %}
The contracts.vue component looks like this:
<template>
<div>
<p>Hi from component</p>
</div>
</template>
<script>
export default {
data() {
return {}
},
props: ['paymentMethods'],
mounted: function () {
console.log(this.paymentMethods)
}
}
</script>
<style>
</style>
How can I pass the php variables as props to vueJs ?
In the example above, I don't get any errors, but the property is not passed to vuejs. The console log prints undefined.
I want to be able to do this, because I don't want to have a SPA, but I also want to pass some variables from symfony to vue, because I won't have to make additional requests.
Instead of passing Twig variable as value of Vue attr:
<contracts :payment-methods="{{ twigVar}}"></contracts>
you can render whole using twig:
<contracts {{ ':payment-methods="' ~ twigVar ~ '"' }}></contracts>
Thanks to this you will avoid delimeters conflict between vue and twig.
Also as the value comes directly from twig, it probably wont change upon a time, as it is generated in backend - not in some vue-source - so you don't need to bind it, just pass it like:
<contracts payment-methods="{{ twigVar}}"></contracts>
The simplest way to pass variables from twig to Vue application is:
Twig:
<div id="app" data-foo="{{ foo }}" data-bar="{{ bar }}"></div>
JS:
import Vue from 'vue'
new Vue({
el: '#app',
data: {
foo: '',
bar: ''
},
template: '<div>foo = {{ foo }}</div><div>bar = {{ bar }}</div>',
beforeMount: function() {
this.foo = this.$el.attributes['data-foo'].value
this.bar = this.$el.attributes['data-bar'].value
}
})
If you would like to use a Vue component you can do it the following way:
Twig:
<div id="app" data-foo="{{ foo }}" data-bar="{{ bar }}"></div>
JS:
import Vue from 'vue'
import App from 'App'
new Vue({
el: '#app',
render(h) {
return h(App, {
props: {
foo: this.$el.attributes['data-foo'].value,
bar: this.$el.attributes['data-bar'].value,
}
})
}
})
App.vue:
<template>
<div>foo = {{ foo }}</div>
<div>bar = {{ bar }}</div>
</template>
<script>
export default {
props: ['foo', 'bar'],
}
</script>
Please note if you would like to pass arrays you should convert them to json format before:
Twig:
<div id="app" data-foo="{{ foo|json_encode }}"></div>
and then you should decode json:
JS:
this.foo = JSON.parse(this.$el.attributes['data-foo'].value)
you need to add the following to your .vue file props: ['paymentMethods'] please refer to the following url for complete documentation https://v2.vuejs.org/v2/guide/components.html#Passing-Data-with-Props
Probably late to the party, but if anyone is having the same issue, the problem here was the casing.
CamelCased props like paymentMethods are converted to hyphen-case in html, and can be used like this:
<contracts :payment-methods="{{ paymentMethods | raw }}"></contracts>

How can I bind the html <title> content in vuejs?

I'm trying a demo on vuejs. Now I want the html title to bind a vm field.
The below is what I tried:
index.html
<!DOCTYPE html>
<html id="html">
<head>
<title>{{ hello }}</title>
<script src="lib/requirejs/require.min.js" data-main="app"></script>
</head>
<body>
{{ hello }}
<input v-model="hello" title="hello" />
</body>
</html>
app.js
define([
'jquery', 'vue'
], function ($, Vue) {
var vm = new Vue({
el: 'html',
data: {
hello: 'Hello world'
}
});
});
But the title seemed not bounded, how to make it work?
There are essentially two ways to solve it.
Use an existing Package
For example, vue-meta:
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
metaInfo: {
// if no subcomponents specify a metaInfo.title, this title will be used
title: 'Default Title',
// all titles will be injected into this template
titleTemplate: '%s | My Awesome Webapp'
}
}
</script>
Create your own Component
Create a vue file containing:
<script>
export default {
name: 'vue-title',
props: ['title'],
watch: {
title: {
immediate: true,
handler() {
document.title = this.title;
}
}
},
render () {
},
}
</script>
Register the component using
import titleComponent from './title.component.vue';
Vue.component('vue-title', titleComponent);
Then you can use it in your templates, e.g.
<vue-title title="Static Title"></vue-title>
<vue-title :title="dynamic.something + ' - Static'"></vue-title>
You can do it with 1 line in the App.vue file, like this:
<script>
export default {
name: 'app',
created () {
document.title = "Look Ma!";
}
}
</script>
Or change the <title> tag content in public/index.html
<!DOCTYPE html>
<html>
<head>
<title>Look Ma!</title> <!- ------ Here ->
</head>
...
This answer is for vue 1.x
using requirejs.
define([
'https://cdn.jsdelivr.net/vue/latest/vue.js'
], function(Vue) {
var vm = new Vue({
el: 'html',
data: {
hello: 'Hello world'
}
});
});
<!DOCTYPE html>
<html id="html">
<head>
<title>{{ hello }}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.js" data-main="app"></script>
</head>
<body>
{{ hello }}
<input v-model="hello" title="hello" />
</body>
</html>
you can do it like this using the ready function to set the initial value and watch to update when the data changes.
<html>
<head>
<title>Replace Me</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/vue/latest/vue.js"></script>
<div id="app">
<input v-model="title">
</div>
<script>
new Vue({
el: '#app',
ready: function () {
document.title = this.title
},
data: {
title: 'My Title'
},
watch: {
title: function (val, old) {
document.title = val
}
}
})
</script>
</body>
</html>
also i tried this based on your original code and it works
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/vue/latest/vue.js"></script>
<div id="app">
<input v-model="title">
</div>
<script>
new Vue({
el: 'html',
data: {
title: 'My Title'
}
})
</script>
</body>
</html>
Just to chime in here. I have read that VueJS wants nothing to do with the meta stuff so I would do such things outside of the "VueJS" realm.
Basically make a plain vanilla js service like below. Here you could add all the functions to handle the meta data stuff such as the Open Graph data.
meta.js
export setTitle(title) {
document.title = title
}
Now we can import the service in main and then provide it to any component in the app who wants it. I could even use my meta service in other projects too which use different frameworks like React or Angular. Portability is super cool!
main.js
import meta from './meta'
new Vue({
router,
render: h => h(App),
provide: {
meta: meta
}
}).$mount('#app')
Here the component injects the meta service it wants to use.
someView.vue
export default {
name: 'someView',
inject: ['meta'],
data: function() {
returns {
title: 'Cool title'
}
},
created: function() {
this.meta.setTitle(this.title);
}
}
This way the meta service is decoupled from the app because different parent components can provide different versions of the meta service. Now you can implement various strategies to see which one is right for you or even different strategies per component.
Basically the inject walks up the component hierarchy and takes the meta service from the first parent who provides it. As long as the meta service follows a proper interface, you're golden.
Decoupling with DI is super cool 😃
Title and meta tags can be edited and updated asynchronously.
You can use state management, create a store for SEO using vuex and update each part accordingly.
Or you can update the element by yourself easily
created: function() {
ajax().then(function(data){
document.title = data.title
document.head.querySelector('meta[name=description]').content = data.description
})
}
If you are using Vuex and want <title> to be part of your application state, then:
create a pageTitle state variable in Vuex
map the state to the template using mapState()
watch it in template, probably add immediate: true to trigger the watcher right away
in watcher, document.title = pageTitle
This will allow you to manage title with Vuex and keep them in sync. I found it useful for SPAs.
By doing this you don't have to mess with your original HTML template, as most of the time Vue root template resides inside <body>.
This is for Vue 2.x.
router.beforeEach((to, from, next) => {
let mohican = to.path; if (mohican == '/') mohican = 'Home'
document.title = mohican.replace('/','');
next();
return;
});
I have an application toolbar component which is common for all pages of my SPA website and is nested in App.vue. In every page I update my common toolbar title in the created hook of the page using Vuex store:
//in every page.vue
created() {
this.$store.commit('toolBar', { pageTitle: this.pageTitle, ... })
},
To automatically update the website title (along with the toolbar title) I use this mutation in the store:
//store.js
toolBar(state,val){
document.title = val.pageTitle
state.toolBar = val
},
Similarly, I use the same mechanism to update e.g. SEO metadata
just pass
:title="data.name"

Categories