How can I get JS access to .content of nested <template>?
I am trying to extend <template> with my imported-template element (which fetches template content from external file) and I would like to implement <imported-content> in similar manner to native <content>. To do so, I simply try
this.content.querySelector("imported-content")
but it occurred, that for nested template this.content is empty.
<script>
(function() {
var XHTMLPrototype = Object.create((HTMLTemplateElement || HTMLElement).prototype);
XHTMLPrototype.attachedCallback = function() {
//..
var distributeHere = this.content.querySelector("imported-content");
var importedContent = document.createElement("span");
importedContent.innerHTML = "Imported content";
distributeHere.parentNode.replaceChild(importedContent, distributeHere);
}
document.register('imported-template', {
prototype: XHTMLPrototype,
extends: "template"
});
})();
</script>
<template id="fails" bind>
<ul>
<template is="imported-template" bind="{{ appdata }}">
<li>
<imported-content></imported-content>
</li>
</template>
</ul>
</template>
JSFiddle here
I am not sure if it is a bug, a design issue, or just template shim limitation.
I thought that maybe I am checking it in wrong life-cycle callback, so I tried MutationObserver fiddle here, but mutation does not occur as well.
I changed your selector to this.content.querySelector("[is='imported-content']"). Is this what your trying to do?
Related
I have a use case (below) where I need to mount (if thats the correct term) a Vue.js component template that was inserted into the DOM via jQuery, I can setup a Mutation Observer or react to certain events that are triggered when the mutation happens.
I am using Vue.js v2
Here is a simple example I put together to illustrate the point:
live jsFiddle https://jsfiddle.net/w7q7b1bh/2/
The HTML below contains inlined-templates for two components
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<div id="app">
<!-- The use of inline-template is required for my solution to work -->
<simple-counter inline-template>
<button v-bind:style="style" v-on:click="add">clicks: {{ counter }}</button>
</simple-counter>
<simple-counter inline-template>
<button v-on:click="counter += 1">{{ counter }}</button>
</simple-counter>
</div>
<button id="mutate">Mutate</button>
The js:
// simple counter component
Vue.component('simple-counter', {
data: function() {
return {
counter: 0,
style: {
color: 'red',
width: '200px'
}
}
},
methods: {
add: function() {
this.counter = this.counter + 1;
this.style.color = this.style.color == 'red' ? 'green' : 'red';
}
}
})
// create the Vue instance
var initV = () => new Vue({
el: '#app'
});
// expose the instance for later use
window.v = initV();
// click handler that will add a new `simple-counter` template to the Vue.el scope
$('#mutate').click(function(){
$('#app').append(` <div is="simple-counter" inline-template>
<button v-bind:style="style" v-on:click="add">click to add: <span class="inactive" v-bind:class="{ active: true }">{{ counter }}</span></button></div>`)
// do something after the template is incerted
window.v.$destroy()
window.v = initV(); // does not work
})
As mentioned in the code, destroying the re-instantiating the Vue instance does not work, I understand why, the templates for the components are changed on first Vue instantiation to their final HTML, when you try and instantiate a second time, templates are not there, components are not mounted
I'd like to be able to find the newly added components after mutation and mount only those, is that possible? and how?
UPDATE:
I was able to find a way to do it via instantiating a new Vue instance with el set to the specific mutated part of the DOM as opposed to the whole #app tree:
$('#mutate').click(function(){
var appended =
$(`
<div is="simple-counter" inline-template>
<button v-bind:style="style" v-on:click="add">
click to add: {{ counter }}
</button>
</div>`
).appendTo($('#app'));
var newV = new Vue({el: appended[0]});
});
Seems to work, but also looks ugly and I am not sure what other implications this might have..
Use Case:
I am working on a way to write Vue.js components for a CMS called Adobe Experience Manager (AEM).
I write my components using inlined-template which gives me the advantage of SEO as well as server-side rendering using another templating language called HTL.
The way AEM authoring works is that, when a component is edited (via a dialog), that specific component is re-rendered on the server-side then injected back to the DOM to replace the old component, all done via Ajax and jQuery (no browser refresh).
Here is an example
AEM component template:
<button>${properties.buttonTitle}</button>
Here is what an author might do:
author visits the authoring page
opens the button component dialog to edit
changes the buttonTitle to "new button title"
Saves
upon saving, an ajax is sent, the component HTML is re-rendered on the server and returned is the new HTML. That HTML now replaces the old HTML via jQuery (mutates the DOM)
This is fine for static components, but if this was a Vue.js component, how do I dynamically mount it while keeping other components mounted.
An easy solution to this is to refresh the page... but that is just bad experience... There has to be a better way.
Thanks to #liam I was able to find an appropriate solution to my problem
After mutating the DOM with the HTML template, keep a reference to that template's parent element
for example:
var $template = $('<div is="simple-counter" inline-template> ..rest of template here.. <div>').appendTo('#app') // app is the Vue instance el or a child of it
Now you can create a new instance of your component and add $template to it as the el property
if my component was:
var simpleCounterComponent = Vue.component('simple-counter', {
data: function() {
return {
counter: 0,
style: {
color: 'red',
width: '200px'
}
}
},
methods: {
add: function() {
this.counter = this.counter + 1;
this.style.color = this.style.color == 'red' ? 'green' : 'red';
}
}
})
I can do:
var instance = new simpleCounterComponent({
el: $template.get(0) // getting an HTML element not a jQuery object
});
And this way, that newly added template has become a Vue component
Take a look at this fiddle for working example based on the question:
https://jsfiddle.net/947ojvnw/11/
One way to instantiate Vue components in runtime-generated HTML is:
var ComponentClass = Vue.extend({
template: '...',
});
var instance = new ComponentClass({
propsData: { name: value },
});
instance.$mount('#uid'); // HTML contains <... id="uid">
...
instance.$destroy(); // if HTML containing id="uid" is dropped
More here (I am not affiliated with this site)
https://css-tricks.com/creating-vue-js-component-instances-programmatically/
Is there any way of detecting when the content of a component changes?
For example:
HTML:
<component>
<div>{{name}}</div>
</component>
JS:
component = Ractive.extend({
on...: function () {
// invoked when the inner HTML changes
}
});
I use {{yield}} so the content is rendered in the context of the parent.
For now I'll have to pass name to the component just for the purpose of observing changes (even though I don't need the value in the context of the component). (Or I'll add a function that I can call).
<component changes="{{name}}">
<div>{{name}}</div>
</component>
Any ideas?
It's possible, but a bit hacky.
http://plnkr.co/edit/YoTZpyTTyCyXijEteGkg?p=preview
<comp>
{{name}} hi {{thing}}
</comp>
comp = Ractive.extend({
template: '<div>{{yield}}</div>',
oncomplete: function() {
var self = this;
self.partials.content.forEach(function(partial) {
if (partial.r) {
self.parent.observe(partial.r, function(newValue) {
console.log(partial.r + ' changed to ', newValue)
}, {init: false});
}
});
}
})
Yield/Content are really just special partials, so this will loop through the items in that partial and set up an observer for each keypath.
This demo only works with simple expressions like {{foo}}. If you have more complicated things inside the partial you'll have to inspect the rendered partial to figure out what keypath you want to observe the parent on.
I have parent template included with child template. I need to use parents ReactiveVar from child template. I can use Session method but for my requirement Session method doesn't works. How do I access ReactiveVar value from parent templates?
HTML:
<template name="ParentTemplate">
{{> ChildTemplate}}
</template>
<template name="ChildTemplate">
//Some HTML content
</template>
JS
Template.ParentTemplate.onCreated(function () {
this.myvalue = new ReactiveVar(5); //I tried this.data.myvalue but doesnt works
});
Template.ChildTemplate.helpers({
'myhelper' : function(){
return Template.parentData(1).myvalue.get();
}
});
Here's an example were the child is a direct descendant of the parent:
<template name="parent">
{{> child}}
</template>
<template name="child">
<p>{{parentValue}}</p>
</template>
In this case we can access the parent's instance variable like this:
Template.child.helpers({
parentValue: function() {
var parentView = Blaze.currentView.parentView.parentView;
var parentInstance = parentView.templateInstance();
// replace parentVariable with the name of the instance variable
return parentInstance.parentVariable.get();
}
});
If the two templates have a more complex relationship in the DOM, you can use something like this:
// replace .parent-class will the selector for your parent template
el = $('.parent-class')[0]
var parentInstance = Blaze.getView(el).templateInstance();
// replace parentVariable with the name of the instance variable
return templateInstance.parentVariable.get();
Another possible solution could be to pass the data to the child explicitly.
// js
if (Meteor.isClient) {
Template.parent.onCreated(function () {
this.reactiveV = new ReactiveVar(42);
});
Template.parent.helpers({
getReactiveVar: function() {
return Template.instance().reactiveV;
},
});
Template.parent.events({
'click button': function(e, tmp) {
tmp.reactiveV.set(tmp.reactiveV.get() + 2);
},
});
}
and in the template file:
<template name="parent">
<p>this is the parent!</p>
<button> var++ </button>
{{> child parentData=getReactiveVar}}
</template>
<template name="child">
<h3>
child template
</h3>
{{parentData.get}}
</template>
as you press the button you will see the child template update. If you needed to, you could also assign the parent data in some other way in the Template.child.onCreated function.
This might provide loser coupling between the two templates.
I'm trying to bind a method to an on-tap attribute of a paper-button. After much testing, I've found that I can only bind a (for lack of a better word) top-level function, and not a method of an object in the template.
For example, I have a template, to which I have bound a number of objects, one of which is a user object. Object user has a bunch of methods and variables, like 'isNew' or 'reputation'. The user object also has a method 'addReputation'
I can use the object variables like this :
<template if = '{{user.new}}'><h1>{{user.name}}</h1></template>
And I can bind button taps like this:
<paper-button on-tap='{{addReputation}}'>Add Rep</paper-button>
But not like this:
<paper-button on-tap='{{user.addReputation}}'>Add Rep</paper-button>
Does anyone know why this may be?
if you set the method to a handler on your element's prototype it works. That way you can still keep things dynamic:
<script src="http://www.polymer-project.org/webcomponents.min.js"></script>
<script src="http://www.polymer-project.org/polymer.js"></script>
<polymer-element name="my-element" on-tap="{{tapHandler}}">
<template>
<style>
:host {
display: block;
}
</style>
click me
<content></content>
</template>
<script>
Polymer({
created: function() {
this.user = {
method: function() {
alert('hi');
}
};
this.tapHandler = this.user.method;
}
});
</script>
</polymer-element>
<my-element></my-element>
i'm sharing my plunk to resolve above problem. plunk link
In the template
<button on-tap="{{fncall}}" data-fnname="b">b call</button>
In the script
x.fncall = function(e) {
var target = e.target;
var fnName = target.getAttribute("data-fnname");
return x.datamodel[fnName]();
}
Polymer(x);
I am quite new with Meteor but have really been enjoying it and this is my first reactive app that I am building.
I would like to know a way that I can remove the .main element when the user clicks or maybe a better way would be to remove the existing template (with main content) and then replace with another meteor template? Something like this would be simple and straightforward in html/js app (user clicks-> remove el from dom) but here it is not all that clear.
I am just looking to learn and for some insight on best practice.
//gallery.html
<template name="gallery">
<div class="main">First run info.... Only on first visit should user see this info.</div>
<div id="gallery">
<img src="{{selectedPhoto.url}}">
</div>
</template>
//gallery.js
firstRun = true;
Template.gallery.events({
'click .main' : function(){
$(".main").fadeOut();
firstRun = false;
}
})
if (Meteor.isClient) {
function showSelectedPhoto(photo){
var container = $('#gallery');
container.fadeOut(1000, function(){
Session.set('selectedPhoto', photo);
Template.gallery.rendered = function(){
var $gallery = $(this.lastNode);
if(!firstRun){
$(".main").css({display:"none"});
console.log("not");
}
setTimeout(function(){
$gallery.fadeIn(1000);
}, 1000)
}
});
}
Deps.autorun(function(){
selectedPhoto = Photos.findOne({active : true});
showSelectedPhoto(selectedPhoto);
});
Meteor.setInterval(function(){
selectedPhoto = Session.get('selectedPhoto');
//some selections happen here for getting photos.
Photos.update({_id: selectedPhoto._id}, { $set: { active: false } });
Photos.update({_id: newPhoto._id}, { $set: { active: true } });
}, 10000 );
}
If you want to hide or show an element conditionaly you should use the reactive behavior of Meteor: Add a condition to your template:
<template name="gallery">
{{#if isFirstRun}}
<div class="main">First run info.... Only on first visit should user see this info.</div>
{{/if}}
<div id="gallery">
<img src="{{selectedPhoto.url}}">
</div>
</template>
then add a helper to your template:
Template.gallery.isFirstRun = function(){
// because the Session variable will most probably be undefined the first time
return !Session.get("hasRun");
}
and change the action on click:
Template.gallery.events({
'click .main' : function(){
$(".main").fadeOut();
Session.set("hasRun", true);
}
})
you still get to fade out the element but then instead of hiding it or removing it and having it come back on the next render you ensure that it will never come back.
the render is triggered by changing the Sessionvariable, which is reactive.
I think using conditional templates is a better approach,
{{#if firstRun }}
<div class="main">First run info.... Only on first visit should user see this info.</div>
{{else}}
gallery ...
{{/if}}
You'll have to make firstRun a session variable, so that it'll trigger DOM updates.
Meteor is reactive. You don't need to write the logic for redrawing the DOM when the data changes. Just write the code that when X button is clicked, Y is removed from the database. That's it; you don't need to trouble yourself with any interface/DOM changes or template removal/redrawing or any of that. Whenever the data that underpins a template changes, Meteor automatically rerenders the template with the updated data. This is Meteor’s core feature.