Updating the view of a screen in AngularJS - javascript

Hey—I’m working on an angular 1.6 app, and I’ve come across an issue I’m not quite sure how to resolve. Here’s a brief discussion of the set up. The page is a form with thirty questions. There is a lot of conditional logic from the client that goes as follows: if user selects “Yes” on Q1 then show them Q2-4, if “No” on Q1, then skip to Q5. This repeats itself in the thirty question form.
I’ve come across the following scenario—what if the user selects “Yes” on Q1, answers Q2-4 and then realizes Q1 should really be “No”.
So my question is: 1) how do I clear the models? and 2) how do I update the view? So that if they decide, third time around, that Q1 should in fact be yes, Q2-Q4 are unchecked/clear/blank.
For 1) I just wrote a simple “clearAll method as follows:
function clearAll(arr){
arr.forEach(function(element){
if(element!==undefined){
element = null;
}
});
}
This clears the models but doesn’t update the view. How do I update the view so that Q2-4 are clear/empty/blank?
The idea I had is to update the view, so
$scope.$watch( myModel, function(val){
//set to null or delete?
}
But I cannot generalize this so it is flexible enough to accept an array. Any advice would be appreciated. Thanks.

For your particular problem I would use ui-router and resolve different states based on a question, i.e. state.questions and take different parameters like if you have url /questions as your base intro page (maybe weclome to questions or something), you would have /questions/1 and it would be 1st question /questions/2 would be second etc...
Then keep one list of objects on some simple service i.e.:
let questions = [];
which will persist throughout the app, later to be filled with some objects like:
{
q1: {
answers: [1,2,3]
},
{
q2: {
answers: [1,3]
}
You can always clear it after you are done with it or push new answer. Also you can check for particular answer before even loading the state, and this will give you great flexibility what you actually want.
well for ugly solution your watch maybe could work if you add watch group and timeout:
$scope.$watchGroup(arrayOfModels, (newVal, oldVal)=>{
if(newVal) {
// now do something you want with that models
// if they don't apply, try $timeout(()=> $scope.$apply());
}
})

Related

How do create a related search terms?

I'm working on a school project in code.org for my CS class. I'm trying to build an app that is about the bird.(We are learning about list/arrays, loops, and traversals)
In the project I'm trying to build a search box where people can type the name of the bird and in the next page will show up the information about the bird. (Those information are from the code.org data library, it has a lot of lists of datasets that you can use to build your app)
The code looks like this:
// code.org is using ES5
var birdSearch = getText('searchInput');
for(var i = 0; i < birdNameList.length; i++) {
if (birdSearch === birdNameList[i]) {
setText('birdNameOutput', birdNameList[i]);
setText('birdDietOuput', birdDietList[i]);
setText('birdImageOutput', birdImageList[i]);
}
But I'm afraid that no one will know those bird's name because you have to search the exactly same name in the list, then my app will be useless. So I am thinking to build a thing that will show the most related name depends on the user input.
It's like if you put 'Am' in the search box and it will show 'American Goldfinch', 'American Purple Gallinule' ... under the search box.
For example:
search box: Am______
do you mean: American Goldfinch
American Purple Gallinule
...
great first question and welcome! There are a lot of things here that you may wish to consider, such as the event that triggers the search - does the search happen when you click a button or when text is typed? If it is the latter then you may also want to think about debouncing the event, which is essentially adding a slight delay to calling the method to prevent it firing too many times and causing performance related issues, though I realise that this is probably a bit further on in your learning, but definitely something worth investigating as your learning progresses.
However, to get to the point of your question, I think it is probably best to look at a filter method to filter the array of results, with something like this:
const birdSearch = getText('searchInput');
const searchList = birdNameList.filter(function(bird) {
return bird.includes('birdSearch');
});
setText('suggestionBox', 'Do you mean: ' + searchList.join(', ') + '?');
Hopefully that should give you a starter for ten!

How to get the text from an Insert event in CKEditor 5?

I am trying to process an insert event from the CKEditor 5.
editor.document.on("change", (eventInfo, type, data) => {
switch (type) {
case "insert":
console.log(type, data);
break;
}
});
When typing in the editor the call back is called. The data argument in the event callback looks like approximately like this:
{
range: {
start: {
root: { ... },
path: [0, 14]
},
end: {
root: { ... },
path: [0, 15]
}
}
}
I don't see a convenient way to figure out what text was actually inserted. I can call data.range.root.getNodeByPath(data.range.start.path); which seems to get me the text node that the text was inserted in. Should we then look at the text node's data field? Should we assume that the last item in the path is always an offset for the start and end of the range and use that to substring? I think the insert event is also fired for inserting non-text type things (e.g. element). How would we know that this is indeed a text type of an event?
Is there something I am missing, or is there just a different way to do this all together?
First, let me describe how you would do it currently (Jan 2018). Please, keep in mind that CKEditor 5 is now undergoing a big refactoring and things will change. At the end, I will describe how it will look like after we finish this refactoring. You may skip to the later part if you don't mind waiting some more time for the refactoring to come to an end.
EDIT: The 1.0.0-beta.1 was released on 15th of March, so you can jump to the "Since March 2018" section.
Until March 2018 (up to 1.0.0-alpha.2)
(If you need to learn more about some class API or an event, please check out the docs.)
Your best bet would be simply to iterate through the inserted range.
let data = '';
for ( const child of data.range.getItems() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
Note, that a TextProxy instance is always returned when you iterate through the range, even if the whole Text node is included in the range.
(You can read more about stringifying a range in CKEditor5 & Angular2 - Getting exact position of caret on click inside editor to grab data.)
Keep in mind, that InsertOperation may insert multiple nodes of a different kind. Mostly, these are just singular characters or elements, but more nodes can be provided. That's why there is no additional data.item or similar property in data. There could be data.items but those would just be same as Array.from( data.range.getItems() ).
Doing changes on Document#change
You haven't mentioned what you want to do with this information afterwards. Getting the range's content is easy, but if you'd like to somehow react to these changes and change the model, then you need to be careful. When the change event is fired, there might be already more changes enqueued. For example:
more changes can come at once from collaboration service,
a different feature might have already reacted to the same change and enqueued its changes which might make the model different.
If you know exactly what set of features you will use, you may just stick with what I proposed. Just remember that any change you do on the model should be done in a Document#enqueueChanges() block (otherwise, it won't be rendered).
If you would like to have this solution bulletproof, you probably would have to do this:
While iterating over data.range children, if you found a TextProxy, create a LiveRange spanning over that node.
Then, in a enqueueChanges() block, iterate through stored LiveRanges and through their children.
Do your logic for each found TextProxy instance.
Remember to destroy() all the LiveRanges afterwards.
As you can see this seems unnecessarily complicated. There are some drawbacks of providing an open and flexible framework, like CKE5, and having in mind all the edge cases is one of them. However it is true, that it could be simpler, that's why we started refactoring in the first place.
Since March 2018 (starting from 1.0.0-beta.1)
The big change coming in 1.0.0-beta.1 will be the introduction of the model.Differ class, revamped events structure and a new API for big part of the model.
First of all, Document#event:change will be fired after all enqueueChange blocks have finished. This means that you won't have to be worried whether another change won't mess up with the change that you are reacting to in your callback.
Also, engine.Document#registerPostFixer() method will be added and you will be able to use it to register callbacks. change event still will be available, but there will be slight differences between change event and registerPostFixer (we will cover them in a guide and docs).
Second, you will have access to a model.Differ instance, which will store a diff between the model state before the first change and the model state at the moment when you want to react to the changes. You will iterate through all diff items and check what exactly and where has changed.
Other than that, a lot of other changes will be conducted in the refactoring and below code snippet will also reflect them. So, in the new world, it will look like this:
editor.document.registerPostFixer( writer => {
const changes = editor.document.differ.getChanges();
for ( const entry of changes ) {
if ( entry.type == 'insert' && entry.name == '$text' ) {
// Use `writer` to do your logic here.
// `entry` also contains `length` and `position` properties.
}
}
} );
In terms of code, it might be a bit more of it than in the first snippet, but:
The first snippet was incomplete.
There are a lot fewer edge cases to think about in the new approach.
The new approach is easier to grasp - you have all the changes available after they are all done, instead of reacting to a change when other changes are queued and may mess up with the model.
The writer is an object that will be used to do changes on the model (instead of Document#batch API). It will have methods like insertText(), insertElement(), remove(), etc.
You can check model.Differ API and tests already as they are already available on master branch. (The internal code will change, but API will stay as it is.)
#Szymon Cofalik's answer went into a direction "How to apply some changes based on a change listener". This made it far more complex than what's needed to get the text from the Document#change event, which boils down to the following snippet:
let data = '';
for ( const child of data.range.getChildren() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
However, reacting to a change is a tricky task and, therefore, make sure to read Szymon's insightful answer if you plan to do so.

Rx.js fromEvent + flatMapLatest broken?

Well, the problem itself is kind of hard to describe briefly, so here's a live example to demonstrate. It seems like I'm misunderstanding something about how Rx.js works, otherwise the functionality here comes from a bug.
What I tried to do was a simple reactive rendering setup, where what you see on the screen, and what events happen are both described in terms of Observables. The problem is that, for some indiscernible reason, the events are dropped entirely when the code is written one way, yet work fine with code that should theoretically be equivalent.
So, let's start with the first case in the example code above:
var dom = makeBox('one');
var clicks = Rx.Observable.fromEvent(dom, 'click');
If you create a DOM fragment, then you can simply use fromEvent to get an Observable for whatever event it emits. So far, so good. You can click this box and see a bunch of lines written to the log.
Now, the next step would be to make the DOM reactive, to express how it changes over time.
var domStream = Rx.Observable.return(makeBox('two'));
var clicks = domStream.flatMapLatest(function(dom) {
return Rx.Observable.fromEvent(dom, 'click');
});
That would make it an Observable, using return here to produce the simplest, constant case. The events you're interested in would be the ones emitted by the latest version of the dom, and that's exactly what the flatMapLatest operator does. This variant still works.
Ultimately, the goal would be to generate the current DOM state based on some application state. That is, map it from one Observable to another. Let's go with the simplest version for now, have a single constant value as the state, and then map it to the same fixed output we used previously:
var updates = Rx.Observable.return(1);
var domStream = updates.map(function (update) {
return makeBox('three');
});
var clicks = domStream.flatMapLatest(function(dom) {
return Rx.Observable.fromEvent(dom, 'click');
});
This should not be any different from the previous version. However, this outputs no events, no matter what you do.
What exactly is going on here? Did I misunderstand some fundamental concept of Rx, or what? I've run into some issues with hot vs cold Observables, but that seems unrelated in this minimal case. So, I'm kind of out of ideas. Can anyone enlighten me?
Sorry to tell you but it is a Hot vs Cold issue.
It is a subtle issue, but the difference between
Rx.Observable.return(makeBox('two'))
and
Rx.Observable.return(1).map(function() {return makeBox('three'); })
Is that the first returns a constant every time you subscribe to it, that is,
a box that you created initially. The second returns a new box every time the Observable is subscribed to, this causes a problem since you actually subscribe to the domStream variable twice, you are creating two instances of Box three, one which has event handlers but isn't shown and one that does not and is shown.
The fix is that you either need to use approach 2 or you need to convert the third into a hot stream either by using:
domStream.replay(1).refCount()
Or by using
domStream.publish()
then after all subscriptions are completed:
domStream.connect()

Angular ngGrid Tree Control: Make a round trip on group expand

I am trying to use ngGrid to make somewhat of a "tree-control" which I can build dynamically by calling API's. ngGrid allows for grouping on rows, yet the nature of it requires that all rows be present at the beginning. This is unfortunate for the fact that an API to pull back all generation data for a File Integrity Monitoring system would be insanely slow and stupid. Instead, I wish to build the "tree" dynamically on the expansion of each generation.
I am trying to inject children (ngRows) into a group-row (ngAggregate) on a callback, yet I do not think that I am calling the correct constructor for the ngRows for the fact that the rows are ignored by the control
Through the use of the aggregateTemplate option on the gridOptions for ngGrid, I have been able to intersept the expansion of a group quite easily.
(maybe not easily, but still)
I've replaced the ng-click of the default template:
ng-click="row.toggleExpand()"
with:
ng-click="$parent.$parent.rowExpanded(row)"
I know that it's a bit of a hack, but we can get to that later. For now, it gets the job done.
The way that I discovered how to work my way up the $scope to my rowExpanded function was by setting a breakpoint in ngGrid's "row.toggleExpand" function and calling it from the template as so:
ng-click="row.toggleExpand(this)"
Once I retrieve the group I want, I call an API to get the children for said group. I then need to make the return as children of the row. I decided to do this by calling ngGrid's ngRow factory:
row.children = [];
for(var i = 0; i < childData.length; i++)
{
row.children[row.children.length] = row.rowFactory.buildEntityRow(childData[i], i);
}
row.toggleExpand();
... yet this does not appear to be working. The rows are not showing up after I do the expand! Why won't my rows show up?
Here's my current Plunker!
By the way
I've placed a debugger statement within the group-expand callback. As long as you have your debugger open, you should catch a breakpoint on the expansion of a group.
Thanks everybody!
I found my answer, I'm an idiot....
I got this control working, and then realized that it was a total hack, that I could have used the control the way it was meant to be used and it would have worked much better, had much better work-flow, and it would have saved me an entire day of development. If you are wondering how you use the control this way, the answer is that you don't.
I got the stupid thing to work by updating my data structure after the round trip and forcing the grid to refresh, pretty obvious. I had to set the grid options so that groups were always expanded and I had to control the collapser icon logic myself, outside of ngGrid. I never called row.toggleExpand. I also hid any rows with null values by a function call within an ng-if on my rowTemplate. After all that was said and done, I put my foot in my mouth.

AngularJS - Continuosly watch over a collection

I'm trying to build a real watcher for a collection in my app and, at first, I thought that Angular would provide me everything I needed.
I mean, I had the $watch, both shallow and deep. and the $watchCollection, a $digest cycle that loops over my $scope-exposed variables through the dirty checking mechanic and triggers all the watchers...
Great! What else could I need?
Wrong!
Turns out that $watchCollection gets triggered only at the first change of the watched variable...
And that's it for the mighty watchers... why???
After a reality check, I realized that I needed some kind of horrible loop to check this collection, or else I had to implement some sort of callback to do this, whenever the var gets modified.
Anybody knows how this can be done in the cleanest way possible?
Important note:
I don't why, but it seems that some horrific bug in my code was gnawing my ankles...
Now that I've fixed it, both $watchCollection(expr, foo) and $watch(expr, foo, true) works as expected...
I was mislead by this SO post , in which an user comments:
[...] I don't see anything in your code that makes the subsequent requests (to check for new messages). Where does that happen?
I took his comments as proof of my hypothesis... my bad!
I'm leaving this question as a memento
I'm pretty sure a regular $watch will do this if you utilize the 3rd parameter (objectEquality). This will check if the objects are equal and not just references.
So, you can use something like this:
$scope.$watch('prop', function(value) {
// do something
}, true);
The true value tells Angular to compare objects instead of references.
The documentation for this feature is with scope.
below solution is bit of an hacking solution and should only be used if $watchCollection does not work. rather than watching on the array, watch on json
$scope.$watch(function() {
return angular.toJson($scope.array);
},
function() {
// watch logic
}
I am using above solution to watch on multiple arrays like below:
$scope.$watch(function() {
return JSON.stringify([$scope.array1, $scope.array2]);
},
function() {
// watch logic
}
you can user either of JSON.stringify or angular.toJson.

Categories

Resources