Monitoring variable change done by jQuery inside component - javascript

I know this isn't optimal, but I have regular javascript code changing a variable that is property in my component.
The reason for this is that I have dynamic javascript (that comes from Google Blockly) being run (eval) in my component.
Here's the component template:
<input #textInput
[attr.max-value]="maxValue"
type="number"
style="width: 495px;"
(change)="onChange($event)"
[id]="key"
class="form-control"
[disabled]="disabled"
value="{{value}}" />
And the component code:
export class NumberStepComponent extends BaseStepComponent {
/** number-step ctor */
constructor() {
super();
}
#ViewChild("textInput") textInput: ElementRef;
private _maxValue: string;
get maxValue(): string {
return this._maxValue;
}
set maxValue(val: string) {
this._maxValue = val;
console.log("Changed ** : " + val);
}
}
When I change maxValue from Angular, I do get the setter code triggered. But when jQuery does it, that setter isn't triggered.
ie:
var code = `$('#ID').attr('max-value', "5")`;
eval(code);
I've tried executing the code "in the zone" thinking this would keep Angular up to date:
this.ngZone.run(() => {
var code = `$('#ID').attr('max-value', "5")`;
eval(code);
});
Doesn't trigger the setter either. Any ways to achieve this ?

You should not use $('#ID').attr('max-value', "5"), since the value is binded (so handled by Angular).
If you want to change the value, update the model (this.maxValue = '5'), not the view. Angular will update the view for you.

It's hard to guess what you can/cannot do but you can try to add a MutationObserver. For example :
const node = document.querySelector('.some-element') // or this.textInput.nativeElement;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => console.log(mutation));
});
observer.observe(node, {
attributes: true,
childList: true,
characterData: true
});

Related

Leaflet geoman "Button with this name already exists" error when creating a new button in react

I am trying to create a custom button for arrows in the drawing tool of leaflet-geoman.
The idea was to work with the copyDrawControl function, and to use Line as a model to make Polylines with arrow tips.
I wrote a code mostly inspired from this demonstration https://codesandbox.io/s/394eq?file=/src/index.js and modified it for my goals. Here is the code :
import { useEffect } from "react";
import { useLeafletContext } from "#react-leaflet/core";
import "#geoman-io/leaflet-geoman-free";
import "#geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
const Geoman = () => {
const context = useLeafletContext();
useEffect(() => {
const leafletContainer = context.layerContainer || context.map;
leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
//draw control options
leafletContainer.pm.addControls({
positions: {
draw: 'topleft',
edit: 'topright',
},
drawMarker: false,
rotateMode: false,
cutPolygon: false,
position: "bottomright"
});
//new button
leafletContainer.pm.Toolbar.copyDrawControl('Line', {
name: 'SoonToBeArrow',
block: 'draw',
title: 'Display text on hover button',
actions: [
// uses the default 'cancel' action
'cancel',
],
});
return () => {
leafletContainer.pm.removeControls();
leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
};
}, [context]);
return null;
};
export default Geoman;
When trying to add the copyDrawControl, I faced a bug that would announce that "Button with this name already exists"
I suspect its because I add the button inside a useEffect that gets called several times, but it's also the only way to access leafletContainer, since it must be updated everytime the context changes.
I tried creating another useEffect that contains the same context and my new button, but it did not work.
Does anyone have any suggestion on how to solve this ?
Thnak you in advance
You only want to run this effect once, just after context becomes available. In order to do this, we can make a state variable to track whether or not you've already added the control:
const Geoman = () => {
const context = useLeafletContext();
const [added, setAdded] = useState(false);
useEffect(() => {
const leafletContainer = context.layerContainer || context.map;
// if the context is ready, and we've not yet added the control
if (leafletContainer && !added){
leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
//draw control options
leafletContainer.pm.addControls({
// ...
});
//new button
leafletContainer.pm.Toolbar.copyDrawControl('Line', {
// ...
});
// register that we've already added the control
setAdded(true);
}
return () => {
leafletContainer.pm.removeControls();
leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
};
}, [context]);
return null;
};
In this way, you effect will run whenever context changes - once context is ready, you add the control. You register that you've added the control, but then your if statement will make sure that further changes in context will not try to keep adding controls again and again.
BTW, a second option to using leaflet geoman with react leaflet is to use the official createControlComponent hook to create custom controls. This is not at all straightforward with leaflet-geoman, as createControlComponent requires you to feed it an instance of an L.Control that has all the required hooks and initializer methods. geoman does not have these - it is quite different in the way it initializes and adds to a map. However, you can create an L.Control from geoman methods, and then feed it to createControlComponent.
Create the L.Control:
/**
* Class abstraction wrapper around geoman, so that we can create an instance
* that is an extension of L.Control, so that react-leaflet can call all
* L.PM methods using the expected L.Control lifecycle event handlers
*/
const GeomanControl = L.Control.extend({
initialize(options: Props) {
L.PM.setOptIn(options.optIn ?? false);
L.setOptions(this, options);
},
addTo(map: L.Map) {
const { globalOptions, events } = this.options;
// This should never happen, but its better than crashing the page
if (!map.pm) return;
map.pm.addControls(toolbarOptions);
map.pm.setGlobalOptions({
pmIgnore: false,
...globalOptions,
});
// draw control options
map.pm.addControls({
// ...
});
// new button
map.pm.Toolbar.copyDrawControl('Line', {
// ...
});
Object.entries(events ?? {}).forEach(([eventName, handler]) => {
map.on(eventName, handler);
});
},
});
Then simply use createControlComponent
const createControl = (props) => {
return new GeomanControl(props);
};
export const Geoman = createControlComponent(createControl);
You can add quite a lot of logic into the addTo method, and base a lot of its behaviors off the props you feed to <Geoman />. This is another flexible way of adapting geoman for react-leaflet v4.

jasmine unit-testing: Can't trigger change event in custom angular dropdown component

I am working on unit-testing with jasmine and karma an angular component. In the template-component a custom dropdown component is used:
<div class="my-modal-body">
<form [formGroup]="form" class="form-horizontal">
<div class="padding-dropdown">
<my-form-wrapper
label="Dateiformat"
labelClass="col-lg-4 two-col-label-size"
inputClass="col-lg-8 two-col-input-size"
>
<my-dropdown
[items]="exportOptionen"
formControlName="dateiTyp"
(selectedChange)="exportFileChange($event)"
>
</my-dropdown>
</my-form-wrapper>
</div>
In my testing I try to test the value change, but can't get it working. However I try to set the value, the exportFileChange is not triggered.
And I know that the component is correct, because it's already in production. So it has to be the test that errors.
Here is my test:
it('dateiTyp auswahl excel', waitForAsync(() => {
spyOn(component, 'exportFileChange');
dateiTyp.setValue('Excel');
component.dateiTyp.setValue('Excel', { emitEvent: true });
fixture.detectChanges();
fixture.whenStable().then(
() => {
expect(component.exportFileChange).toHaveBeenCalled();
let exDiv = fixture.debugElement.query(By.css("#excelExportDiv"));
expect(exDiv).not.toBeNull();
}
);
}));
When changing the selection the exportFileChange(event) method should be called and in the template an div appears. The exportFileChange-Method in the component just changes an boolean.
I tested changing the boolean in the test and that worked, but the event still wasn't triggered.
Here are the most relevant parts of the setup:
describe('ExportModalComponent', () => {
[...]
let dateiTyp: jasmine.SpyObj<FormControl>;
let dateiTypChange: Subject<string>;
[...]
beforeEach( waitForAsync(() => {
[...]
dateiTyp = jasmine.createSpyObj('dateiTyp', ['value', 'setValue']);
formGroup.get.withArgs('dateiTyp').and.returnValue(dateiTyp);
dateiTypChange = new Subject();
Object.defineProperty(dateiTyp, 'valueChanges', { value: dateiTypChange });
[...]
and my-dropdown.component.d.ts:
export declare class myDropdownComponent implements ControlValueAccessor, OnInit, OnChanges
{ [...] }
I can change the ExportModal-template or the ExportModal-component but I can't change the implementation or use of myDropdownComponent.
I am grateful for every help!
Thanks!
This is not a complete answer but it should help you.
This is a good read. In these kind of situations, I just use triggerEventHandler on the DebugElement.
Something like this:
it('should do abc', () => {
// get a handle on the debugElement
const myDropDown = fixture.debugElement.query(By.css('my-dropdown'));
// the first argument is the name of the event you would like to trigger
// and the 2nd argument of type object (or anything) is the $event you want to mock for the calling function
myDropDown.triggerEventHandler('selectedChange', { /* mock $event here */});
// continue with tests
});
I am not entirely sure how your components are wired but that's the best way I have found to raise custom events for custom components.

VueJs 2.0 emit event from grand child to his grand parent component

It seems that Vue.js 2.0 doesn't emit events from a grand child to his grand parent component.
Vue.component('parent', {
template: '<div>I am the parent - {{ action }} <child #eventtriggered="performAction"></child></div>',
data(){
return {
action: 'No action'
}
},
methods: {
performAction() { this.action = 'actionDone' }
}
})
Vue.component('child', {
template: '<div>I am the child <grand-child></grand-child></div>'
})
Vue.component('grand-child', {
template: '<div>I am the grand-child <button #click="doEvent">Do Event</button></div>',
methods: {
doEvent() { this.$emit('eventtriggered') }
}
})
new Vue({
el: '#app'
})
This JsFiddle solves the issue https://jsfiddle.net/y5dvkqbd/4/ , but by emtting two events:
One from grand child to middle component
Then emitting again from middle component to grand parent
Adding this middle event seems repetitive and unneccessary. Is there a way to emit directly to grand parent that I am not aware of?
Vue 2.4 introduced a way to easily pass events up the hierarchy using vm.$listeners
From https://v2.vuejs.org/v2/api/#vm-listeners :
Contains parent-scope v-on event listeners (without .native modifiers). This can be passed down to an inner component via v-on="$listeners" - useful when creating transparent wrapper components.
See the snippet below using v-on="$listeners" in the grand-child component in the child template:
Vue.component('parent', {
template:
'<div>' +
'<p>I am the parent. The value is {{displayValue}}.</p>' +
'<child #toggle-value="toggleValue"></child>' +
'</div>',
data() {
return {
value: false
}
},
methods: {
toggleValue() { this.value = !this.value }
},
computed: {
displayValue() {
return (this.value ? "ON" : "OFF")
}
}
})
Vue.component('child', {
template:
'<div class="child">' +
'<p>I am the child. I\'m just a wrapper providing some UI.</p>' +
'<grand-child v-on="$listeners"></grand-child>' +
'</div>'
})
Vue.component('grand-child', {
template:
'<div class="child">' +
'<p>I am the grand-child: ' +
'<button #click="emitToggleEvent">Toggle the value</button>' +
'</p>' +
'</div>',
methods: {
emitToggleEvent() { this.$emit('toggle-value') }
}
})
new Vue({
el: '#app'
})
.child {
padding: 10px;
border: 1px solid #ddd;
background: #f0f0f0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent></parent>
</div>
NEW ANSWER (Nov-2018 update)
I discovered that we could actually do this by leveraging the $parent property in the grand child component:
this.$parent.$emit("submit", {somekey: somevalue})
Much cleaner and simpler.
The Vue community generally favors using Vuex to solve this kind of issue. Changes are made to Vuex state and the DOM representation just flows from that, eliminating the need for events in many cases.
Barring that, re-emitting would probably be the next best choice, and lastly you might choose to use an event bus as detailed in the other highly voted answer to this question.
The answer below is my original answer to this question and is not an approach I would take now, having more experience with Vue.
This is a case where I might disagree with Vue's design choice and resort to DOM.
In grand-child,
methods: {
doEvent() {
try {
this.$el.dispatchEvent(new Event("eventtriggered"));
} catch (e) {
// handle IE not supporting Event constructor
var evt = document.createEvent("Event");
evt.initEvent("eventtriggered", true, false);
this.$el.dispatchEvent(evt);
}
}
}
and in parent,
mounted(){
this.$el.addEventListener("eventtriggered", () => this.performAction())
}
Otherwise, yes, you have to re-emit, or use a bus.
Note: I added code in the doEvent method to handle IE; that code could be extracted in a reusable way.
Yes, you're correct events only go from child to parent. They don't go further, e.g. from child to grandparent.
The Vue documentation (briefly) addresses this situation in the Non Parent-Child Communication section.
The general idea is that in the grandparent component you create an empty Vue component that is passed from grandparent down to the children and grandchildren via props. The grandparent then listens for events and grandchildren emit events on that "event bus".
Some applications use a global event bus instead of a per-component event bus. Using a global event bus means you will need to have unique event names or namespacing so events don't clash between different components.
Here is an example of how to implement a simple global event bus.
If you want to be flexible and simply broadcast an event to all parents and their parents recursively up to the root, you could do something like:
let vm = this.$parent
while(vm) {
vm.$emit('submit')
vm = vm.$parent
}
Another solution will be on/emit at root node:
Uses vm.$root.$emit in grand-child, then uses vm.$root.$on at the ancestor (or anywhere you'd like).
Updated: sometimes you'd like to disable the listener at some specific situations, use vm.$off (for example: vm.$root.off('event-name') inside lifecycle hook=beforeDestroy).
Vue.component('parent', {
template: '<div><button #click="toggleEventListener()">Listener is {{eventEnable ? "On" : "Off"}}</button>I am the parent - {{ action }} <child #eventtriggered="performAction"></child></div>',
data(){
return {
action: 1,
eventEnable: false
}
},
created: function () {
this.addEventListener()
},
beforeDestroy: function () {
this.removeEventListener()
},
methods: {
performAction() { this.action += 1 },
toggleEventListener: function () {
if (this.eventEnable) {
this.removeEventListener()
} else {
this.addEventListener()
}
},
addEventListener: function () {
this.$root.$on('eventtriggered1', () => {
this.performAction()
})
this.eventEnable = true
},
removeEventListener: function () {
this.$root.$off('eventtriggered1')
this.eventEnable = false
}
}
})
Vue.component('child', {
template: '<div>I am the child <grand-child #eventtriggered="doEvent"></grand-child></div>',
methods: {
doEvent() {
//this.$emit('eventtriggered')
}
}
})
Vue.component('grand-child', {
template: '<div>I am the grand-child <button #click="doEvent">Emit Event</button></div>',
methods: {
doEvent() { this.$root.$emit('eventtriggered1') }
}
})
new Vue({
el: '#app'
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<parent></parent>
</div>
VueJS 2 components have a $parent property that contains their parent component.
That parent component also includes its own $parent property.
Then, accessing the "grandparent" component it's a matter of accessing the "parent's parent" component:
this.$parent["$parent"].$emit("myevent", { data: 123 });
Anyway, this is kinda tricky, and I recommend using a global state manager like Vuex or similar tools, as other responders have said.
I've made a short mixin based on #digout answer. You want to put it, before your Vue instance initialization (new Vue...) to use it globally in project. You can use it similarly to normal event.
Vue.mixin({
methods: {
$propagatedEmit: function (event, payload) {
let vm = this.$parent;
while (vm) {
vm.$emit(event, payload);
vm = vm.$parent;
}
}
}
})
Riffing off #kubaklam and #digout's answers, this is what I use to avoid emitting on every parent component between the grand-child and the (possibly distant) grandparent:
{
methods: {
tunnelEmit (event, ...payload) {
let vm = this
while (vm && !vm.$listeners[event]) {
vm = vm.$parent
}
if (!vm) return console.error(`no target listener for event "${event}"`)
vm.$emit(event, ...payload)
}
}
}
When building out a component with distant grand children where you don't want many/any components to be tied to the store, yet want the root component to act as a store/source of truth, this works quite well. This is similar to the data down actions up philosophy of Ember. Downside is that if you want to listen for that event on every parent in between, then this won't work. But then you can use $propogateEmit as in above answer by #kubaklam.
Edit: initial vm should be set to the component, and not the component's parent. I.e. let vm = this and not let vm = this.$parent
This is the only case when I use event bus!! For passing data from deep nested child, to not directly parent, communication.
First: Create a js file (I name it eventbus.js) with this content:
import Vue from 'vue'
Vue.prototype.$event = new Vue()
Second: In your child component emit an event:
this.$event.$emit('event_name', 'data to pass')
Third: In the parent listen to that event:
this.$event.$on('event_name', (data) => {
console.log(data)
})
Note: If you don't want that event anymore please unregister it:
this.$event.$off('event_name')
INFO: No need to read the below personal opinion
I don't like to use vuex for grand-child to grand-parent communication (Or similar communication level).
In vue.js for passing data from grand-parent to grand-child you can use provide/inject. But there is not something similar for the opposite thing. (grand-child to grand-parent) So I use event bus whenever I have to do that kind of communication.
Riffing off #digout answer. I am thinking that if the purpose is to send data to a far-ancestor then we don't need $emit at all. I did this for my edge-case and it seems to work. Yes, it could be implemented via a mixin but it doesn't have to be.
/**
* Send some content as a "message" to a named ancestor of the component calling this method.
* This is an edge-case method where you need to send a message many levels above the calling component.
* Your target component must have a receiveFromDescendant(content) method and it decides what
* to do with the content it gets.
* #param {string} name - the name of the Vue component eg name: 'myComponentName'
* #param {object} content - the message content
*/
messageNamedAncestor: function (name, content) {
let vm = this.$parent
let found = false
while (vm && !found) {
if (vm.$vnode.tag.indexOf('-' + name) > -1) {
if (vm.receiveFromDescendant) {
found = true
vm.receiveFromDescendant(content)
} else {
throw new Error(`Found the target component named ${name} but you dont have a receiveFromDescendant method there.`)
}
} else {
vm = vm.$parent
}
}
}
Given an ancestor:
export default {
name: 'myGreatAncestor',
...
methods: {
receiveFromDescendant (content) {
console.log(content)
}
}
}
A great grand-child says
// Tell the ancestor component something important
this.messageNamedAncestor('myGreatAncestor', {
importantInformation: 'Hello from your great descendant'
})
As of Vue 3, a number of fundamental changes have happened to root events:
The $on, $off and $once root methods no longer exist. There is to a certain extent something to replace this, since you can listen to root events by doing this:
createApp(App, {
// Listen for the 'expand' event
onExpand() {
console.log('expand')
}
})
Another solution are event buses, but the Vue.js documents take a dim view - they can cause maintenance headaches in the long run. You might get an ever spreading set of emits and event sinks, with no clear or central idea of how it is managed or what components could be affected elsewhere. Nonetheless, examples given by the docs of event buses are mitt and tiny-emitter.
However the docs make it clear that they recommend handling these sorts of situations in this order:
Props A convenient solution for parent / child communications.
Provide/Inject A simple way for ancestors to communicate with their descendants (although critically, not the other way around).
Vuex A way of handling global state in a clear fashion. It's important to note that this is not solely for events, or communications - Vuex was built primarily to handle state.
Essentially the choice for the OP would come down to using an event bus, or Vuex. In order to centralise the event bus, you could place it inside Vuex, if state was also needed to be globally available. Otherwise using an event bus with strict centralised controls on it's behaviour and location might help.
I really dig the way this is handled by creating a class that is bound to the window and simplifying the broadcast/listen setup to work wherever you are in the Vue app.
window.Event = new class {
constructor() {
this.vue = new Vue();
}
fire(event, data = null) {
this.vue.$emit(event, data);
}
listen() {
this.vue.$on(event, callback);
}
}
Now you can just fire / broadcast / whatever from anywhere by calling:
Event.fire('do-the-thing');
...and you can listen in a parent, grandparent, whatever you want by calling:
Event.listen('do-the-thing', () => {
alert('Doing the thing!');
});

cloneConnectFn is not a function when using $filter

I'm working on a project that's using typescript and angular to generate dynamic web content. Currently, my job is to format an input field as currency, which is simple enough in plain javascript (with angular), but I need to do it in typescript, which is presenting some problems. Here's my directive (which I formatted as a class as per another stack overflow answer suggested)
namespace incode.directives.label {
interface IScope extends ng.IScope {
amount: number;
}
export class IncodeCurrencyInputDirective implements ng.IDirective {
restrict ='A';
public require: 'ngModel';
public scope: Object;
replace = true;
public link: ng.IDirectiveLinkFn | ng.IDirectivePrePost;
constructor(private $filter: ng.IFilterService) {
this.scope = {
amount: '='
};
this.link = (scope: IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctlr: any, $filter: ng.IFilterService) => {
//element.bind('focus',
// function () {
// element.val(scope.amount);
// });
element.bind('input',
function () {
scope.amount = element.val();
scope.$apply;
});
element.bind('blur',
function () {
element.val($filter('currency')(scope.amount));
});
}
}
public static factory(): ng.IDirectiveFactory {
var directive = ($filter) => new IncodeCurrencyInputDirective($filter);
directive.$inject = ['$filter'];
return directive;
}
}
angular.module('incode.module')
.directive('ixCurrency', incode.directives.label.IncodeCurrencyInputDirective.factory());
}
and here's the template that uses it
<md-input-container layout-fill class="number-range">
<input placeholder="From"
type="text"
name="from"
ix-currency
amount="0.00"
precision="{{rangeController.precision}}"
ng-model="rangeController.Bucket.RangeFilterFromString"/>
</md-input-container>
As it stands at the moment, the script generates an error on blur: TypeError: cloneConnectFn is not a function which seems like a very low-level function used by angular, and shouldn't normally be producing this error. Any insight you can provide is greatly appreciated!
EDIT: Since I know a lot of angular gurus have never touched typescript, I'll attach a pastebin of the compiled javascript, in case that helps http://pastebin.com/NnTLqauL
I figured it out, no idea why it was giving me that error precisely, but the root of the problem is a miscommunication of scope between typescript and angular. You'll notice I have the line
element.val($filter('currency')(scope.amount));
in the 'blur' binding above. The reason I reference filter in this way is because if you write this.$filter, typescript compiles it such that even though it's inside an object, this refers to window instead of the object, so $filter is out of scope. The solution is to use a lambda function to spoof the scoping like so:
element.bind('blur',
() => {
element.val(this.$filter('currency')(scope.amount));
});
Always be mindful of where this points (something extremely difficult for me it seems) and you should be fine.

AngularJS 2: How to have an 'attribute' directive communicate with its host component?

I'm trying to write a utility directive in Angular 2, which should collaborate with its host component:
<my-component my-directive></my-component>
In particular, my-directive will watch for certain inputs, then calls certain functions that should be provided by my-component. For example, the directive I'm writing would encapsulate the boilerplate for having a component act as a drop-area for a drag-and-drop operation:
export const ResourceDropArea = ng.Directive({
selector: '[resource-drop-area]',
inputs: ['data: resourceDropArea'],
host: {
'(dragenter)': ' dragenter($event) ',
'(dragleave)': ' dragleave($event) ',
'(dragover)': ' dragover ($event) ',
'(drop)': ' drop ($event) '
}
}).Class({
constructor() {}
// event handling code
});
But the component itself would still have to specify what to do when data is dropped on it. That's where I'm having trouble. How do I get a hold of the component object? Or is there a better way?
Oh, and I would much appreciate an ES6 solution (not just Typescript).
You can use dependency injection to get the instance of the host component.
export const ResourceDropArea = ng.Directive({ /* ... */ }).Class({
constructor(myComponent) {
this.myComponent = myComponent;
},
drop($event) {
this.myComponent.callSomeMethod();
}
});
ResourceDropArea.parameters = [MyComponent];

Categories

Resources