I have an angular application where I have a timeline with list event dates and the respective event description. This is the Html source code.
<!-- timeline -->
<h4 class="font-thin m-t-lg m-b-lg text-primary-lt">Historical Timeline</h4>
<p></p>
<div id="timeline"class="timeline m-l-sm m-r-sm b-info b-l">
<div ng-repeat = "timeline in formattedTimelineData | orderBy : '-eventDate'">
<div class = "tl-item">
<i class="pull-left timeline-badge {{timeline.class}} "></i>
<div class="m-l-lg">
<div id="eventDate{{$index}}" class="timeline-title">{{timeline.eventDate}}</div>
<p id="eventDescription{{$index}}" class="timeline-body">{{timeline.description}}</p>
</div>
</div>
</div>
</div>
<!-- / timeline -->
Now I am basically trying to make use protractor to ensure that the correct event date matches the event description. So i decided to use a map function. The issue is I would have a variable x which would tell me how many events there are . For example there can be 2 events, 6 events, etc. Events are dynamically
generated dynamically as you can tell by looking at html code also. Here is the code for my test I wrote.
it('FOO TEST', function(){
var x = 0;
while(x<4){
var timeline = element.all(by.css('#timeline')).map(function (timeline) {
return {
date: timeline.element(by.css('#eventDate'+x)).getText(),
events: timeline.element(by.css('#eventDescription'+x)).getText()
}
});
x++
}
timeline.then(function (Value) {
console.log(Value);
});
});
The issue is that for some reason in command line it only prints the last event out of 5 events. It does not print other events. I am definitely doing something wrong. I am brand new to promises so any suggestion here is appreciated. And yes i want to do like a individual test for each event in the timeline.
The problem is in the timeline locator: #timeline matches the timeline container while you need the inner repetative timeline blocks. Here is how you can match them:
var timeline = element.all(by.repeater('timeline in formattedTimelineData')).map(function (timeline) {
return {
date: timeline.element(by.binding('timeline.eventDate')).getText(),
events: timeline.element(by.binding('timeline.description')).getText()
}
});
timeline.then(function (timeline) {
console.log(timeline);
});
You can then loop over items like this:
timeline.then(function (timeline) {
for (var i = 0; i < timeline.length; ++i) {
// do smth with timeline[i]
}
});
Or, you can assert the complete timeline variable which is a promise and can be implicitly resolved by expect into an array of objects, for instance:
expect(timeline).toEqual([
{
date: "First date",
events: "Nothing happened"
},
{
date: "Second date",
events: "First base"
},
{
date: "Third date",
events: "Second base"
},
]);
I recommend against putting logic in your test - http://googletesting.blogspot.com/2014/07/testing-on-toilet-dont-put-logic-in.html.
A while loop is logic.
You should know ahead of time how many events will be in your timeline. In the example case 4. Then your specs should look like
element.all(by.css("#timeline")).then(function(events){
expect(events.count).toEqual(4);
expect(events[0].someThing()).toEqual(expectedValue0);
expect(events[1].someThing()).toEqual(expectedValue1);
...
expect(events[3].someThing()).toEqual(expectedValue3);
})
The repeater in my case has a lot of elements. I dont want to repeat through all elementsas my protractor spec times out while looping through so many elements. How can I just restrict the loop to run only for 1st 10 elements of the repeater? I have tried many things but I am just not able to get this working. How to loop only through first 10 elements of a repeater when using map()
The timeline variable in above example returns data for all elements in the repeater. How can i get the timeline variable to just have data for first 10 elements of the repeater as the looping through 1000 of entries of the repeater is time consuming that causes my protractor spec to timeout.
Related
Let's say I have the following array in an angular controller:
somelist = [
{ name: 'John', dirty: false },
{ name: 'Max', dirty: false },
{ name: 'Betty', dirty: false }
];
I want to ng-repeat through it in my view, and generate editable fields for each record:
<div ng-repeat="i in somelist">
<input type="text" ng-model="i.name"/>
</div>
How would I go about efficiently marking the field as dirty if someone edits the textbox(model)?
I realize that I could use ng-change on the text field, however, that fires every time a user makes a single change(enters a key) on the textbox, making loads of calls unnecessarily.. Is there a more efficient way of doing this which I am missing?
With JavaScript...
*Edited: if that textareas don't have any other 'change' event to run, you can try inline onchange event, and replace it's value after have run once. Just making onchange="once(this)" become into this — onchange="" *In background. The code will still stay in your HTML. Demo:
(also exist input and keyup events... in Angular as well)
function once(e){
e.style.color="red";
e.onchange = "";
//just demo... remove this.
const d = document.getElementById('demo');
d.innerText = Number(d.innerText) + 1;
}
<textarea class="moo" onchange="once(this)">Change me!</textarea>
<textarea class="moo" onchange="once(this)">Me too!</textarea>
<textarea class="moo" onchange="once(this)">And me!</textarea>
<br><br>
Triggered times: <span id="demo">0</span>
Running eventListener function once (not really):
let once = [];//creating empty array
const moo = document.getElementsByClassName('moo');//getting all textareas
for(let i = 0; i < moo.length; i++ ){//looping, to add 'change' event to each element
once.push(1);//adding '1' to array 'i' times. Here it will look like [1,1,1];
moo[i].addEventListener('change', function(){
if(once[i]==0){return}//if array element equals 0 = return and don't run the function
this.style.color = "red";
once[i] = 0;//after triggered = making array element = 0;
//just demo... remove this.
const d = document.getElementById('demo');
d.innerText = Number(d.innerText) + 1;
});
}
<textarea class="moo">Change me!</textarea>
<textarea class="moo">Me too!</textarea>
<textarea class="moo">And me!</textarea>
<br><br>
Triggered times: <span id="demo">0</span>
*function is still working each time... but returning immediately, which is better, than "full" run.
To improve efficiency, reduce the number of watchers by using :: and eliminating two-way binds, e.g.ng-model:
<div ng-repeat="i in ::somelist">
<input type="text" value="{{i.name}}"
ng-blur="$emit('nameChanged', i)"/>
</div>
Then, in your controller:
$scope.$on('nameChanged', (event, i) => updateName(i));
Then a quick, simple function that updates the name with the corresponding ID using i.id and i.name, assuming you have:
$scope.someList = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Max' },
{ id: 3, name: 'Betty }'
Explanation
If people aren't going to be added/removed from the list, then you can use :: on someList, also known as a one-time binding, to improve efficiency. This circumvents setting up a watcher.
Also, by setting value={{i.name}}, you effectively set up a one-way bind from the controller to the DOM, rather than two-way, meaning that the value of the input isn't being checked every loop of the $digest cycle, but any changes to the model will update the DOM.
Just an idea, feel free to play with variations, such as dropping blur and using a single button that would update all changed fields at once.
You won't get much more efficient than that, unless you also remove the watcher from value="{{i.name}}" like so value="{{::i.name}}", and then you manually update the DOM when the event is received.
Basically, I have an appointment form which is broken down into panels.
Step 1 - if a user clicks london (#Store1) then hide Sunday and Monday from the calendar in panel 5.
Basically, I want to store this click so that when the user gets to the calendar panel, it will know not to show Sunday and Monday
$('#store1').click(function () {
var $store1 = $(this).data('clicked', true);
console.log("store 1 clicked");
$('.Sunday').hide();
$('.Monday').hide();
});
after I have captured this in a var I then want to run it when the calendar displays.
function ReloadPanel(panel) {
return new Promise(function (resolve, reject, Store1) {
console.log(panel);
console.log("finalpanel");
panel.nextAll('.panel').find('.panel-updater').empty();
panel.nextAll('.panel').find('.panel-title').addClass('collapsed');
panel.nextAll('.panel').find('.panel-collapse').removeClass('in');
var panelUpdater = $('.panel-updater:eq(0)', panel),
panelUrl = panelUpdater.data('url');
if (panelUpdater.length) {
var formData = panelUpdater.parents("form").serializeObject();
panelUpdater.addClass('panel-updater--loading');
panelUpdater.load(panelUrl, formData, function (response, status) {
panelUpdater.removeClass('panel-updater--loading');
if (status == "error") {
reject("Panel reload failed");
} else {
resolve("Panel reloaded");
}
});
} else {
resolve("no reloader");
}
});
}
I'm not sure if this is even written right, so any help or suggestions would be great
Thanks in advance
Don't think of it as "storing a click". Instead, consider your clickable elements as having some sort of data values and you store the selected value. From this value you can derive changes to the UI.
For example, consider some clickable elements with values:
<button type="button" class="store-button" data-store-id="1">London</button>
<button type="button" class="store-button" data-store-id="2">Paris</button>
<button type="button" class="store-button" data-store-id="3">Madrid</button>
You have multiple "store" buttons. Rather than bind a click event to each individually and customize the UI for each click event, create a single generic one which captures the clicked value. Something like:
let selectedStore = -1;
$('.store-button').on('click', function () {
selectedStore = $(this).data('store-id');
});
Now anywhere that you can access the selectedStore variable can know the currently selected store. Presumably you have some data structure which can then be used to determine what "days" to show/hide? For example, suppose you have a list of "stores" each with valid "days":
let stores = [
{ id: 1, name: 'London', days: [2,3,4,5,6] },
// etc.
];
And your "days" buttons have their corresponding day ID values:
<button type="button" class="day-button" data-day-id="1">Sunday</button>
<button type="button" class="day-button" data-day-id="2">Monday</button>
<!--- etc. --->
You can now use the data you have to derive which buttons to show/hide. Perhaps something like this:
$('.day-button').hide();
for (let i in stores) {
if (stores[i].id === selectedStore) {
for (let j in stores[i].days) {
$('.day-button[data-day-id="' + stores[i].days[j] + '"]').show();
}
break;
}
}
There are a variety of ways to do it, much of which may depend on the overall structure and flow of your UX. If you need to persist the data across multiple pages (your use of the word "panels" implies more of a single-page setup, but that may not necessarily be the case) then you can also use local storage to persist things like selectedStore between page contexts.
But ultimately it just comes down to structuring your data, associating your UI elements with that data, and performing logic based on that data to manipulate those UI elements. Basically, instead of manipulating UI elements based only on UI interactions, you should update your data (even if it's just in-memory variables) based on UI interactions and then update your UI based on your data.
you can use the local storage for that and then you can get your value from anywhere.
Set your value
localStorage.setItem("store1", JSON.stringify(true))
Get you value then you can use it anywhere:
JSON.parse(localStorage.getItem("store1"))
Example:
$('#store1').click(function() {
var $store1 = $(this).data('clicked', true);
localStorage.setItem("store1", JSON.stringify(true))
console.log("store 1 clicked");
$('.Sunday').hide();
$('.Monday').hide();
});
I have deferred updates enabled.
I have two components.
The first is a list, which is simply implemented as a div with a foreach data binding:
<div class="list-people" data-bind="foreach: { data: people, afterRender: afterRenderPeople }">
<!-- ko component: { name: "listitem-person", params: { person: $data } } --><!-- /ko -->
</div>
The second is the list item:
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
afterRender is called for each item in the foreach.
My afterRenderPerson function is simple enough:
public afterRenderPerson = (elements: any[], data: Person) => {
let top = $(element[0]).offset().top;
scrollTo(top);
};
The problem is that when afterRenderPerson is called the sub-component listitem-person hasn't yet been rendered.
Which means the element array passed to afterRenderPerson has 4 nodes:
A text node containing \n i.e. a new line.
A comment node containing <!-- ko component: { name: "listitem-person", params: { person: $data } } -->.
A comment node containing <!-- /ko -->.
A text node containing \n i.e. a new line.
None of these are suitable for getting the top pixel, and even if they were, the sub-component being rendered could affect the layout at that location changing the value of the pixel I'm trying to get.
Unfortunately it seems that the documentation for foreach doesn't take in to account the delayed nature of components.
If you need to run some further custom logic on the generated DOM elements, you can use any of the afterRender/afterAdd/beforeRemove/beforeMove/afterMove callbacks described below.
Note: These callbacks are only intended for triggering animations related to changes in a list.
There are two workarounds I've come across, neither of which are great, but that's why they're workarounds and not solutions!
user3297291 gave the suggestion in a comment of making a scrollTo binding that's placed on the child components.
Only workaround I can think of is to define a custom scrollTo binding and include it in the component template... Quite easy to implement, but still feels hacky and makes your inner component harder to reuse. You might also want to track this feature request – user3297291
This would simply be a custom binding that conditionally executes some code based on a value provided to it.
The bindings aren't called until the HTML has been inserted in to the DOM. That's not perfect, as later changes to the DOM could affect the position of the inserted HTML elements, but it should work for many situations.
I wasn't very keen on having to modify the child components though, I preferred a solution when remained encapsulated in the parent component.
The second workaround is to check to see if the child component HTML element exists in the DOM by it's ID. Since I don't know when they will come in to existence this has to be done in some sort of loop.
A while loop isn't suitable as it'll run the check far too often, in a "tight" loop, so instead setTimeout is used.
setTimeout is a horrid hack, and it makes me feel dirty to use it, but it does work for this situation.
private _scrollToOffset = -100;
private _detectScrollToDelayInMS = 200;
private _detectScrollToCountMax = 40;
private _detectScrollToCount = 0;
private _detectScrollTo = (scrollToContainerSelector: string, scrollToChildSelector: string) => {
//AJ: If we've tried too many times then give up.
if (this._detectScrollToCount >= this._detectScrollToCountMax)
return;
setTimeout(() => {
let foundElements = $(scrollToChildSelector);
if (foundElements.length > 0) {
//AJ: Scroll to it
$(scrollToContainerSelector).animate({ scrollTop: foundElements.offset().top + this._scrollToOffset });
//AJ: Give it a highlight
foundElements.addClass("highlight");
} else {
//AJ: Try again
this._detectScrollTo(scrollToContainerSelector, scrollToChildSelector);
}
}, this._detectScrollToDelayInMS);
this._detectScrollToCount++;
};
I made sure to put a limit on how long it can run for, so if something goes wrong it won't loop forever.
It should probably be noted that there is an "Ultimate" solution to this problem, and that's TKO, AKA Knockout 4.
But that's not "production ready" yet.
How to know when a component has finished updating DOM?
brianmhunt commented on Jun 20
knockout/tko (ko 4 candidate) latest master branch has this.
More specifically, the applyBindings family of functions now return a Promise that resolves when sub-children (including asynchronous ones) are bound.
The API isn't set or documented yet, but the bones have been set up.
This appears to work. I made a binding handler that runs a callback in its init (it uses tasks.schedule to allow a rendering cycle). Attaching it at the parent level does not get the children rendered in time, but attaching it to the virtual element does.
I designed it to work with a function whose signature is like afterRender. Because it runs for each of the elements, the callback function has to test that the data is for the first one of them.
ko.options.deferUpdates = true;
ko.bindingHandlers.notify = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make it asynchronous, to allow Knockout to render the child component
ko.tasks.schedule(() => {
const onMounted = valueAccessor().onMounted;
const data = valueAccessor().data;
const elements = [];
// Collect the real DOM nodes (ones with a tagName)
for(let child=ko.virtualElements.firstChild(element);
child;
child=ko.virtualElements.nextSibling(child)) {
if (child.tagName) { elements.push(child); }
}
onMounted(elements, data);
});
}
};
ko.virtualElements.allowedBindings.notify = true;
function ParentVM(params) {
this.people = params.people;
this.afterRenderPeople = (elements, data) => {
console.log("Elements:", elements.map(e => e.tagName));
if (data === this.people[0]) {
console.log("Scroll to", elements[0].outerHTML);
//let top = $(element[0]).offset().top;
//scrollTo(top);
}
};
}
ko.components.register('parent-component', {
viewModel: ParentVM,
template: {
element: 'parent-template'
}
});
function ChildVM(params) {
this.Name = params.person;
}
ko.components.register('listitem-person', {
viewModel: ChildVM,
template: {
element: 'child-template'
}
});
vm = {
names: ['One', 'Two', 'Three']
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<template id="parent-template">
<div class="list-people" data-bind="foreach: people">
<!-- ko component: { name: "listitem-person", params: { person: $data } }, notify: {onMounted: $parent.afterRenderPeople, data: $data} -->
<!-- /ko -->
</div>
</template>
<template id="child-template">
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
</template>
<parent-component params="{ people: names }">
</parent-component>
I'm using Vue.Js for a survey, which is basically the main part and the purpose of the app. I have problem with the navigation. My prev button doesn't work and next keeps going in circles instead of only going forward to the next question. What I'm trying to accomplish is just to have only one question visible at a time and navigate through them in correct order using next and prev buttons and store the values of each input which I'll later use to calculate the output that will be on the result page, after the survey has been concluded. I've uploaded on fiddle a short sample of my code with only two questions just to showcase the problem. https://jsfiddle.net/cgrwe0u8/
new Vue({
el: '#quizz',
data: {
question1: 'How old are you?',
question2: 'How many times do you workout per week?',
show: true,
answer13: null,
answer10: null
}
})
document.querySelector('#answer13').getAttribute('value');
document.querySelector('#answer10').getAttribute('value');
HTML
<script src="https://unpkg.com/vue"></script>
<div id="quizz" class="question">
<h2 v-if=show>{{ question1 }}</h2>
<input v-if=show type="number" v-model="answer13">
<h2 v-if="!show">{{ question2 }}</h2>
<input v-if="!show" type="number" v-model="answer10">
<br>
<div class='button' id='next'>Next</div>
<div class='button' id='prev'>Prev
</div>
</div>
Thanks in advance!
You should look at making a Vue component that is for a survey question that way you can easily create multiple different questions.
Vue.component('survey-question', {
template: `<div><h2>{{question.text}}</h2><input type="number" v-model="question.answer" /></div>`,
props: ['question']
});
I've updated your code and implemented the next functionality so that you can try and create the prev functionality. Of course you should clean this up a little more. Maybe add a property on the question object so it can set what type the input should be. Stuff like that to make it more re-useable.
https://jsfiddle.net/9rsuwxvL/2/
If you ever have more than 1 of something, try to use an array, and process it with a loop. In this case you don't need a loop, but it's something to remember.
Since you only need to render one question at a time, just use a computed property to find the current question, based on some index. This index will be increased/decreased by the next/previous buttons.
With the code in this format, if you need to add a question, all you have to do is add it to the array.
https://jsfiddle.net/cgrwe0u8/1/
new Vue({
el: '#quizz',
data: {
questions:[
{question:'How old are you?', answer: ''},
{question:'How many times do you workout per week?', answer: ''},
],
index:0
},
computed:{
currentQuestion(){
return this.questions[this.index]
}
},
methods:{
next(){
if(this.index + 1 == this.questions.length)
this.index = 0;
else
this.index++;
},
previous(){
if(this.index - 1 < 0)
this.index = this.questions.length - 1;
else
this.index--;
}
}
})
I have a silly problem, where my only solution is a sloppy hack that is now giving me other problems.
See my fiddle,
or read the code here:
HTML:
<input id='1' value='input1' />
<template id='template1'>
<input id='2' value='input2' />
</template>
JS - Item View Declaration:
// Declare an ItemView, a simple input template.
var Input2 = Marionette.ItemView.extend({
template: '#template1',
onRender: function () {
console.log('hi');
},
ui: { input2: '#2' },
onRender: function () {
var self = this;
// Despite not being in the DOM yet, you can reference
// the input, through the 'this' command, as the
// input is a logical child of the ItemView.
this.ui.input2.val('this works');
// However, you can not call focus(), as it
// must be part of the DOM.
this.ui.input2.focus();
// So, I have had to resort to this hack, which
// TOTALLY SUCKS.
setTimeout(function(){
self.ui.input2.focus();
self.ui.input2.val('Now it focused. Dammit');
}, 1000)
},
})
JS - Controller
// To start, we focus input 1. This works.
$('#1').focus();
// Now, we make input 2.
var input2 = new Input2();
// Now we 1. render, (2. onRender is called), 3. append it to the DOM.
$(document.body).append(input2.render().el);
As one can see above, my problem is that I can not make a View call focus on itself after it is rendered (onRender), as it has not yet been appended to the DOM. As far as I know, there is no other event called such as onAppend, that would let me detect when it has actually been appended to the DOM.
I don't want to call focus from outside of the ItemView. It has to be done from within for my purposes.
Any bright ideas?
UPDATE
Turns out that onShow() is called on all DOM appends in Marionette.js, be it CollectionView, CompositeView or Region, and it isn't in the documentation!
Thanks a million, lukaszfiszer.
The solution is to render your ItemView inside a Marionette.Region. This way an onShow method will be called on the view once it's inserted in the DOM.
Example:
HTML
<input id='1' value='input1' />
<div id="inputRegion"></div>
<template id='template1'>
<input id='2' value='input2' />
</template>
JS ItemView
(...)
onShow: function () {
this.ui.input2.val('this works');
this.ui.input2.focus();
},
(...)
JS Controller
$('#1').focus();
var inputRegion = new Backbone.Marionette.Region({
el: "#inputRegion"
});
var input2 = new Input2();
inputRegion.show(input2);
More information in Marionette docs: https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#region-events-and-callbacks
Well, I managed to solve it by extending Marionette.js, but if anyone else has a better idea that doesn't involve extending a library, I will GLADLY accept it and buy you a doughnut.
// After studying Marionette.js' annotated source code,
// I found these three functions are the only places
// where a view is appended after rendering. Extending
// these by adding an onAppend call to the end of
// each lets me focus and do other DOM manipulation in
// the ItemView or Region, once I am certain it is in
// the DOM.
_.extend(Marionette.CollectionView.prototype, {
appendHtml: function(collectionView, itemView, index){
collectionView.$el.append(itemView.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.CompositeView.prototype, {
appendHtml: function(cv, iv, index){
var $container = this.getItemViewContainer(cv);
$container.append(iv.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.Region.prototype, {
open: function(view){
this.$el.empty().append(view.el);
if (view.onAppend) { view.onAppend() }
},
});