How to dynamically compile a component added to a template in VueJS? - javascript

I am building a blog with VueJS 2. Most of my articles are stored as Markdown files, but I want to me able to cover some more advanced topics, using features that Markdown doesn't cover. I am considering making these special posts VueJS components that would be used in a template as <article-name>, or <special-article article-title="{{articleTitle}}">. Pretty simple.
I have the component loaded already, so all I need to do is compile the template string into a real template. I might be thinking too much with my AngularJS background rather than with Vue.
I can't find any solid direction for dynamically adding a component to a template in VueJS.

You can compile a template with Vue.compile. Just be aware it's not available in all builds. That's covered in the documentation.
Getting the data associated with it is a little more work.
console.clear()
const articles = [
{
title: "Testing",
articleTemplate: "<article-title></article-title>"
},
{
title: "Testing 2",
articleTemplate: "<special-article :article-title='title'></special-article>"
},
]
Vue.component("article-title",{
template: `<span>Article Title</span>`
})
Vue.component("special-article", {
props:["articleTitle"],
template: `
<div>
<h1>{{articleTitle}}</h1>
<p>Some article text</p>
</div>
`
})
new Vue({
el: "#app",
data:{
articles
},
computed:{
compiledArticles() {
return this.articles.map(a => {
// compile the template
let template = Vue.compile(a.articleTemplate)
// build a component definition object using the compile template.
// What the data function returns is up to you depending on where
// the data comes from.
return Object.assign({}, template, {data(){return a}})
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.min.js"></script>
<div id="app">
<component v-for="article in compiledArticles" :is="article"></component>
</div>

VueJS has a built-in component for this scenario:
<component is="article-component-name"></component>

Related

Using the Vue "v-slot" feature for a named slot in a javascript object

I come here because I can't find how to use "v-slot" when using the syntax leveraging a javascript object. This syntax I am talking about is the one presented in the course course Intro to Vue3 I followed (If this syntax has a particular name, apologize for my explanation: I would be happy to know it).
Below screenshot shows an example:
I have a first file named ComponentToBeCalled.js, containing the following code for building a vue component:
app.component('component-to-be-called', {
data: function(){},
props: {},
template:
/*html*/
`<div>
SOME_HTML_HERE
</div>`,
computed: {},
methods: {}
})
I have a second file named ParentComponent.js with the following code for building another vue component:
app.component('parent-component', {
data: function(){},
props: {},
template:
/*html*/
`<div>
<slot name="SLOT_NAME"></slot>
</div>`,
computed: {},
methods: {}
})
--> My objective would be to build 'component-to-be-called' within 'parent-component'.
From what I read, the classic syntax for defining a named slot in a CompenentToBeCalled.vue file would be:
<component-to-be-called>
<template v-slot:SLOT_NAME>
<div>
SOME_HTML_HERE
</div>
</template>
</component-to-be-called>
Do you know what is the equivalent when we play with a javascript object ?
__
Your first bit of code is defining a regular component (component-to-be-called). Your second bit of code is defining a component which has a slot (parent-component).
If you want to have a component-to-be-called inside a parent-component using the slot, you would do as follows:
<parent-component>
<template v-slot:SLOT_NAME>
<component-to-be-called />
</template>
</parent-component>
This would render as:
<div> //This is from parent-component
<div> //This is from component-to-be-called
SOME_HTML_HERE
</div>
</div>
The official docs has some nice diagrams to help you get your head around what goes where. Be careful though that what the docs calls parent template is the template that's using the Component with the defined slot.
This should all work the same with plain javascript syntax (template: "<div>...) or Single File Components.

Embed Vue component within Shopify store

Within a product page, I am trying to display a custom Vue component. For brevity, the component displays some information from a Firebase database based on the given product id.
I originally tried to make this a Shopify app so I could access their APIs. I implemented OAuth and can retrieve the required information. However, actually including the component within the store has been unsuccessful.
What is the best way of including Vue inside Shopify?
I have tried including the script files directly inside the template files, inside snippets, and included them within the global scripts tag. But nothing I have tried has been able to render even a simple component.
Inside product.liquid:
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
<div id="app">
{{ message }}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
</script>
Inside Vue developer tools the component appears inside the DOM but the "Hello Vue!" message does not appear as it should.
There are no other errors in the console. Which is most puzzling.
Any insight into the proper way of including Vue into Shopify would be greatly appreciated.
Liquid files will by default parse {{ }} tags. So you need to change your templating mechanism. Below is updated code which works in Shopify Liquid files -
<div id="app">
${ message }
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
delimiters: ['${', '}']
})
</script>
Basically, i have added delimeters which vue will check to find templates and they are different from Shopify Parsing which will result in shopify not parsing those holders. You can read up more about vue delimeters here - Link

How can I make a third party Vue library have access to other third-party Vue plugins being used?

Let's say I'm using vuex, vue-i18n, and Syncfusion ej2-vue-grids in a project.
My app will have access to $store and $t, but if I make a grid and define a custom template for a column, the component that gets rendered in that field does not have access to vuex or vue-i18n.
I've been able to get around this for vuex by setting Vue.prototype.$store = store; when first setting up the Vue app. vue-i18n has several additional properties, though, and setting everything on the prototype feels like a hack.
I assume Syncfusion must be calling Vue.extend when creating the components for the grid column, so the component is losing all context from the app. Is this a bug on their side, or is there something I should be doing differently?
EDIT
Here is a plnkr with an example of the behavior I'm seeing.
https://plnkr.co/edit/XwVC6yNQaI2vQIUoWfSm?p=preview
When you first view it, the Freight column should be empty (because it can't access the store) and below the grid should be a line of !!!!!!!! (because it can access the store).
If you uncomment line 15 in index.js, both of line of !!!!!! below the grid and the contents of the Freight column should be visible.
We need to pass the store and vue-i18n reference to the template declaration.AS in below code snippet.
<pre>
<code>
tmpl() {
return {
template: {
store,
i18n,
data() {
return {
data: {}
}
},
computed: {
test() {
return this.$store.state.test;
}`enter code here`
},
template: `<div>
<div> Translated text child: {{ $t("message.hello") }}</div>
<span>{{ test }} {{ data.Freight }} {{ test }}</span> </div>`
}
}
}
</code>
</pre>
Plunker sample with updated column template using store: https://plnkr.co/edit/Sei9F1MEvNyLGseZEzM1?p=preview

Enhance parts of multi page application with react or vue

I'm developing a enterprise application with java & hibernate & spring mvc in the server side and using jquery in the client side (not a SPA).
Now in the search page i use ajax and get only json response, but i don't want to write something like this below in every search or pagination request.
function(ajaxData) {
....
$('#search').html('' +
'<div class="search-container">' +
'<div class="search-item">' +
'<div class="title-item">'+ajaxData.title+'</div>' +
...
...
'</div>' +
'</div>'
)
....
}
I think it's easy to use jsx with a react or vue component just in this page to refresh the results.
I want also reuse some html blocks and i think it will be easy with react or vue
I used to build a little SPA project and it's all about npm and webpack and bundling, but i really don't want to use them since i have a multi page application and it's very suitable for my project.
I think the same thing facebook is doing, they use react but facebook is not a SPA.
How can i achieve this hybrid approach ?
I had done a similar kind of stuff in the past. I injected a small react component into the DOM.
Here is how I did it:
Create a React component in JSX, let's call it Demo:
export class Demo extends React.Component {
render() {
return <h1>This is a dummy component.</h1>
}
}
Now use the renderToStaticMarkup function to get the static html.
const staticMarkup = renderToStaticMarkup(<Demo PASS_YOUR_PROPS/>);
You have the HTML, now you can insert this markup at the desired location using the innerHTML.
Apologies if I misunderstood your question.
UPDATE
We could also use the render() for this purpose. Example:
document.getElementById("button").onclick = () => {
render(
<Demo PASS_YOUR_PROPS/>,
document.getElementById("root")
);
};
Working solution with render() and renderToStaticMarkup: https://codesandbox.io/s/w061xx0n38
render()
Render a ReactElement into the DOM in the supplied container and
return a reference to the component.
If the ReactElement was previously rendered into the container, this
will perform an update on it and only mutate the DOM as necessary to
reflect the latest React component.
renderToStaticMarkup()
This doesn't create extra DOM attributes such as data-react-id, that
React uses internally. This is useful if you want to use React as a
simple static page generator, as stripping away the extra attributes
can save lots of bytes.
Since you have an existing multi page application without a build step (that is, without webpack/babel), I believe one very simple way of achieving what you want is using Vue.js.
You can define a template and update only the data.
Here's a demo of how you would do the code you showed in the question:
new Vue({
el: '#app',
data: {
ajaxDataAvailable: false,
ajaxData: {
title: '',
results: []
}
},
methods: {
fetchUsers() {
this.ajaxDataAvailable = false; // hide user list
$.getJSON("https://jsonplaceholder.typicode.com/users", (data) => {
this.ajaxData.title = 'These are our Users at ' + new Date().toISOString();
this.ajaxData.results = data;
this.ajaxDataAvailable = true; // allow users to be displayed
});
}
}
})
/* CSS just for demo, does not affect functionality could be existing CSS */
.search-container { border: 2px solid black; padding: 5px; }
.title-item { background: gray; font-weight: bold; font-size: x-large; }
.result-item { border: 1px solid gray; padding: 3px; }
<script src="https://unpkg.com/vue"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<div id="app">
<button #click="fetchUsers">Click to fetch the Users</button><br><br>
<div class="search-container" v-if="ajaxDataAvailable">
<div class="search-item">
<div class="title-item"> {{ ajaxData.title }}</div>
<div class="result-item" v-for="result in ajaxData.results">
Name: {{ result.name }} - Phone: {{ result.phone }} - Edit name: <input v-model="result.name">
</div>
</div>
</div>
</div>
In this example, we have:
A method fetchUsers that will perform the ajax call;
The fetchUsers method is bound to the click event of the <button> via #click="methodName" (which is a shorthand to v-on:click="methodName").
A v-if (v-if="ajaxDataAvailable") that makes the .search-container div hidden until the ajaxDataAvailable property is true.
The rendering of some data in the template using interpolation: {{ ajaxData.title }}, note that this picks the value from the objects declared in the data: part of the Vue instance (the new Vue({... code) below and is automatically updated whenever ajaxData.title changes (what happens inside the fetchUsers method when the Ajax call completes.
The rendering of a list using v-for: v-for="result in ajaxData.results". This iterates in the ajaxData.results array, that is too updated inside the fetchUsers method.
The use of an <input> element with the v-model directive, which allows us to edit the result.name value directly (which also updates the template automatically).
There's much more to Vue, this is just an example. If needed, more elaborated demos can be made.
As far as integrating into an existing application, you could paste this very code into any HTML page and it would be already working, no need for webpack/babel whatsoever.
So, there are two things you could do to re-use your code:
I would recommend React for sharing code as components. This page from Official docs explains how to use react with jquery.
Additional resources for integrating react and jquery jquery-ui with react, Using react and jquery together, react-training
Or, use some template engine so you do not need to go through the trouble of integrating a new library and making it work alongside jquery. This SO answer provides lot of options for doing this.
Unless your planning on migrating your jquery app to react in the long run, I would not recommend using react just for one page. It would be easier to go with the template engine route.
From the looks of the requirement, you just need:
Dynamic, data driven HTML blocks
Which need to be reusable
In this case, since we don't need state, having an entire framework like react/vue/angular may be overkill.
My recommendation would be to go for a templating engine like jQuery Templates Plugin or Handlebars
You can even store the HTML blocks as separate reusable modules which you can invoke as needed across your pages.
Sample using Handlebars:
var tmpl = Handlebars.compile(document.getElementById("comment-template").innerHTML);
function simulatedAjax(cb){
setTimeout(function(){
cb(null, {title:'First Comment', body: 'This is the first comment. Be sure to add yours too'});
},2000);
}
simulatedAjax(function(err, data){
if(err){
// Handle error
return;
}
document.getElementById('target').innerHTML = tmpl(data);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js"></script>
<div id="target">
Loading...
</div>
<script id="comment-template" type="text/x-handlebars-template">
<div class="comment">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>
</script>
NOTE:
One disadvantage of using this approach (as opposed to a framework like react/vue/angular) is that you will need to handle event binding and unbinding manually. You can mitigate this to some extent by wrapping your templates in a function which mounts and unmounts it automatically.
// Single reuable JS file for each component
(function(namespace) {
var _tmpl = Handlebars.compile('<div class="comment"><h1>{{title}}</h1><p>{{body}}</p></div>');
function titleClickHandler(ev) {
alert(ev.target.innerHTML)
}
function CommentWidget(target, data) {
var self = this;
self.data = data;
self.target = document.querySelector(target);
}
CommentWidget.prototype.render = function() {
var self = this;
self.target.innerHTML = _tmpl(self.data);
// Register Event Listeners here
self.target.querySelector('h1').addEventListener('click', titleClickHandler)
}
CommentWidget.prototype.unmount = function() {
var self = this;
// Unregister Event Listeners here
self.target.querySelector('h1').removeEventListener('click', titleClickHandler);
self.target.innerHTML = '';
}
window._widgets.CommentWidget = CommentWidget;
})(window._widgets = window._widgets || {})
// Usage on your page
var w = new _widgets.CommentWidget('#target', {
title: 'Comment title',
body: 'Comment body is remarkably unimaginative'
});
// Render the widget and automatically bind listeners
w.render();
// Unmount and perform clean up at a later time if needed
//w.unmount();
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js"></script>
<div id="target"></div>

Using handlebars or markdown to output a model property

I have a model which defines a property with either markdown or html content.
I am wondering whether using a markdown JS library to output the info or use handlebars to generate the html output inside the view.
Any recommendations, examples will be appreciated.
Using a Markdown converter worked for me.
Here is my view code:
App.ActivityDetailsView = Em.View.extend(
templateName : 'activity-details',
classNames : ['details rounded shadow'],
rawDescriptionBinding: 'App.activityDetailsController.description',
description: (->
converter = new Markdown.Converter().makeHtml
return converter(#rawDescription)
).property('rawDescription')
)
Here is the template code (note the triple handlebars {{{}}} for raw html):
<script type="text/x-handlebars" data-template-name="activity-details">
{{{description}}}
</script>
Here is a link to more details and the showdown.js script
Ember recommends that you use your controller to decorate your model. Given this model, we want to render each of these blog posts using the appropriate rendering engine:
[
{ id: 1, isMD: false, md_or_html: "<p>This is HTML.</p>" },
{ id: 2, isMD: true, md_or_html: "*This is MD.*" }
]
You'll start by creating a route which returns that model:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [
{ id: 1, isMD: false, md_or_html: "<p>This is HTML.</p>" },
{ id: 2, isMD: true, md_or_html: "*This is MD.*" }
];
}
});
Just having the model returned doesn't mean that things get rendered. You also need to make sure the template for the index route attempts to put something on the page:
<script type="text/x-handlebars" data-template-name="index">
<ul>
{{#each}}
<li>{{output}}</li>
{{/each}}
</ul>
</script>
You'll note that we haven't yet created an output property, though we've included it in our template. We need to decorate our model to add the processed HTML or Markdown output:
App.IndexController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
output: function() {
var result;
if (this.get('isMD')) {
var converter = new Markdown.Converter();
result = converter.makeHtml(this.get('md_or_html'));
} else {
result = this.get('md_or_html');
}
/*
IMPORTANT!!! Ember automatically escapes HTML upon insertion.
To actually embed the result as HTML you will need tell Ember
that the value is safe to embed as HTML.
DO NOT RETURN SafeStrings UNLESS THE VALUE IS TRUSTED AND SANITIZED!
*/
return new Handlebars.SafeString(result);
}.property('isMD', 'md_or_html')
});
We can't just add the output property to PostController and have everything work without telling IndexController to use PostController for each item in the model. This is accomplished by setting itemController on IndexController (think: "what controller to use for each item"). This allows us to decorate each blog post individually with the output property. We use a computed property to tell Ember that the value of output is dependent upon whether or not the post isMD and the body of the post. If either changes we want Ember to re-render the output.
The complete example includes additional comments and details about how to extend the pattern for introspection into the post body to determine if it is HTML or MD.
I encountered a similar case that I handled with a dynamically inserted handlebars template: I have a field containing a template which may have content bound to application values.
Ember.View.create({
tagName: 'span',
classNames: ['dynamic-content'],
template: Ember.Handlebars.compile(App.preCompileTemplate(template)),
context: someContextObject
});
The App.preCompileTemplate function replaces bindings with valid handlebars expressions, but you could also imagine using Markdown here:
App.preCompileTemplate = function(template) {
return template.replace /{(.*?)}/g, '{{context.$1}}'
}
Using the context object scopes the values that you bind into the template.

Categories

Resources