Aurelia - when are child compositions done? - javascript

Using the Aurelia JS framework, I need to be able to detect when the results of a repeat.for over elements have been loaded completely into the DOM by code in the parent VM. I know I can inject a listener into a #bindable and trigger the listener on attached(), but that seems very hokey to me.
Any ideas?

The problem is that you are pushing items inside the attached method and you are using dynamic composition, which usually is not a problem but your situation is a bit different. Queuing a micro-task doesn't help because it runs before your view (child.html) is loaded.
There are couple ways to solve the this problem:
1° Solution
Push your items inside bind():
bind() {
this.items.push({
view: 'child.html',
viewModel: new Child('A')
});
this.items.push({
view: 'child.html',
viewModel: new Child('B')
});
}
attached() {
let container = document.getElementById('container');
console.log('Height after adding items: ' + container.clientHeight);
this._taskQueue.queueMicroTask(() => {
console.log('Height after queued microtask: ' + container.clientHeight);
});
setTimeout(() => {
console.log('Height after waiting a second: ' + container.clientHeight);
}, 1000);
}
Fix the HTML:
<template>
<div id="container">
<div repeat.for="item of items">
<compose view-model.bind="item.viewModel" view.bind="item.view"></compose>
</div>
</div>
<div style='margin-top: 20px'>
<div repeat.for="log of logs">
${log}
</div>
</div>
</template>
Running example https://gist.run/?id=7f420ed45142908ca5713294836b2d5e
2º Solution
Do not use <compose>. Compose was made to be used when dynamic compositions are necessary. Are you sure you need to use <compose>?.
<template>
<require from="./child"></require>
<div id="container">
<div repeat.for="item of items">
<child>${item.data}</child>
</div>
</div>
</template>

Related

How and when to bind a function to a dynamic html content in Angular?

I have a component where I have to bind a function to every div which are dynamic. I have used UIkit sortable component group, where on drag and drop to a specific div it executes the specific code bound to it.
For example: I have a div
<div id="abc"></div>
and we have to bind a function to it like this:
UIkit.util.on("abc", "added", (item)=>{
//Do something
})
In my case I have dynamic contents. The div are rendered using *ngFor
and functions are bounded to them by using .map function.
To bind the function the html content should be loaded to the DOM. But the function is called before the DOM gets loaded with the dynamic content. So the functions are not getting bound.
I called the function in ngAfterViewInit still its not helping.
The following is my code:
component.html
<div *ngFor="let batch of batches">
<div [attr.id]="batch.batchId" uk-sortable="group: sortable-group">
<div *ngFor="let stud of batch.students">
<div [attr.id]="stud.id" class="uk-margin">
<div class="uk-card uk-card-default uk-card-body uk-card-small">Item 1</div>
</div>
</div>
</div>
</div>
component.ts
ngOnInit=()=>{
this.getBatch().then(batch=>{
this.batches = batch
});
}
ngAfterViewInit=()=>{
this.initializeDragChange();
}
initializeDragChange = () => {
this.batches.map((item) => {
UIkit.util.on(`${item.batchId}`, "added", (item) => {
//do something
});
});
};

How to fire function after v-model change?

I have input which I use to filter my array of objects in Vue. I'm using Salvattore to build a grid of my filtered elements, but it doesn't work too well. I think I have to call rescanMediaQueries(); function after my v-model changes but can't figure how.
Here is my Vue instance:
var articlesVM = new Vue({
el: '#search',
data: {
articles: [],
searchInput: null
},
ready: function() {
this.$http.get('posts').then(function (response) {
this.articles = response.body;
});
}
});
And here is how I have built my search
<div class="container" id="search">
<div class="input-field col s6 m4">
<input v-model="searchInput" class="center-align" id="searchInput" type="text" >
<label class="center-align" for="searchInput"> search... </label>
</div>
<div id="search-grid" v-show="searchInput" data-columns>
<article v-for="article in articles | filterBy searchInput">
<div class="card">
<div class="card-image" v-if="article.media" v-html="article.media"></div>
<div class="card-content">
<h2 class="card-title center-align">
<a v-bind:href="article.link">{{ article.title }}</a>
</h2>
<div class="card-excerpt" v-html="article.excerpt"></div>
</div>
<div class="card-action">
<a v-bind:href="article.link"><?php _e('Read More', 'sage'); ?></a>
</div>
</div>
</article>
</div>
I did get the grid system working by adding watch option to my Vue, but every time I wrote something to my input and then erase it my filterBy method wouldn't work at all. It didn't populate any data even if I tried to retype the same keyword as earlier. Here is the watch option I used:
watch: {
searchInput: function (){
salvattore.rescanMediaQueries();
}
}
I think your problem is with the scoping of this in your success handler for http. Your articles object in Vue component is not getting any values from your http.get(..) success handler.
Inside your ready function, your http success handler should be as follows:
this.$http.get('posts').then(response => {
this.articles = response.body; // 'this' belongs to outside scope
});`
Alternatively you can also do:
var self = this; // self points to 'this' of Vue component
this.$http.get('posts').then(response => {
self.articles = response.body; // 'self' points to 'this' of outside scope
});`
Another similar issue: https://stackoverflow.com/a/40090728/654825
One more thing - it is preferable to define data as a function, as follows:
var articlesVM = new Vue({
el: '#search',
data: function() {
return {
articles: [],
searchInput: null
}
},
...
}
This ensures that your articles object is unique to this instance of the component (when you use the same component at multiple places within your app).
Edited after comment #1
The following code seems to work alright, the watch function works flawlessly:
var vm = new Vue({
el: '#search',
template: `<input v-model="searchInput" class="center-align" id="searchInput" type="text" >`,
data: {
searchInput: ""
},
watch: {
searchInput: function() {
console.log("searchInput changed to " + this.searchInput);
}
}
})
The input in template is an exact copy of your version - I have even set the id along with v-model, though I do not see the reason to set an id
Vue.js version: 2.0.3
I am unable to see any further, based on details in the question. Can you check if your code matches with the one above and see if you can get the console debugging messages?
Edited after comment #4, #5
Here is another thought which you need to verify:
Role of vue.js: Render the DOM
Role of salvattore plugin: Make the DOM layouts using CSS only
Assuming the above is true for salvattore plugin, and hopefully it does not mess with vue.js observers / getters / setters, then you can do the following: provide a time delay of about 50 ms so that vue.js completes the rendering, and then call the salvattore plugin to perform the layouts.
So your watch function needs to be as follows:
watch: {
searchInput: function (){
setTimeout(function(){
salvattore.rescanMediaQueries();
}, 50);
}
}
Alternatively you may also use Vue.nexttick() as follows:
Vue.nextTick(function () {
// DOM updated
})
The nextTick is documented here: https://vuejs.org/api/#Vue-nextTick
I do not know if you may need to provide a little bit of extra time for salvattore plugin to start the layouts, but one of the above should work out.
Let me know if it works!

Vue: How do I call multiple functions with #click?

How can I call multiple functions in a single #click? (aka v-on:click)?
So far I tried
Splitting the functions with a semicolon: <div #click="fn1('foo');fn2('bar')"> </div>;
Using several #click: <div #click="fn1('foo')" #click="fn2('bar')"> </div>;
and as a workaround, I can just create a handler:
<div v-on:click="fn3('foo', 'bar')"> </div>
function fn3 (args) {
fn1(args);
fn2(args);
}
But sometimes this isn't nice. What would be the proper method/syntax?
On Vue 2.3 and above you can do this:
<div v-on:click="firstFunction(); secondFunction();"></div>
// or
<div #click="firstFunction(); secondFunction();"></div>
First of all you can use the short notation #click instead of v-on:click for readability purposes.
Second You can use a click event handler that calls other functions/methods as #Tushar mentioned in his comment above, so you end up with something like this :
<div id="app">
<div #click="handler('foo','bar')">
Hi, click me!
</div>
</div>
<!-- link to vue.js !-->
<script src="vue.js"></script>
<script>
(function(){
var vm = new Vue({
el:'#app',
methods:{
method1:function(arg){
console.log('method1: ',arg);
},
method2:function(arg){
console.log('method2: ',arg);
},
handler:function(arg1,arg2){
this.method1(arg1);
this.method2(arg2);
}
}
})
}());
</script>
If you want something a little bit more readable, you can try this:
<button #click="[click1($event), click2($event)]">
Multiple
</button>
To me, this solution feels more Vue-like hope you enjoy
updated dec-2021
you need to separate with a comma
like this:
<button #click="open(), onConnect()">Connect Wallet</button>
to add an anomymous function to do that may be an alternative:
<div v-on:click="return function() { fn1('foo');fn2('bar'); }()"> </div>
Separate into pieces.
Inline:
<div #click="f1() + f2()"></div>
OR: Through a composite function:
<div #click="f3()"></div>
<script>
var app = new Vue({
// ...
methods: {
f3: function() { f1() + f2(); }
f1: function() {},
f2: function() {}
}
})
</script>
This simple way to do
v-on:click="firstFunction(); secondFunction();"
This works for me when you need to open another dialog box by clicking a button inside a dialogue box and also close this one. Pass the values as params with a comma separator.
<v-btn absolute fab small slot="activator" top right color="primary" #click="(addTime = true),(ticketExpenseList = false)"><v-icon>add</v-icon></v-btn>
in Vue 2.5.1 for button works
<button #click="firstFunction(); secondFunction();">Ok</button>
I just want to add one small missing bit here which I felt missing in all of the answers above; that is you actually need to call the method rather than just passing its name as callable, when want to add multiple click handlers.
This might come as a surprise since Vue allows passing a callable to the click handler.
This works
<div><button #click="foo(); bar();">Button1</button></div>
<div><button #click="foo">Button2</button></div>
This does not
<div><button #click="foo; bar;">Button3</button></div>
JsFiddle example
The Vue event handling only allows for single function calls. If you need to do multiple ones you can either do a wrapper that includes both:
<div #click="handler"></div>
////////////////////////////
handler: function() { //Syntax assuming its in the 'methods' option of Vue instance
fn1('foo');
fn2('bar');
}
EDIT
Another option is to edit the first handler to have a callback and pass the second in.
<div #click="fn1('foo', fn2)"></div>
////////////////////////////////////
fn1: function(value, callback) {
console.log(value);
callback('bar');
},
fn2: function(value) {
console.log(value);
}
Html:
<div id="example">
<button v-on:click="multiple">Multiple</button>
</div>
JS:
var vm = new Vue({
el: '#example',
data: {
name: 'Vue.js'
},
// define methods under the `methods` object
methods: {
multiple: function (event) {
this.first()
this.second()
}
first: function (event) {
//yourstuff
}
second: function (event) {
//yourstuff
}
}
})
vm.multiple()
Based on ES6 with anonymous functions:
<button #click="() => { function1(); function2(); }"></button>
You can use this:
<div #click="f1(), f2()"></div>
Simply do like below:
with $event:
<div #click="function1($event, param1); function2($event,param1);"></div>
without $event:
<div #click="function1(param1); function2(param1);"></div>
I'd add, that you can also use this to call multiple emits or methods or both together by separating with ; semicolon
#click="method1(); $emit('emit1'); $emit('emit2');"
You can do it like
<button v-on:click="Function1(); Function2();"></button>
OR
<button #click="Function1(); Function2();"></button>
I was also looking this solution and used different methods and I found this one best for me. Just shared with you
***You can use template literals to use multiple function in one event in vuejs
<div #click="`${firstFunction() ${secondFunction() ${thirdFucntion()}`"></div>
Note:I am using vue3.
you can, however, do something like this :
<div onclick="return function()
{console.log('yaay, another onclick event!')}()"
#click="defaultFunction"></div>
yes, by using native onclick html event.

Reusing the same block helper in Meteor causes odd context issue

I'm attempting to reuse a custom Block Helper that I wrote to provide basic carousel functionality to some of my templates.
simple-carousel.html
<template name="SimpleCarousel">
<div class="simple-carousel {{class}}">
<div class="slides">
{{#each slides}}
{{> UI.contentBlock this}}
{{/each}}
</div>
{{#if showControls}}
{{> SimpleCarouselControls}}
{{/if}}
</div>
</template>
<template name="SimpleCarouselControls">
// control structure here
</template>
simple-carousel.js
var actions = {
back: function() {
// move slide back once
},
forward: function() {
// move slide forward once
}
};
var showSlide = function() {
// code to show the next slide
};
Template.SimpleCarousel.onRendered(function() {
// set up carousel logic here
});
Template.SimpleCarousel.events({
'click [data-sc-move="forward"]': function() {
actions.forward();
},
'click [data-sc-move="back"]': function() {
actions.back();
}
});
breaking_stories.html
<template name="BreakingStories">
{{#SimpleCarousel class="breaking-stories" showControls=false autoForward=8000 slides=breakingStories}}
{{> BreakingStorySlide}}
{{/SimpleCarousel}}
</template>
<template name="BreakingStorySlide">
<div class="breaking-story slide">
<div class=breaking-story-title">{{title}}</div>
</div>
</template>
breaking_stories.js
Template.BreakingStories.helpers({
breakingStories: function() {
return BreakingStories.find();
}
});
daily_summary.html
<template name="DailySummary">
{{#with thisDailySummary}}
{{#SimpleCarousel class="daily-summaries" showControls=true slides=items}}
{{> DailySummarySlide}}
{{/SimpleCarousel}}
{{/with}}
</template>
<template name="DailySummarySlide">
<div class="daily-summary slide">
<div class="daily-summary-title">{{title}}</div>
</div>
</template>
I've tried to simplify the code as there is a lot more HTML involved in the templates. Anyway, as you can see I've defined the #SimpleCarousel block helper and used it in two places: the breaking stories section, and the daily summaries section. These two templates happen to be on the same page (route), so they are near each other on the page. I need one of them to auto cycle through, in which I've provided the autoForward property to the helper, and the other one should just show controls.
Both templates render fine and show the correct data, but the problem lies in that instead of the breaking news template doing any automatic cycling, the other one does (and does it twice), as if they are sharing the same context.
My question is, can I use custom Block Helpers multiple times on the same route safely? I'm open to any suggestions on how to do this a better/different way.
Thanks to #JeremyK for pointing me in the right direction; it happened to be the exact code I left out which was the problem. Of course!
Here's what I had in the old version:
simple_carousel.js
var $slideContainer, $controls, $markers, $activeSlide, $nextSlide;
var actions = {
back: function() {
// move slide back
},
forward: function() {
// move slide forward
}
};
function showSlide() {
// show the "next" slide
}
Template.SimpleCarousel.onRendered(function() {
var data = this.data;
$slideContainer = this.$('.sc-slides');
// rest of this code is irrelevant
});
I had thought that the variables I had declared on the first line were independent of multiple instantiations of the templates I was using, but I was wrong. The first use of $slideContainer = this.$('.sc-slides'); worked fine, but $slideContainer and all the others are shared.
To fix this, I simply moved the local variables/actions into Template.SimpleCarousel.onRendered
Template.SimpleCarousel.onRendered(function() {
var $slideContainer, $markers, ...
this.actions = {
//...
};
});
Template.SimpleCarousel.events({
'click [data-sc-move="forward"]': function( event, template ) {
template.actions.forward();
}
//...
});

Backbone/Marionette: Adding view events to a dynamically added view for a bootstrap modal

I've got a Marionette layout and for demo purposes the html looks like:
<header id="header-region" class="page-title"></header>
<section id="template-content" class="full-section">
<div id="error-messages" class="fade main-section"><!-- errors --></div>
<div id="content-region"> </div>
</section>
Its layout view's regions are:
regions: {
header: "#header-region",
content: "#content-region"
}
Up until now, I've had any given page's modal html inside the page's template html which would be contained in the content region.
I have an idea to now create a separate region for modals to be shown in.
Changing things to look like this:
Template:
<section id="template-content" class="full-section">
<div id="error-messages" class="fade main-section"><!-- errors --></div>
<div id="content-region"> </div>
<div id="modal-region"></div>
</section>
And the region:
regions: {
header: "#header-region",
content: "#content-region",
modal: "#modal-region"
}
So I'd like to be able to do something like this:
// Controller
define([], function(){
initialize: function(){},
showHeaderView: function(){
this.HeaderView = new HeaderView();
this.layout.header.show(this.HeaderView);
},
showContentView: function(){
// this.BodyView's template used to contain the modal html
this.BodyView = new BodyView();
this.layout.content.show(this.BodyView);
},
showModalView: function(){
this.ModalView = new ModalView();
this.layout.modal.show(this.ModalView);
}
});
This works and renders the modal properly but the modal's events are lost because they were originally set by this.BodyView.
The modal has a checkbox that on change runs a function that is on this.BodyView but I want to bind the events for this.ModalView from this.BodyView.
How can I accomplish that? I've tried making this.ModalView's el the same as this.BodyView's but that breaks things. I've tried to use delegateEvents as well but with no luck.
This screencast does exactly what you want: http://www.backbonerails.com/screencasts/building-dialogs-with-custom-regions
Code is here: https://github.com/brian-mann/sc01-dialogs
If you are having the HeaderView as ItemView(or CollectionView/CompositeView) in it, you can instantiate it with passing arguments like
new HeaderView({events:{
"click .x" : function(){} // your function in-line or reference
});
So same applies to ModalView.

Categories