I am reading some backbone source code and in all the parent views I see the following line of code:
this.$el.children().detach();
I can't figure out the purpose for this though. I think it has something to do with resetting the parents content in the case we want to rerender the parent for some reason. Is this the reason or is it something else?
From the jquery documentation:
The .detach() method is the same as .remove(), except that .detach()
keeps all jQuery data associated with the removed elements. This
method is useful when removed elements are to be reinserted into the
DOM at a later time.
http://api.jquery.com/detach/
Sample code given as illustration:
<p>Hello</p>
how are
<p>you?</p>
<button>Attach/detach paragraphs</button>
<script>
$( "p" ).click(function() {
$( this ).toggleClass( "off" );
});
var p;
$( "button" ).click(function() {
if ( p ) {
p.appendTo( "body" );
p = null;
} else {
p = $( "p" ).detach();
}
});
</script>
Related
I have been using bootstrap selectpicker where I've added an Add Button for user to replicate the button as much as he wants. Problem is, selectpicker is not working on the second / cloned element and values of dropdown are just showing and not changing on click.
Main Select:
<div id="main_product">
<select name="product[]" class="selectpicker" >
<option value="Tube Lights" >Tube Lights</option>
<option value="Downlights" >Downlights</option>
</select>
</div>
Clone Function:
function clone()
{
var $orginal = $('#main_product');
var $cloned = $orginal.clone();
$cloned.appendTo('#new_products');
// $cloned.find('.bootstrap-select').remove();
// $cloned.find('select').selectpicker();
}
Note that I tried to reassign the selectpicker to the cloned object which is in comments atm, because it dint work also.
Any help would be really appreciated.
I came across this problem but if using the latest version now the select is placed inside the .bootstrap-select element for html5 error handling purposes. The remove() also removes the select, a work around is:
Instead of:
$cloned.find('.bootstrap-select').remove();
Use:
$cloned.find('.bootstrap-select').replaceWith(function() { return $('select', this); });
This will replace the .bootstrap-select element with the select element that it contains inside.
function clone()
{
//you can use :
var $orginal = $('#main_product');
var $cloned = $orginal.clone();
//Or
var $cloned = $('#main_product').clone();
//then use this to solve duplication problem
$cloned.find('.bootstrap-select').replaceWith(function() { return $('select', this); })
$cloned .find('.selectpicker').selectpicker('render');
//Then Append
$cloned.appendTo('#new_products');
}
if we replace the bootstrap-select with the select then the problem is in HTML structure.
so, original element is actual bootstrap-select and cloned are normal select.
take look at following for more clarification.
instead of this
$cloned.find('.bootstrap-select').replaceWith(function() { return $('select', this); });
use
$cloned.selectpicker('refresh');
$cloned.find('.bootstrap-select:odd').remove();
You can please change clone function
function clone() {
var $orginal = $('#main_product');
var $cloned = $orginal.clone();
var $selectPicker = $cloned.find('select');
$cloned
.find('.bootstrap-select').remove().end()
.append($selectPicker).end();
$cloned.find('select').selectpicker();
$cloned.appendTo('#new_products');
}
When using in Form Sets along with other form fields you need to replace and render all the selectpicker mentioned by #cm_mehdi. First find the new form html element and apply the below code:
//Clone new form
let newForm = purchaseForm[0].cloneNode(true);
// Replace and render the cloned html selectpicker
$(newForm).find('.bootstrap-select').replaceWith(function() { return $('select', this); })
$(newForm).find('.selectpicker').selectpicker('render');
// Re-Initialize search in dropdown
$(`#id_form_formfield-${formNum}`).selectpicker({liveSearch: true});
I have two connected ui-sortable lists. When one of the lists is empty, I need to show a message; when that empty list is hovered while dragging, I need to show a styled drop target and hide the empty list message. I was able to program the vast majority of this code and here is a simplifed Codepen of it working.
The bug is that when you drag from the populated list over the empty list and then out again, the empty list shows both the empty list placeholder and the styled drop target. Here is a screen capture:
The root of the problem appears to be in way I calculate if the list is empty for the sortableList directive:
scope.isEmpty = function() {
if (!scope.attachments) {
return true;
} else if (scope.dragDirection === 'drag-out' && !scope.hovered) {
return scope.attachments.length <= 1;
} else if (scope.hovered) {
return false;
} else {
return scope.attachments.length === 0;
}
};
Note that I am keeping track of the state on the scope and using $apply to ensure the DOM updates like so:
function onDragStart() {
scope.$apply(function() {
scope.dragDirection = 'drag-out';
});
}
function onDragStop() {
scope.$apply(function() {
scope.dragDirection = '';
});
}
function onDragOver() {
scope.$apply(function() {
scope.hovered = true;
});
}
function onDragOut() {
scope.$apply(function() {
scope.hovered = false;
});
}
Here is the html for the directives template:
<div class="drop-target" ui-sortable="sortOptions" ng-model="attachments">
<div ng-repeat="attachment in attachments" class="attachment-box">
<span class="fa fa-bars pull-left drag-handle"></span>
<div class="link-attachment">
<a href ng-href="{{ attachment.fileUrl }}" target="_blank" class="attachment-name">{{ attachment.name }}</a>
<div class="extra-info link-info">{{ attachment.fileType }}</div>
</div>
</div>
<attachment-empty-state ng-show="isEmpty()"></attachment-empty-state>
</div>
The dependency list is quite long for the codepen to work, I simplified the code from actual production code and eliminating the dependencies would have made the custom code quite substantial. Here is a list of the dependencies if you want to try to get it running yourself: jquery, jquery-ui, angular, bootstrap, lodash, and sortable from angular-ui. There is some font-awesome in there as well.
I think I solved the problem. Here is a codepen with the solution.
Basically, the problem was that the dragout event was being (correctly) fired when your cursor dragged the item out of a sortable-list, but the placeholder would stay in the sortable-list until you dragged it into another sortable-list. So in that in between time, both the attachment-empty-state element and the placeholder would be shown in the sortable-list.
Here are the lines that I edited in the code:
Less file:
attachment-empty-state {
...
// hide empty state when the placeholder is in this list
.placeholderShown & {
display:none;
}
}
JS:
//Inside sortable-list
// Helper function
function setPlaceholderShownClass(element) {
$(".drop-target").removeClass("placeholderShown");
$(element).addClass("placeholderShown");
}
...
function onPlaceholderUpdate(container, placeholder) {
setPlaceholderShownClass(container.element.context);
...
}
If you don't like using jQuery to add and remove classes globally, you could use $rootScope.$broadcast("placeholderShown") and $rootScope.$on("placeholderShown",function() { // scope logic }. I figured a little jQuery is less complex, even though it isn't pure Angular.
I'm trying to test if an element is visible using protractor. Here's what the element looks like:
<i class="icon-spinner icon-spin ng-hide" ng-show="saving"></i>
When in the chrome console, I can use this jQuery selector to test if the element is visible:
$('[ng-show=saving].icon-spin')
[
<i class="icon-spinner icon-spin ng-hide" ng-show="saving"></i>
]
> $('[ng-show=saving].icon-spin:visible')
[]
However, when I try to do the same in protractor, I get this error at runtime:
InvalidElementStateError:
invalid element state: Failed to execute 'querySelectorAll' on 'Document':
'[ng-show=saving].icon-spin:visible' is not a valid selector.
Why is this not valid? How can I check for visibility using protractor?
This should do it:
expect($('[ng-show=saving].icon-spin').isDisplayed()).toBe(true);
Remember protractor's $ isn't jQuery and :visible is not yet a part of available CSS selectors + pseudo-selectors
More info at https://stackoverflow.com/a/13388700/511069
The correct way for checking the visibility of an element with Protractor is to call the isDisplayed method. You should be careful though since isDisplayed does not return a boolean, but rather a promise providing the evaluated visibility. I've seen lots of code examples that use this method wrongly and therefore don't evaluate its actual visibility.
Example for getting the visibility of an element:
element(by.className('your-class-name')).isDisplayed().then(function (isVisible) {
if (isVisible) {
// element is visible
} else {
// element is not visible
}
});
However, you don't need this if you are just checking the visibility of the element (as opposed to getting it) because protractor patches Jasmine expect() so it always waits for promises to be resolved. See github.com/angular/jasminewd
So you can just do:
expect(element(by.className('your-class-name')).isDisplayed()).toBeTruthy();
Since you're using AngularJS to control the visibility of that element, you could also check its class attribute for ng-hide like this:
var spinner = element.by.css('i.icon-spin');
expect(spinner.getAttribute('class')).not.toMatch('ng-hide'); // expect element to be visible
I had a similar issue, in that I only wanted return elements that were visible in a page object. I found that I'm able to use the css :not. In the case of this issue, this should do you...
expect($('i.icon-spinner:not(.ng-hide)').isDisplayed()).toBeTruthy();
In the context of a page object, you can get ONLY those elements that are visible in this way as well. Eg. given a page with multiple items, where only some are visible, you can use:
this.visibileIcons = $$('i.icon:not(.ng-hide)');
This will return you all visible i.icons
If there are multiple elements in DOM with same class name. But only one of element is visible.
element.all(by.css('.text-input-input')).filter(function(ele){
return ele.isDisplayed();
}).then(function(filteredElement){
filteredElement[0].click();
});
In this example filter takes a collection of elements and returns a single visible element using isDisplayed().
This answer will be robust enough to work for elements that aren't on the page, therefore failing gracefully (not throwing an exception) if the selector failed to find the element.
const nameSelector = '[data-automation="name-input"]';
const nameInputIsDisplayed = () => {
return $$(nameSelector).count()
.then(count => count !== 0)
}
it('should be displayed', () => {
nameInputIsDisplayed().then(isDisplayed => {
expect(isDisplayed).toBeTruthy()
})
})
To wait for visibility
const EC = protractor.ExpectedConditions;
browser.wait(EC.visibilityOf(element(by.css('.icon-spinner icon-spin ng-hide')))).then(function() {
//do stuff
})
Xpath trick to only find visible elements
element(by.xpath('//i[not(contains(#style,"display:none")) and #class="icon-spinner icon-spin ng-hide"]))
element(by.className('your-class-name'))
.isDisplayed()
.then(function (isVisible) {
if (isVisible) { // element is visible
} else { // element is not visible
}
})
.catch(function(err){
console.error("Element is not found! ", err);
})
Here are the few code snippet which can be used for framework which use Typescript, protractor, jasmine
browser.wait(until.visibilityOf(OversightAutomationOR.lblContentModal), 3000, "Modal text is present");
// Asserting a text
OversightAutomationOR.lblContentModal.getText().then(text => {
this.assertEquals(text.toString().trim(), AdminPanelData.lblContentModal);
});
// Asserting an element
expect(OnboardingFormsOR.masterFormActionCloneBtn.isDisplayed()).to.eventually.equal(true
);
OnboardingFormsOR.customFormActionViewBtn.isDisplayed().then((isDisplayed) => {
expect(isDisplayed).to.equal(true);
});
// Asserting a form
formInfoSection.getText().then((text) => {
const vendorInformationCount = text[0].split("\n");
let found = false;
for (let i = 0; i < vendorInformationCount.length; i++) {
if (vendorInformationCount[i] === customLabel) {
found = true;
};
};
expect(found).to.equal(true);
});
Something to consider
.isDisplayed() assumes the element is present (exists in the DOM)
so if you do
expect($('[ng-show=saving]').isDisplayed()).toBe(true);
but the element is not present, then instead of graceful failed expectation, $('[ng-show=saving]').isDisplayed() will throw an error causing the rest of it block not executed
Solution
If you assume, the element you're checking may not be present for any reason on the page, then go with a safe way below
/**
* element is Present and is Displayed
* #param {ElementFinder} $element Locator of element
* #return {boolean}
*/
let isDisplayed = function ($element) {
return (await $element.isPresent()) && (await $element.isDisplayed())
}
and use
expect(await isDisplayed( $('[ng-show=saving]') )).toBe(true);
waitTillElementIsPresent(locator: Locator): promise.Promise<boolean>
{
const EC = protractor.ExpectedConditions;
return browser.wait(EC.visibilityOf(element(by.id('xyz')), browser.params.explicitWaitTime, 'Element taking too long to appear in the DOM');
}
const isDisplayed = await $('div').isDisplayed().then(null, err => false)
I am using same el for more than 1 view like below. I'm not facing any problem till now. Is this good approach or should i do any changes?
<div id="app">
<div id="app-header"></div>
<div id="app-container"></div>
<div id="app-footer">
</div>
App View:
{
el: "#app",
v1: new View1(),
v2: new View2(),
render: function () {
if (cond1) {
this.v1.render();
} else if (cond2) {
this.v2.render();
}}
}
View 1:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
View 2:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
By reading your question, I do not really see what advantages you could possibly have using this approach rather than having the different div elements being the root el for your views 1, 2, 3 and using
this.$el.html(template)
in the render method.
Your approach could work for a small application, but I think it will become really hard to maintain as the application grows.
EDIT
I still do not really get your point, you could only initialize everything only once in both cases.
Here is a working Fiddle.
By the way I am changing the content by listening to the click event but this is to simplify the example. It should be done by the router.
I do use a mixin to handle such situation, I call it stated view. For a view with all other options I will send a parameter called 'state', render will in-turn call renderState first time and there after every time I set a 'state' renderState will update the view state. Here is my mixin code looks like.
var setupStateEvents = function (context) {
var stateConfigs = context.getOption('states');
if (!stateConfigs) {
return;
}
var state;
var statedView;
var cleanUpState = function () {
if (statedView) {
statedView.remove();
}
};
var renderState = function (StateView) {
statedView = util.createView({
View: StateView,
model: context.model,
parentEl: context.$('.state-view'),
parentView:context
});
};
context.setState = function (toState) {
if (typeof toState === 'string') {
if (state === toState) {
return;
}
state = toState;
var StateView = stateConfigs[toState];
if (StateView) {
cleanUpState();
renderState(StateView);
} else {
throw new Error('Invalid State');
}
} else {
throw new Error('state should be a string');
}
};
context.getState = function () {
return state;
};
context.removeReferences(function(){
stateConfigs = null;
state=null;
statedView=null;
context=null;
})
};
full code can be seen here
https://github.com/ravihamsa/baseapp/blob/master/js/base/view.js
hope this helps
Backbone Rule:
When you create an instance of a view, it'll bind all events to el if
it was assigned, else view creates and assigns an empty div as el for that view and bind
all events to that view.
In my case, if i assign #app-container to view 1 and view 2 as el and when i initialize both views like below in App View, all events bind to the same container (i.e #app-container)
this.v1 = new App.View1();
this.v2 = new App.View2();
Will it lead to any memory leaks / Zombies?
No way. No way. Because ultimately you are having only one instance for each view. So this won't cause any memory leaks.
Where does it become problematic?
When your app grows, it is very common to use same id for a tag in both views. For example, you may have button with an id btn-save in both view's template. So when you bind btn-save in both views and when you click button in any one the view, it will trigger both views save method.
See this jsFiddle. This'll explain this case.
Can i use same el for both view?
It is up to you. If you avoid binding events based on same id or class name in both views, you won't have any problem. But you can avoid using same id but it's so complex to avoid same class names in both views.
So for me, it looks #Daniel Perez answer is more promising. So i'm going to use his approach.
In my router I require a view like this:
require(['views/business-detail-view'],
function(BusinessDetailView) {
var businessDetailView = new BusinessDetailView({collection: businessOverviewCollection.models[id], id: id});
businessDetailView.render();
}
);
and in the view I'm binding events like this:
events: {
'click #about-btn' : 'aboutHandler',
'click #contact-btn' : 'contactHandler',
'click #deals-btn' : 'dealsHandler',
'click #map-btn' : 'mapHandler'
},
Now the issue is that if the view gets rendered the first times the callbacks are invoked ones. But if the view needs to be rendered again in some other place the callbacks are invoked twice and so on.
How can I prevent this or am I doing something wrong?
UPDATE:
In the meantime I have changed the code in my router to:
if ( !businessDetailView ) {
require(['views/business-detail-view'],
function(BusinessDetailView) {
businessDetailView = new BusinessDetailView({collection: businessOverviewCollection.models[id]});
businessDetailView.render();
}
);
}
else {
businessDetailView.collection = businessOverviewCollection.models[id];
businessDetailView.render();
}
which seem to solve the issue, but I'm still to new to backbone this know whether this is a valid pattern.
At some point in your view you probably clear out the existing HTML on the page and replace it with new HTML. When you clear out old HTML you should also clear out old event handlers so they aren't laying around. For example, in your view when you want to render your newHtml you could do:
#$el.off().html(newHtml)