Related
In my laravel 5.8 / vue 2.5.17 / vuex^3.1.0 I have a problem that with dialog opened I have event duplication.
I have an event for item deletion :
In my vue file:
...
mounted() {
bus.$on('dialog_confirmed', (paramsArray) => {
if (paramsArray.key == this.deleteFromUserListsKey(paramsArray.user_list_id)) {
this.runDeleteFromUserLists(paramsArray.user_list_id, paramsArray.index);
}
})
bus.$on('onUserListDeleteSuccess', (response) => {
this.is_page_updating = false
this.showPopupMessage("User lists", 'User\'s list was successfully deleted!', 'success');
})
bus.$on('onUserListDeleteFailure', (error) => {
this.$setLaravelValidationErrorsFromResponse(error.message);
this.is_page_updating = false
this.showRunTimeError(error, this);
this.showPopupMessage("User lists", 'Error adding user\'s list !', 'error');
})
}, // mounted() {
methods: {
confirmDeleteUserList(user_list_id, user_list_title, index) {
this.confirmMsg("Do you want to exclude '" + user_list_title + "' user list ?", {
key: this.deleteFromUserListsKey(user_list_id), user_list_id: user_list_id, index: index
}, 'Confirm', bus);
}, //confirmDeleteUserList(id, user_list_title, index) {
deleteFromUserListsKey(user_list_id) {
return 'user_list__remove_' + user_list_id;
},
runDeleteFromUserLists(user_list_id, index) {
this.$store.dispatch('userListDelete', { logged_user_id : this.currentLoggedUser.id, user_list_id : user_list_id } );
}, // runDeleteFromUserLists() {
and in resources/js/store.js :
state : {
...
userLists: [],
...
actions : {
userListDelete(context, paramsArray ) {
axios({
method: ( 'delete' ),
url: this.getters.apiUrl + '/personal/user-lists/' + paramsArray.user_list_id,
}).then((response) => {
let L = this.getters.userLists.length
for (var I = 0; I < L; I++) {
if (response.data.id == this.getters.userLists[I].id) {
this.getters.userLists.splice(this.getters.userLists.indexOf(this.getters.userLists[I]), 1)
context.commit('refreshUserLists', this.getters.userLists);
break;
}
}
bus.$emit( 'onUserListDeleteSuccess', response );
}).catch((error) => {
bus.$emit('onUserListDeleteFailure', error);
});
}, // userListDelete(context, paramsArray ) {
confirmMsg (based on https://github.com/euvl/vue-js-modal )is defined in my mixing :
confirmMsg: function (question, paramsArray, title, bus) {
this.$modal.show('dialog', {
title: title,
text: question,
buttons: [
{
title: 'Yes',
default: true, // Will be triggered by default if 'Enter' pressed.
handler: () => {
bus.$emit('dialog_confirmed', paramsArray);
this.$modal.hide('dialog')
}
},
{
title: '', // Button title
handler: () => {
} // Button click handler
},
{
title: 'Cancel'
}
]
})
},
it worked ok, until I moved userListDelete method from my vue file into store.js.
As a result on 1st event item is deleted ok, the the second item raise error that item was not found and I do not know event is doubled...
How to fix it ?
UPDATED BLOCK :
I still search for valid decision :
I uploaded live demo at :
http://178.128.145.48/login
demo#demo.com wdemo
http://178.128.145.48/websites-blogs will be opened.
Please, try to go to “User's lists” by link at top left menu https://prnt.sc/nq4qiy
and back several times. When on “User's lists” page I try to delete 1 user list it is deleted, but I got several messages
and url in “network” section of my browser : https://imgur.com/a/4ubFB0g
Looks like events are duplicated. And looks like that is move between pages number of guplications is raised.
Why and how to fix it ?
I use #click.prevent in triggering the event to show confirm delete message.
There is “ Add Demo Data” to add more demo rows.
Thanks!
Well, it is quite obvious.
Take a closer look at the Vue component lifecycle diagram.
Your component is mounted each time you enter a route.
So, bus.$on inside your mounted block executed each time you enter this route.
I suggest you move bus event handlers to some other location. For example app.js/ App.vue mounted hook or directly into the store. Since all you do inside handler is calling store actions.
Angular has protractor to listen to lifecycle events in Angular.
eg: ptor.waitForAngular();
Is their a way to get selenium tests to wait for the various life-cycle events in polymer?
Currently we can have simple e2e tests running mocha, to do this is embed the tests directly in the html like how they do it in the source code.
You can just run your tests like they do here.
<script>
document.addEventListener('polymer-ready', function() {
mocha.run();
});
</script>
Eg to check for correct updates in a change watcher you can do this:
Polymer('x-test', {
bar: '',
ready: function() {
this.bar = 'bar';
setTimeout(function() {
this.zonk = 'zonk';
}.bind(this));
},
barChanged: function() {
chai.assert.equal(this.bar, 'bar', 'change in ready calls *Changed');
checkDone();
},
zonkChanged: function() {
chai.assert.equal(this.zonk, 'zonk', 'change calls *Changed without prototype value')
checkDone();
}
});
and for eg if you wanted to check a computer property is correct after the ready event you can do this:
<x-foo foo="mee" bar="too" count=3></x-foo>
<polymer-element name="x-foo" attributes="foo bar count">
<template>{{ fooBar }}:{{ fooBarCounted }}</template>
<script>
Polymer('x-foo', {
computed: {
fooBarCounted: 'repeat(fooBar, count)',
fooBar: "foo + '-' + bar"
},
repeat: function(str, count) {
var retval = '';
for (var i = 0; i < count; i++) {
retval += (i ? ' ' : '') + str + '(' + i + ')';
}
return retval;
},
ready: function() {
chai.assert.equal(this.shadowRoot.innerHTML, 'mee-too:mee-too(0) mee-too(1) mee-too(2)');
done();
}
})
</script>
</polymer-element>
Actually selenium (and other libs based on on it) can wait elements to appear on the page.
E.g. browser.waitForExist('#selector')
See API documentation here http://webdriver.io/api/utility/waitForExist.html
Hope it might be useful for you.
I have an infinite scroll directive that I am trying to unit test. Currently I have this:
describe('Infinite Scroll', function(){
var $compile, $scope;
beforeEach(module('nag.infiniteScroll'));
beforeEach(inject(function($injector) {
$scope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
$scope.scrolled = false;
$scope.test = function() {
$scope.scrolled = true;
};
}));
var setupElement = function(scope) {
var element = $compile('<div><div id="test" style="height:50px; width: 50px;overflow: auto" nag-infinite-scroll="test()">a<br><br><br>c<br><br><br><br>e<br><br>v<br><br>f<br><br>g<br><br>m<br>f<br><br>y<br></div></div>')(scope);
scope.$digest();
return element;
}
it('should have proper initial structure', function() {
var element = setupElement($scope);
element.find('#test').scrollTop(10000);
expect($scope.scrolled).toBe(true);
});
});
However the .scrollTop(10000); does not seem to trigger anything.
Is there anyway to unit test this type of functionality (I am able to unit test other directives with similar interactions like clicking on elements)?
In case it is relative, this is the infinite scroll code:
angular.module('nag.infiniteScroll', [])
.directive('nagInfiniteScroll', [
function() {
return function(scope, elm, attr) {
var raw = elm[0];
elm.bind('scroll', function() {
if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
scope.$apply(attr.nagInfiniteScroll);
}
});
};
}
]);
You have to trigger the scroll event on your element manually in your test:
element.find('#test').scrollTop(10000);
element.find('#test').triggerHandler('scroll');
Had the same issue recently. For the scrolling to work, you will need to set some dimensions on the body tag, so the window can be scrolled.
var scrollEvent = document.createEvent( 'CustomEvent' ); // MUST be 'CustomEvent'
scrollEvent.initCustomEvent( 'scroll', false, false, null );
var expectedLeft = 123;
var expectedTop = 456;
mockWindow.document.body.style.minHeight = '9000px';
mockWindow.document.body.style.minWidth = '9000px';
mockWindow.scrollTo( expectedLeft, expectedTop );
mockWindow.dispatchEvent( scrollEvent );
Unfortunately this does not work in PhantomJS.
If you are running your tests on Travis CI, you can also use Chrome by adding the following to your .travis.yml
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
And a custom Chrome launcher in your karma config:
module.exports = function(config) {
var configuration = {
// ... your default content
// This is the new content for your travis-ci configuration test
// Custom launcher for Travis-CI
customLaunchers: {
Chrome_travis_ci: {
base: 'Chrome',
flags: ['--no-sandbox']
}
},
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: true
};
if(process.env.TRAVIS){
configuration.browsers = ['Chrome_travis_ci'];
}
config.set( configuration );
};
I worked with backbone before and was wondering if there's a similar way to achieve this kind of pattern in dojo. Where you have a router and pass one by one your view separately (like layers) and then you can add their intern functionality somewhere else (e.g inside the view) so the code is very modular and can be change/add new stuff very easily. This code is actually in jquery (and come from a previous project) and it's a "common" base pattern to develop single application page under jquery/backbone.js .
main.js
var AppRouter = Backbone.Router.extend({
routes: {
"home" : "home"},
home: function(){
if (!this.homeView) {
this.homeView= new HomeView();
}
$('#content').html(this.homeView.el);
this.homeView.selectMenuItem('home-link');
}};
utils.loadTemplate(['HomeView'], function() {
app = new AppRouter();
Backbone.history.start();
});
utils.js
loadTemplate: function(views, callback) {
var deferreds = [];
$.each(views, function(index, view) {
if (window[view]) {
deferreds.push($.get('tpl/' + view + '.html', function(data) {
window[view].prototype.template = _.template(data);
}));
} else {
alert(view + " not found");
}
});
$.when.apply(null, deferreds).done(callback);
}};
HomeView.js
window.HomeView = Backbone.View.extend({
initialize:function () {
this.render();
},
render:function () {
$(this.el).html(this.template());
return this;
}
});
And basically, you just pass the html template. This pattern can be called anywhere with this link:
<li class="active"><i class="icon-home"></i> Dashboard</li>
Or, what is the best way to implement this using dojo boilerplate.
The 'boilerplate' on this subject is a dojox.mvc app. Reference is here.
From another aspect, see my go at it a while back, ive setup an abstract for 'controller' which then builds a view in its implementation.
Abstract
Then i have an application controller, which does following on its menu.onClick
which fires loading icon,
unloads current pane (if forms are not dirty)
loads modules it needs (defined 'routes' in a main-menu-store)
setup view pane with a new, requested one
Each view is either simply a server-html page or built with a declared 'oocms' controller module. Simplest example of abstract implementation here . Each implements an unload feature and a startup feature where we would want to dereference stores or eventhooks in teardown - and in turn, assert stores gets loaded etc in the setup.
If you wish to use templates, then base your views on the dijit._TemplatedMixin
edit
Here is a simplified clarification of my oocms setup, where instead of basing it on BorderLayout, i will make it ContentPanes:
Example JSON for the menu, with a single item representing the above declared view
{
identifier: 'view',
label: 'name',
items: [
{ name: 'myForm', view: 'App.view.MyForm', extraParams: { foo: 'bar' } }
]
}
Base Application Controller in file 'AppPackagePath/Application.js'
Note, the code has not been tested but should give a good impression of how such a setup can be implemented
define(['dojo/_base/declare',
"dojo/_base/lang",
"dijit/registry",
"OoCmS/messagebus", // dependency mixin which will monitor 'notify/progress' topics'
"dojo/topic",
"dojo/data/ItemFileReadStore",
"dijit/tree/ForestStoreModel",
"dijit/Tree"
], function(declare, lang, registry, msgbus, dtopic, itemfilereadstore, djforestmodel, djtree) {
return declare("App.Application", [msgbus], {
paneContainer: NULL,
treeContainer: NULL,
menuStoreUrl: '/path/to/url-list',
_widgetInUse: undefined,
defaultPaneProps: {},
loading: false, // ismple mutex
constructor: function(args) {
lang.mixin(this, args);
if(!this.treeContainer || !this.paneContainer) {
console.error("Dont know where to place components")
}
this.defaultPaneProps = {
id: 'mainContentPane'
}
this.buildRendering();
},
buildRendering: function() {
this.menustore = new itemfilereadstore({
id: 'appMenuStore',
url:this.menuStoreUrl
});
this.menumodel = new djforestmodel({
id: 'appMenuModel',
store: this.menustore
});
this.menu = new djtree( {
model: this.menumodel,
showRoot: false,
autoExpand: true,
onClick: lang.hitch(this, this.paneRequested) // passes the item
})
// NEEDS a construct ID HERE
this.menu.placeAt(this.treeContainer)
},
paneRequested: function(item) {
if(this.loading || !item) {
console.warn("No pane to load, give me a menustore item");
return false;
}
if(!this._widgetInUse || !this._widgetInUse.isDirty()) {
dtopic.publish("notify/progress/loading");
this.loading = true;
}
if(typeof this._widgetInUse != "undefined") {
if(!this._widgetInUse.unload()) {
// bail out if widget says 'no' (isDirty)
return false;
}
this._widgetInUse.destroyRecursive();
delete this._widgetInUse;
}
var self = this,
modules = [this.menustore.getValue(item, 'view')];
require(modules, function(viewPane) {
self._widgetInUse = new viewPane(self.defaultProps);
// NEEDS a construct ID HERE
self._widgetInUse.placeAt(this.paneContainer)
self._widgetInUse.ready.then(function() {
self.paneLoaded();
})
});
return true;
},
paneLoaded: function() {
// hide ajax icons
dtopic.publish("notify/progress/done");
// assert widget has started
this._widgetInUse.startup();
this.loading = false;
}
})
})
AbstractView in file 'AppPackagePath/view/AbstractView.js':
define(["dojo/_base/declare",
"dojo/_base/Deferred",
"dojo/_base/lang",
"dijit/registry",
"dijit/layout/ContentPane"], function(declare, deferred, lang, registry, contentpane) {
return declare("App.view.AbstractView", [contentpane], {
observers: [], // all programmatic events handles should be stored for d/c on unload
parseOnLoad: false,
constructor: function(args) {
lang.mixin(this, args)
// setup ready.then resolve
this.ready = new deferred();
// once ready, create
this.ready.then(lang.hitch(this, this.postCreate));
// the above is actually not nescessary, since we could simply use onLoad in contentpane
if(typeof this.content != "undefined") {
this.set("content", this.content);
this.onLoad();
} else if(typeof 'href' == "undefined") {
console.warn("No contents nor href set in construct");
}
},
startup : function startup() {
this.inherited(arguments);
},
// if you override this, make sure to this.inherited(arguments);
onLoad: function() {
dojo.parser.parse(this.contentNode);
// alert the application, that loading is done
this.ready.resolve(null);
// and call render
this.render();
},
render: function() {
console.info('no custom rendering performed in ' + this.declaredClass)
},
isDirty: function() { return false; },
unload: function() {
dojo.forEach(this.observers, dojo.disconnect);
return true;
},
addObserver: function() {
// simple passthrough, adding the connect to handles
var handle = dojo.connect.call(dojo.window.get(dojo.doc),
arguments[0], arguments[1], arguments[2]);
this.observers.push(handle);
}
});
});
View implementation sample in file 'AppPackagePath/view/MyForm.js':
define(["dojo/_base/declare",
"dojo/_base/lang",
"App/view/AbstractView",
// the contentpane href will pull in some html
// in the html can be markup, which will be renderered when ready
// pull in requirements here
"dijit/form/Form", // markup require
"dijit/form/Button" // markup require
], function(declare, lang, baseinterface) {
return declare("App.view.MyForm", [baseinterface], {
// using an external HTML file
href: 'dojoform.html',
_isDirty : false,
isDirty: function() {
return this._isDirty;
},
render: function() {
var self = this;
this.formWidget = dijit.byId('embeddedForm') // hook up with loaded markup
// observer for children
dojo.forEach(this.formWidget._getDescendantFormWidgets(), function(widget){
if(! lang.isFunction(widget.onChange) )
console.log('unable to observe ' + widget.id);
self.addObserver(widget, 'onChange', function() {
self._isDirty = true;
});
});
//
},
// #override
unload: function() {
if(this.isDirty()) {
var go = confirm("Sure you wish to leave page before save?")
if(!go) return false;
}
return this.inherited(arguments);
}
})
});
I want a button in column header dropdown menu of grid in extjs4.
so that i can add or delete columns which are linked in database.
Any help will be appreciated...
Thankyou..:)
Couple of months ago I had the same problem. I've managed to solve it by extending Ext.grid.header.Container (I've overrided getMenuItems method). However, recently, I've found another solution which requires less coding: just add menu item manualy after grid widget is created.
I'll post the second solution here:
Ext.create('Ext.grid.Panel', {
// ...
listeners: {
afterrender: function() {
var menu = this.headerCt.getMenu();
menu.add([{
text: 'Custom Item',
handler: function() {
var columnDataIndex = menu.activeHeader.dataIndex;
alert('custom item for column "'+columnDataIndex+'" was pressed');
}
}]);
}
}
});
Here is demo.
UPDATE
Here is demo for ExtJs4.1.
From what I have been seeing, you should avoid the afterrender event.
Context:
The application I am building uses a store with a dynamic model. I want my grid to have a customizable model that is fetched from the server (So I can have customizable columns for my customizable grid).
Since the header wasn't available to be modified (since the store gets reloaded and destroys the existing menu that I modified - using the example above). An alternate solution that has the same effect can be executed as such:
Ext.create('Ext.grid.Panel', {
// ...
initComponent: function () {
// renders the header and header menu
this.callParent(arguments);
// now you have access to the header - set an event on the header itself
this.headerCt.on('menucreate', function (cmp, menu, eOpts) {
this.createHeaderMenu(menu);
}, this);
},
createHeaderMenu: function (menu) {
menu.removeAll();
menu.add([
// { custom item here }
// { custom item here }
// { custom item here }
// { custom item here }
]);
}
});
For people who would like to have not just one "standard" column menu but have an individual columnwise like me, may use the following
initComponent: function ()
{
// renders the header and header menu
this.callParent(arguments);
// now you have access to the header - set an event on the header itself
this.headerCt.on('menucreate', function (cmp, menu, eOpts) {
menu.on('beforeshow', this.showHeaderMenu);
}, this);
},
showHeaderMenu: function (menu, eOpts)
{
//define array to store added compoents in
if(this.myAddedComponents === undefined)
{
this.myAddedComponents = new Array();
}
var columnDataIndex = menu.activeHeader.dataIndex,
customMenuComponents = this.myAddedComponents.length;
//remove components if any added
if(customMenuComponents > 0)
{
for(var i = 0; i < customMenuComponents; i++)
{
menu.remove(this.myAddedComponents[i][0].getItemId());
}
this.myAddedComponents.splice(0, customMenuComponents);
}
//add components by column index
switch(columnDataIndex)
{
case 'xyz': this.myAddedComponents.push(menu.add([{
text: 'Custom Item'}]));
break;
}
}
I took #nobbler's answer an created a plugin for this:
Ext.define('Ext.grid.CustomGridColumnMenu', {
extend: 'Ext.AbstractPlugin',
init: function (component) {
var me = this;
me.customMenuItemsCache = [];
component.headerCt.on('menucreate', function (cmp, menu) {
menu.on('beforeshow', me.showHeaderMenu, me);
}, me);
},
showHeaderMenu: function (menu) {
var me = this;
me.removeCustomMenuItems(menu);
me.addCustomMenuitems(menu);
},
removeCustomMenuItems: function (menu) {
var me = this,
menuItem;
while (menuItem = me.customMenuItemsCache.pop()) {
menu.remove(menuItem.getItemId(), false);
}
},
addCustomMenuitems: function (menu) {
var me = this,
renderedItems;
var menuItems = menu.activeHeader.customMenu || [];
if (menuItems.length > 0) {
if (menu.activeHeader.renderedCustomMenuItems === undefined) {
renderedItems = menu.add(menuItems);
menu.activeHeader.renderedCustomMenuItems = renderedItems;
} else {
renderedItems = menu.activeHeader.renderedCustomMenuItems;
menu.add(renderedItems);
}
Ext.each(renderedItems, function (renderedMenuItem) {
me.customMenuItemsCache.push(renderedMenuItem);
});
}
}
});
This is the way you use it (customMenu in the column config let you define your menu):
Ext.define('MyGrid', {
extend: 'Ext.grid.Panel',
plugins: ['Ext.grid.CustomGridColumnMenu'],
columns: [
{
dataIndex: 'name',
customMenu: [
{
text: 'My menu item',
menu: [
{
text: 'My submenu item'
}
]
}
]
}
]
});
The way this plugin works also solves an issue, that the other implementations ran into. Since the custom menu items are created only once for each column (caching of the already rendered version) it will not forget if it was checked before or not.