The documentation describing method call event handling suggests that it's possible to attach multiple handlers to an event separated by a comma, like this:
<p on-click='speak(), bark()'>{{name}}</p>
However this throws an error when the element is first rendered, so clearly I'm misunderstanding the docs. Can someone please help me to understand how to attach multiple handlers to the on-click event.
thanks in advance
Les
I believe you're still using 0.7 or older. Update to the latest version (0.8.7 as of this writing) and you'll be able to do the following:
const Component = Ractive.extend({
template: `
<button on-click="#this.foo(), #this.bar()">Click Me</button>
`,
foo(){
alert('Hello')
},
bar(){
alert('World!')
}
});
new Component({ el: 'body' });
Related
I'm trying to create a reusable styled input field in Vue. To make it styled (e.g. with an icon inside) I need to wrap it in another html-element.
Lets call the example below StyledInput
<div class="hasIcon">
<input />
<i class="someIcon"></i>
<div>
If I want to use StyledInput it might look like so:
<styled-input #keyup.enter="doSomething">
</styled-input>
But this would not work, due to the event listener being attached to the <div> instead of the <input>.
A workaround to that could be to emit all key-events from the input field:
<div class="hasIcon">
<input #keyup="$emit('keyup', $event) />
<i class="someIcon"></i>
<div>
But this will not scale well since it would have to be rewritten every time a developer uses an unmapped prop or event.
Is there a way to only make the inner element exposed to whomever uses it?
I'm not sure there is a Vue way to achieve this, because, as far as I'm aware there is no way to bind vue events dynamically, it is however possible to do this using vanilla javascript by passing all events as a prop then mapping them using addEventListener() to add your custom events:
Vue.component('my-input', {
template: "#my-input",
props: ['events'],
mounted() {
// get the input element
let input = document.getElementById('styled-input');
// map events
this.events.forEach((event) => {
let key = Object.keys(event);
input.addEventListener(key, event[key]);
});
}
})
Then you can just pass through all events as a prop like so:
<my-input :events="events"></my-input>
View Model:
var app = new Vue({
el: "#app",
data: {
events: [{
focus: () => {
console.log('focus')
}
}, {
keyup: (e) => {
console.log(e.which)
}
}]
}
})
Heres the JSFiddle: https://jsfiddle.net/h1dnk40v/
Of course, this means any developer would have to do things like map key codes etc, so you will lose some of the convenience Vue provides.
One thing I will just mention is that Vue components aren't necessarily intended to be infinitely reusable, they are supposed to provide specific functionality and encapsulate complex logic, so you would probably do better to implement the most likely use cases, and if the component doesn't fit you can extend it or write a new one for that particular event.
You can also use $attrs to pass props and events onto children elements:
<template>
<div>
<input v-bind="$attrs">
</div>
</template>
In Vue 3, you can specify a second script tag:
<script setup>
</script>
<script>
export default {
inheritAttrs: false,
};
</script>
https://vuejs.org/guide/components/attrs.html#disabling-attribute-inheritance
You could use slots to achieve this. If your <styled-input> template looks like this:
<div class="hasIcon">
<slot><input></slot>
<i class="someIcon"></i>
<div>
Then you can use it like this:
<styled-input>
<input #keyup.enter="doTheThing">
</styled-input>
Or, in cases where you don't care about the input events, like this:
<styled-input></styled-input>
and the default slot content (a bare <input>) will be used. You can use CSS to style the <input> inside the component, but you can't add custom properties or classes to it, so this approach may or may not fit your requirements.
I've got a non-angular page made with fairly basic JS, and thought it'd be a splendid idea to try and add learn some Angular2 and use it for some new functionality.
My plan was that I'd bind an Angular2 component to an object that is being updated by the old code, and I'd use Angular2 magic to update a chunk of UI.
The problem is I cant convince Angular2 to react to any changes made in the outside JS. What the trick to doing that? Attempts at googling the problem lead to in depth explanations of Angular2's change detection process, which hasn't been helpful so far. Is this just an awful idea?
I found a random Angular2 jsfiddle and hacked it up to show the problem. Strings are added to 'window.names', but you dont see them until one is added from the angular side: https://jsfiddle.net/byfo3jg3/ . The code follows:
var names = ['Joe'];
setTimeout(function() {
names.push("Frank");
}, 1000);
setTimeout(function() {
names.push("Sterve");
}, 2000);
setTimeout(function() {
names.push("Garfield");
}, 3000);
(function() {
var HelloApp,
ListThing;
ListThing = ng
.Component({
selector: 'list-thing',
template: '<ul><li *ng-for="#name of names">{{name}}</li></ul>',
directives: [ng.NgFor]
})
.Class({
constructor: function() {
this.names = window.names;
setTimeout(function() {
this.names.push("Oh hai");
}.bind(this), 10000);
}
});
HelloApp = ng
.Component({
selector: 'hello-app',
template: '<list-thing></list-thing>',
directives: [ListThing]
})
.Class({
constructor: function() {}
});
document.addEventListener('DOMContentLoaded', function() {
ng.bootstrap(HelloApp);
});
}());
You will need to set the NgZone to window object and then call run function of the zone.
Please refer to Angular 2 How to get Angular to detect changes made outside Angular? SO Question
names should be component property to work inside of template:
constructor(){this.names = window.names}
Changes to window.names will not be detected by angular, so you have few options: poll names using setInterval(()=>{this.names = window.names}, 1000) or expose global callback:
constructor(zone:NgZone)
{
window.notify = ()=> {
zone.run(()=> {
this.names = window.names;
});
}
}
and call it from plain js window.notify() or use other methods to invoke change detection.
Is this just an awful idea?
Yes.
Angular's automatic change detection system assumes that changes to data (that you want your components to display) are happening inside an event handler that is monkey-patched by Zone.js. Because then Angular's change detection will execute when such an event handler fires (well, technically, it will execute after the event handler finishes).
If you want a component view to automatically update, you have to change the bound data inside Angular – inside the Angular zone. As #Jigar answered, you can modify your code to call angularZone.run(_ => // make changes here), but if you have to do that, you might as well move the code that manages and manipulates the data into a service (or a component, if the logic is minimal).
See also Günter's alternative approach: set up an event listener inside Angular (hence inside the Angular zone). Then fire that event whenever you make changes outside the Angular zone.
I'm trying to customize events added to FullCalendar, using eventRender. I'm aware that I can directly return HTML from my eventRender method, but I would prefer to programmatically merge event data with a predefined Meteor template (with associated events).
Previously I could have used Meteor.render() but that functionality is no longer available. I'm familiar with Template.dynamic, but that appear to only be available declaratively, and most of the questions I've seen here are quite old, so refer to deprecated functionality.
Here's what I would like to do:
Calendar - event population & rendering:
Template.dashboard.rendered = function(){
$('#calendar').fullCalendar({
events: function(start, end, timezone, callback) {
callback(Events.find().fetch());
},
eventRender: function(event, element) {
// PROGRAMMATICALLY RENDER TEMPLATE
// The following does not work - no data is attached
return Template.calendarEvent.renderFunction(event);
}
});
};
Event template HTML
<template name="calendarEvent">
{{title}}
<!-- full layout for rendering event here -->
</template>
Event template JS
Template.calendarEvent.events({
// define template event handlers
});
That function was not removed, it was renamed and quite a while ago it also changed behaviour (when spark was replaced by blaze).
What you are looking for is Blaze.renderWithData.
Note that it returns a Blaze.View and not a DOM object.
To make it a dom object you could provide it as a parent:
var renderedCalendarEvent = document.createElement("div");
Blaze.renderWithData(Template.calenderEvent, event, renderedCalendarEvent);
The DOM element renderedCalendarEvent will react to any reactive sources the template uses.
If you need HTML you can use Blaze.toHTMLWithData, but that html will remain static.
Blaze.toHTMLWithData(Template.calenderEvent, event);
I am currently using iron-router and this is my very first attempt to try out the Meteor platform. I has been running into issues where most of the jquery libraries failed to initialized properly because the of the way Meteor renders html, $(document).ready() fires before any templates are rendered. I am wondering is there any callbacks from Meteor/iron-router that allows me to replace the jQuery's dom ready?
Also, how should I (easily and properly) handle the live update of the dom elements if some of them are customized by jQuery/javascript?
This is what i am currently doing, i feel like it is very hackish and probably would run into issues if the elements got updated after the initialization.
var jsInitalized = false;
Router.map(function () {
this.route('', {
path: '/',
layoutTemplate: 'default',
after: function(){
if(!jsInitalized){
setTimeout(function(){
$(document).ready( function() { $$$(); });
}, 0);
jsInitalized = true;
}
}
});
}
With Meteor you generally want to think about when a template is ready, not when the dom is ready.
For example, let's say you want to use the jQuery DataTables plugin to add sorting to a table element that's created by a template. You would listen to the template's rendered event and bind the plugin to the dom:
HTML:
<template name="data_table">
<table class="table table-striped" id="tblData">
</table>
</template>
JavaScript:
Template.data_table.rendered = function () {
$('#tblData').dataTable();
};
Now anytime the template is re-rendered (for example, if the data changes), your handler will be called and you can bind the jQuery plugin to the dom again.
This is the general approach. For a complete example (that includes populating the table with rows) see this answer.
Try making a separate .js file, call it rendered.js if you'd like. and then;
Template.layout.rendered = function ()
{
$(document).ready(function(){console.log('ready')});
}
I use template layout, but you can do Template.default.rendered. I hope that helps.
Also take a look at this part of documentation, especially the Template.events; http://docs.meteor.com/#templates_api
I use Meteor v0.8.0 with Iron Router (under Windows 7) and here is how I handle 'DOM ready':
When I want to modify the DOM after a specific template has been rendered:
I use Template.myTemplateName.rendered on the client side :
Template.blog.rendered = function()
{
$('#addPost').click(function()
{
...
});
}
When I want to modify the DOM after any new path has been rendered:
I use Router.onAfterAction, but there seems to be a trick:
Router.onAfterAction(function()
{
setTimeout(function()
{
$('.clickable').click(function()
{
...
});
}, 0);
});
Notice the setTimeout(..., 0), it doesn't work for me otherwise (DOM empty).
Notice that you can use onAfterAction on specific path, but most of the time I think it is redundant with the Template.myTemplateName.rendered method above.
What seems to be missing:
A way to modify the DOM after any template has been rendered.
Having a problem, and so far couldn't get any solutions for seemingly similar SO questions to work. Problem is this:
Using Trigger.io's forge.ajax, my Angular.js view is not updated after the data is returned. I realize this is because forge.ajax is an asychronous function, and the data is returned after the view has already been displayed. I have tried to update the view by using $rootScope.apply(), but it doesn't work for me as shown in the many examples I have seen.
See the Controller code below:
function OfferListCtrl($scope) {
$scope.offers = [];
$scope.fetchOffers = function(callback) {
$scope.offers = [];
var successCallback = function(odataResults) {
var rawJsonData = JSON.parse(odataResults);
var offers = rawJsonData.d;
callback(offers);
};
var errorCallback = function (error){
alert("Failure:" + error.message);
};
forge.request.ajax({
type: 'GET',
url: 'https://www.example.com/ApplicationData.svc/Offers',
accepts: 'application/json;odata=verbose',
username: 'username',
password: 'password',
success: successCallback,
error: errorCallback
});
};
$scope.fetchOffers(function(offers) {
$scope.offers = offers;
forge.logging.info($scope.offers);
});
}
All the code there works fine, and $scope.offers gets populated with the Offer data from the database. The logging function shows the data is correct, and in the correct format.
I have tried using $rootScope.apply() in the logical places (and some illogical ones), but cannot get the view to update. If you have any ideas how I can get this to work, I would greatly appreciate it.
Edit: Added HTML
The HTML is below. Note the button with ng-click="refresh()". This is a just a workaround so I can at least see the data. It calls a one-line refresh function that executes $rootScope.apply(), which does update the view.
<div ng-controller="OfferListCtrl">
<h1>Offers</h1>
<ul>
<li ng-repeat="offer in offers">
<p>Description: {{offer.Description}}<br />
Id: {{offer.Id}}<br />
Created On: {{offer.CreatedOn}}<br />
Published: {{offer.Published}}<br />
</p>
</li>
</ul>
<input type="button" ng-click="refresh()" value="Refresh to show data" />
</div>
You need to change
$scope.fetchOffers(function(offers) {
$scope.$apply(function(){
$scope.offers = offers;
});
forge.logging.info($scope.offers);
});
It is because all changes to the $scope has to be made within the angular scope, in this case since you are calling ajax request using forge the callback is not executing within the angular framework, that is why it is not working.
You can use $scope.$apply() in this case to execute the code within angular framework.
Look at the $apply() methods doc
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life-cycle of
exception handling, executing watches.
do this
function MyController($scope, myService)
{
myService.fetchOffers(data){
//assign your data here something like below or whateever
$offers = data
$scope.$apply();
}
});
Thanks
Dhiraj
When I do that I have an error like : "$digest already in progress"...
I'm Working with $q...
Someone knwo how I can resolve this issue ?
yes, this is caused where ur data comes fast enough and angular has not finished his rendering so the update cant update "outside" angular yet.
so use events:
http://bresleveloper.blogspot.co.il/2013/08/angularjs-and-ajax-angular-is-not.html