AngularJS: my Accordion expands, but does not collapse - javascript

I have an accordion object in AngularJS that is data driven. Here is what my html looks like:
HTML:
<div class="field-accordion" ng- if="field.fieldAccordion">
<ul class=a ccordion>
<li ng-repeat="fieldAccordion in field.fieldAccordion" ng- click="accordion.current = fieldAccordion.fieldName">
{{ fieldAccordion.fieldName }}
<ul ng-show="accordion.current == fieldAccordion.fieldName">
<li ng-repeat="fieldSub in fieldAccordion.fieldSub">
{{fieldSub.fieldName}}
</li>
</ul>
</li>
</ul>
</div>
Then in my JS file, I simply use it like this:
JS:
app.controller("myCtrl", function($scope) {
$scope.mySettings({
Header: '',
Title: '',
Img: '',
fieldAccordion: [{
// "this is my accordion list"
This works great for me. Whenever I click on the parent, it expands. The problem that I'm having is I can't get it to collapse. I am also trying to do it while keeping everything data driven like it is now. Is there anything I can change in my HTML to allow the text to collapse after expanding ?
Thank you

So the problem is because of a new scope being created, what you need to know is ng-if and ng-r creates a new scope, I checked your code and the ng-repeat is the root cause of the issue! hence the variable is not updating and the accordion is not toggling. Here is a simple way to update the parent controller using the $parent.
I have created a mockup highlighting the issue. So instead of updating accordion.current which will update the scope created by the ng-if you can just give it as $parent.accordion.current in the ng-click which will update the parent scope and the variable will be available for the ng-show and the accordion toggling works as expected.
Below is a working snippet, let me know if this fix solved your issue!
var app = angular.module('myApp', []);
app.controller('MyController', function MyController($scope) {
$scope.field = {
Header: '',
Title: '',
Img: '',
fieldAccordion: [{
fieldName: "one",
fieldSub: [{
fieldName: "oneone"
}]
}, {
fieldName: "two",
fieldSub: [{
fieldName: "twotwo"
}]
}]
};
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller='MyController' ng-app="myApp">
<div class="field-accordion" ng-if="field.fieldAccordion">
<ul class="accordion">
<li ng-repeat="fieldAccordion in field.fieldAccordion" ng-click="$parent.accordion.current = fieldAccordion.fieldName">
{{ fieldAccordion.fieldName }}
<ul ng-show="accordion.current == fieldAccordion.fieldName">
<li ng-repeat="fieldSub in fieldAccordion.fieldSub">
{{fieldSub.fieldName}}
</li>
</ul>
</li>
</ul>
</div>
</div>

Related

ng-model with unique id inside ng-repeat ionic 1 angularjs

I want to create like button with ionic 1/angular js.
The problem is when like buttons doesnt work with ng-model within ng-repeat because all ng-model have same names.
This is the html so far.
<div ng-repeat="item in items">
<div class = "item item-text-wrap">
<button class="button button-block icon ion-thumbsup" ng-model="islike" ng-class="islike== true?'button-on':'button-light'" ng-click="changeLikeState(item.id);"> Like</button>
</div>
</div>
I want to initialize the button state if a user already like this item or not. If user already like the item, islike is set to true otherwise islike set to false.
I want to create a function to check like state with $http.get call and set the islike based on the results
The problem is there are multiple item with same islike, How do you set item.id within ng-model="islike" to identify specific button?
Is this the correct approach ? How to solve this in ionic/angularjs?
You don't need an ng-model for buttons. Simply pass the object into your function with ng-click to trigger any changes there. Here is a demo:
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.items = [
{"islike":false,"id":1},
{"islike":false,"id":2},
{"islike":false,"id":3},
];
$scope.changeLikeState = function(item) {
item.islike = !item.islike;
console.log(item.id);
}
});
.button-on {
color: blue;
font-weight: bold;
}
.button-light {
color: green;
font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myCtrl">
<div ng-repeat="item in items">
<button ng-class="{'button-on':item.islike,'button-light':!item.islike}" ng-click="changeLikeState(item);">
Like
</button>
</div>
</div>
as pointed above in the comment islike should be a property of each item in your object, for example :-
let's assume your collection looks something like
$scope.items = [{ name: 'pan cakes', islike: false },
{ name: 'pea nut butter', islike: true },
{ name: 'pizza', islike: true }]
along with other properties if there's an id associated too, that's more than enough otherwise you can always use the $index in ng-repeat to track the current item in items
your code then may look something like this:-
<div ng-repeat="item in items track by ($index)">
<div class = "item item-text-wrap">
<button class="button button-block icon ion-thumbsup" ng-model="item.islike" ng-class="item.islike == true?'button-on':'button-light'" ng-click="changeLikeState($index);"> Like</button>
</div>
</div>
notice the change track by and instead of islike its not item.islike

How to put class="active" to first element in vuejs for loop

I'm trying to learn vue.js. I'm adding list of elements, I want to add class="active" only to the first element in the for loop. following is my code:
<div class="carousel-inner text-center " role="listbox">
<div class="item" v-for="sliderContent in sliderContents">
<h1>{{ sliderContent.title }}</h1>
<p v-html="sliderContent.paragraph"></p>
</div>
</div>
So the first element should look like something this:
<div class="item active">
<h1>WELCOME TO CANVAS</h1>
<p>Create just what you need for your Perfect Website. Choose from a wide<br>range of Elements & simple put them on your own Canvas</p>
</div>
I'm able to get the data so everything is working perfectly fine.
And following is my script code:
<script>
export default{
data() {
return {
sliderContents: [
{
title: "WELCOME TO CANVAS",
paragraph: "Create just what you need for your Perfect Website. Choose from a wide<br>range of Elements & simple put them on your own Canvas"
},
{
title: "WELCOME TO CANVAS",
paragraph: "Create just what you need for your Perfect Website. Choose from a wide<br>range of Elements & simple put them on your own Canvas"
},
{
title: "WELCOME TO CANVAS",
paragraph: "Create just what you need for your Perfect Website. Choose from a wide<br>range of Elements & simple put them on your own Canvas"
},
{
title: "WELCOME TO CANVAS",
paragraph: "Create just what you need for your Perfect Website. Choose from a wide<br>range of Elements & simple put them on your own Canvas"
}
]
}
}
}
Help me out.
const TextComponent = {
template: `
<p>{{ text }}</p>
`,
props: ['text'],
};
new Vue({
components: {
TextComponent,
},
template: `
<div>
<text-component
v-for="(item, index) in items"
:class="{ 'active': index === 0 }"
:text="item.text">
</text-component>
</div>
`,
data: {
items: [
{ text: 'Foo' },
{ text: 'Bar' },
{ text: 'Baz' },
],
},
}).$mount('#app');
.active {
background-color: red;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title></title>
</head>
<body>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
</body>
</html>
Above is a snippet demonstrating a solution to your problem. Here's an outline of it:
Inside v-for blocks we have full access to parent scope properties. v-for also supports an optional second argument for the index of the current item.
– https://v2.vuejs.org/v2/guide/list.html#Basic-Usage
The v-for directive has a second argument giving you the index of the item.
v-for="(item, index) in items"
Since you want the active class on the first item you can use an expression testing if the index is 0 and bind it to the class attribute.
:class="{ 'active': index === 0 }"
The easiest solution is to check every element if index is equal to 0 and then setting the active class (or the class you needed). Here is the code of class definition:
:class="{ 'active': index === 0 }"
Here is working example of making Bootstrap Tabs:
<ul class="nav nav-tabs tab-nav-right" role="tablist">
<li role="presentation" :class="{ 'active': index === 0 }" v-for="(value, index) in some_array" v-bind:key="index">
{{some_array[index]}}
</li>
</ul>
Also, if you have multiple classes you can mix them like this:
:class="['tab-pane fade', (index === 0 ? 'active in' : 'something_else')]"
To put active class to first element in loop, basically, you are trying to programattically control the class via VuejS.
VueJS allows you to bind the class of the anchor tag (say, but it could even be an li tag) directly to the index of the li element so that when the vuejs variable bound to the index changes, the class also changes. Check these two links for more details
This is the crux of the solution
:class="{current:i == current}
available on the fiddle below and another post that explains in blog format how class of an li element can be dynamically controlled in vuejs
https://jsfiddle.net/Herteby/kpkcfcdw/
https://stackoverblow.wordpress.com/2021/04/03/how-modern-javascript-makes-click-simulation/

Knockout foreach dynamically set ID

I just started using knockout and having some problems using foreach.
I need to dynamically populate a list with "collapsing" div.
I cant figuer out how to set the
"data-target=""" with the according div id.Is there a kind of $index as in Angular.How do i declare it inside of the data-target?
Thank for the help.
<ul class="nav navbar-nav side-nav">
<li data-bind="foreach: { data: categories, as: 'category' }">
<div id="??" class="collapse">
<h1>Some text</h1>
</div>
</li>
</ul>
Do it within the data-bind:
<a href="javascript:;" data-toggle="collapse" data-bind="attr: { 'data-target': ... }">
<div class="collapse" data-bind="attr: { id: ... }">
Knockout does have a $index context property, too:
<div data-bind="attr: { id: 'foo' + $index() }">
What's data-target being used for? I don't think you not need that in the first place.
If you want to toggle a section, I recommend using a more natural way to solve this. In the context of knockout this means: write a binding handler that encapsulates the behavior you want.
Expressed in simplest terms: You want to click on something and it should toggle something else. For example the visibility of the following <h1>
The minimal thing to do would be: Toggle a CSS class and use that to influence visibility.
Here is a binding handler that switches a CSS class on and off. Together with the simplest CSS you get collapse/expand behavior.
ko.bindingHandlers.toggleClass = {
init: function (element, valueAccessor) {
ko.utils.registerEventHandler(element, 'click', function () {
var cssClass = valueAccessor(),
shouldHaveClass = element.className.indexOf(cssClass) < 0;
ko.utils.toggleDomNodeCssClass(element, cssClass, shouldHaveClass);
});
}
}
ko.applyBindings({
categories: [
{title: 'Category A'},
{title: 'Category B'},
{title: 'Category C'},
{title: 'Category D'}
]
});
.collapsed+.collapse {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: categories">
<li>
<div class="collapse">
<h1>Some text</h1>
</div>
</li>
</ul>
Of course you can use all kinds of more advanced techniques, but the principle would stay the same. Define a behavior, attach that behavior to an element via a binding handler.

AngularJS: Is there a better way to achieve this than the one specified?

I use a template that generates a Bootstrap tab layout. Like below:
<div class="a">
<ul class="nav nav-bar nav-stacked" role="tabs">
<li></li>
...
</ul>
<div class="tab-content">
<div id="home">abc</div>
...
</div>
</div>
Now this is pretty simple and straightforward tab navigation that can be hardcoded and achieved.
I have a dynamic ng-repeat on the ul's li and the tab-content's divs.
The JSON that I get from the REST service is something that contains the data for the tabs and the content to be displayed inside the tab-content within a single object. For eg:
{
"0": "a": [{ // a- tab data
"0": "abc", // abc - data to be displayed inside the tab-content
"1": "xyz"
}]
...
}
With such a JSON hierarchy, I basically need to ng-repeat twice. Once for the ul li and once for the tab-content as each object of the tab contains the data related to it.
So what I have done so far is:
<div class="a">
<ul class="nav nav-bar nav-stacked" role="tabs">
<li ng-repeat="foo in data">{{foo.name}}</li>
...
</ul>
<div class="tab-content">
<div id="home" ng-repeat="foo in data">
<p ng-repeat="f in foo.desc">{{f}}</p>
</div>
...
</div>
</div>
EDIT:
So my question is, is there a smarter way to achieve this using a single ng-repeat rather than doing "foo in data" twice?
Sorry if my question isn't clear.
Use ng-include with $templateCache rather than ng-repeat. For example:
var app = angular.module('foo', []);
function foo($templateCache)
{
var model =
{"data":
[
{"name": "Stack",
"desc": ["Exchange", "Overflow"]
}
]
},
cursor, i, bar = baz = "";
for (i = 0; i < model.data.length; i++)
{
bar = bar.concat("<li>", model.data[i].name,"</li>");
baz = baz.concat("<li>", model.data[i].desc.join().replace(/,/g,"</li><li>") );
}
$templateCache.put('name', bar);
$templateCache.put('desc', baz);
}
app.run(foo);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="foo">
<ul ng-include="'name'"></ul>
<ul ng-include="'desc'"></ul>
</div>
References
Tweak the Angular Test by Controlling the Template
AngularJS in a Groovy World
AngularJS Source: templateRequestSpec.js
AngularJS Source: ngIncludeSpec.js

angularjs accordion load data when opening

We are making use of the angularjs accordion, however, our accordion is fairly big, and contains a lot of data.
What we are looking at doing is loading the data when the accordion opens. ie dont render the internal content, until a user click on the accordion header.
Below is how we are using the accordion at the moment:-
<accordion close-others="false">
<accordion-group heading="{{result.ReceivedDateTime}}{{result.OrderName}}" ng-repeat="result in resultsbyorder">
<table id="tbl" class="table table-striped table-bordered" ng-repeat="Grid in result.Results">
<tr ng-repeat="item in Grid" ng-show="$first">
<th ng-repeat="somat in item">
{{somat.ColumnHeader}}
</th>
</tr>
<tr ng-repeat="item in Grid">
<td ng-repeat="somat in item">
{{somat.ColumnValue}}
</td>
</tr>
</table>
</accordion-group>
</accordion>
as a side note, is it possible to only render the accordion headers that are displayed on the screen, then render the remaining on scrolling?
I think what you are better off doing is to compile it once when it is opened the first time.
That way, you don't have to compile the same content again, even though it was compiled before the last time the accordion group was opened.
Over the top of my head, I can think of using the ng-switch directive. As you must be aware, this directive allows to display DOM elements conditionally, based on the case variable.
However, where this directive differs from the ng-show or ng-hide directive is that the contents within the case statement are not compiled until the corresponding case variable is matched.
Thus, what you could do is have the following setup in your HTML document:
<accordion close-others="oneAtATime">
<accordion-group is-open="isopen">
<accordion-heading>
Accordion Heading
</accordion-heading>
<div ng-switch on="isopen">
<div ng-switch-when="true">
<!-- Content that should be compiled when the accordion is opened
goes here -->
</div>
</div>
</accordion-group>
</accordion>
A demonstration of the same can be found in this plunkr.
Also, I believe ng-if directive also achieves the same effect. So, you can use either of the directives to achieve your requirements.
I am doing this very thing at the moment and have found that Angular likes using a collection (even if it's just a single element) to make the accordion, to wit if you do, you can bind the is-open to an element on that group and then setup a watch on that group in the controller. Consider the following (think of putting an ajax call to get your data instead of logging to console):
var ngtestapp = angular.module("ngtestapp", ['ui.bootstrap']);
ngtestapp.controller("ngTestController", function ($scope) {
$scope.userComments = [
{
RowId: 123,
DateCreated: new Date(2015, 1, 2, 0, 0, 0, 0),
CreatedBy: 564,
Message: 'Carpe Dieum',
CreatedByDisplayName: 'Daniel Graham'
},
{
RowId: 124,
DateCreated: new Date(2015, 1, 5, 0, 0, 0, 0),
CreatedBy: 562,
Message: 'That was awesome',
CreatedByDisplayName: 'Peter Griffin'
}
];
$scope.feedbackGroup = [{ title: "User Comments", open: false }];
$scope.$watch('feedbackGroup', function (feedbackGroup) {
if (feedbackGroup[0].open) {
console.log("opened feedback group.");
} else {
console.log("closed feedback group.");
}
}, true);
$scope.userTaskNote = "user task test note";
});
.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }
<html lang="en" ng-app="ngtestapp">
<head>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.12.1/ui-bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.12.1/ui-bootstrap-tpls.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
</head>
<body>
<div ng-controller="ngTestController">
<div class="container-fluid">
<accordion close-others="true">
<accordion-group ng-repeat="group in feedbackGroup" heading="{{group.title}}" is-open="group.open">
<ul class="list-group">
<li class="list-group-item" ng-repeat="comment in userComments track by $index | orderBy:comment.DateCreated">
<span class="badge" ng-bind="comment.DateCreated | date:'shortDate'"></span>
<strong ng-bind="comment.CreatedByDisplayName">Username</strong> <p ng-bind="comment.Message">comment</p>
</li>
</ul>
</accordion-group>
<accordion-group is-open="false" heading="Other">
<textarea class="form-control input-sm" ng-model="userTaskNote" placeholder="Add a task note here..."></textarea>
</accordion-group>
</accordion>
</div>
</div>
</body>
</html>

Categories