In my Angular app I have the following menu:
As you can see I have items (that are a elements) and a checkbox and a label in each of them:
<span class="caption">RAM</span>
<a class="item">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden">
<label>4 GB</label>
</div>
</a>
<a class="item">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden">
<label>8 GB</label>
</div>
</a>
<a class="item">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden">
<label>16 GB</label>
</div>
</a>
How should I add (click) to every item to correctly handle event capturing so if user click on label or on whole item I get the related checkbox selected?
...Or do you know a better way to reach what I mean?
To make sure that clicking on the label toggles the checkbox, include the input element inside of the label (as explained in MDN):
<a class="sub-item item">
<div class="item-checkbox">
<label><input type="checkbox" tabindex="0" class="hidden">4 GB</label>
</div>
</a>
If you also want the label to fill the anchor element, define the CSS as shown below. With this styling in place, clicking on the anchor will toggle the checkbox.
.ui.secondary.menu a.item {
padding: 0px;
}
div.item-checkbox {
width: 100%;
height: 100%;
}
div.item-checkbox > label {
display: block;
padding: .78571429em .92857143em;
}
div.item-checkbox > label > input {
margin-right: 0.25rem;
}
See this stackblitz for a demo.
If you can place all the checkboxes in a container, you can set a single click event listener on the container, and event.target will give you the clicked element and previousElementSibling will select the sibling input.
function doSomething() {
const selectedInput = event.target.previousElementSibling;
selectedInput.checked = selectedInput.checked? false : true;
}
But in case there is a probability that you document structure changes in the future, say for example, if other elements get in between the input and the label, or their order changes, then the sibling selector will fail. To solve this you can use a parent selector instead and select the chechbox input element inside of it.
document.getElementById('container').addEventListener('click', doSomething);
function doSomething() {
const selectedElement = event.target.parentElement.querySelector('input[type="checkbox"]');
selectedElement.checked = selectedElement.checked? false:true;
}
<div id= 'container'>
<span class="caption">RAM</span>
<a class="item">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden" value="4 GB">
<label>4 GB</label>
</div>
</a>
<a class="item">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden" value="8 GB">
<label>8 GB</label>
</div>
</a>
<a class="item">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden" value="16 GB">
<label>16 GB</label>
</div>
</a>
</div>
First of all you should use *ngFor to list all your options:
html:
<a class="item" *ngFor="let choice of checks; let i=index">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden" [value]="choice.value" (change)="onCheckChange($event)">
<label>{{choice.title}}</label>
</div>
</a>
component:
public checks: Array<choices> = [
{title: '4 GB', value: '4'},
{title: "8 GB", value: '8'},
{title: "16 GB", value: '16'}
];
public onCheckChange(event) {
// do some things here
}
Further, you should use Reactive Forms to validate your choices:
https://angular.io/guide/reactive-forms
You have to create a boolean array and bind this to your checkboxes.
.ts file
myArray: any[] = [
{
"size": "2GB",
"value":false
},
{
"size": "4GB",
"value":false
},
{
"size": "8GB",
"value":false
}
]
.html file
<div *ngFor="let data of myArray">
<a class="item">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden" [(ngModel)]="data.value" (ngModelChange)="changeHandler()">
<label>{{data.size}}</label>
</div>
</a>
</div>
Working demo : link
You could do something like that:
const ramOptions = [
{
id: 1,
size: '4 GB'
},
{
id: 2,
size: '8 GB'
},
{
id: 3,
size: '16 GB'
}
]
And in html:
<a class="item" *ngFor="let option of ramOptions">
<div class="item-checkbox">
<input type="checkbox" tabindex="0" class="hidden" (change)="onSelect(option.id)">
<label>{{option.size}}</label>
</div>
</a>
If in the .js/.ts file, you can create an interface to define the handler. Just as:
onSelect(id: string) {
...
}
Related
What I need to do is to display the filtered array in the DOM once I click on any value from the dropdown. The return from filtered function is right but I couldn't update the DOM.
HTML CODE
This is my side dropdown list that I take the value from
<div class="col-md-3">
<div class="widget">
<h4 class="widget-title">Sort By</h4>
<div>
<select class="form-control" (change)="selectChangeHandler($event)">
<option value="Man">Man</option>
<option value="Women">Women</option>
<option value="Accessories">Accessories</option>
<option value="Shoes">Shoes</option>
</select>
</div>
</div>
</div>
Table that shows the results on DOM take the value from above dropdown list to be able to show them by category. Unfortunately the DOM doesn't change (I used NGX pagination library), please see my TS code that sends the filtered array to the loop? So, I don't understand why it didn't update.
<div class="col-md-9">
<div class="row" id="top">
<div class="col-md-4" *ngFor="let item of collection | paginate: { itemsPerPage: 100, currentPage: p ,id: 'foo' }">
<div class="product-item">
<div class="product-thumb">
<span class="bage">Sale</span>
<img class="img-responsive" src="https://via.placeholder.com/150" alt="product-img" />
<div class="preview-meta">
<ul>
<li>
<span data-toggle="modal" data-target="#product-modal">
<i class="fas fa-search"></i>
</span>
</li>
<li>
</i>
</li>
<li>
<i class="fas fa-shopping-cart"></i>
</li>
</ul>
</div>
</div>
<div class="product-content">
<h4>{{item.name}}</h4>
<p class="price">{{item.price}}</p>
</div>
</div>
</div>
Type Script Code
export class AllproductsComponent implements OnInit {
allProducts:any[]=[]
filteredData=[...this.allProducts]
p: number = 1;
collection: any[] = this.filteredData;
constructor(private _allproducts:ProductService) {
console.log(this.collection);
}
ngOnInit(): void {
this.getAllProducts()
}
getAllProducts():any{
this._allproducts.getAllproducts().subscribe(res=>{
console.log(res.data);
this.allProducts = res.data
this.collection =res.data
})
}
pageChanged(pee:number){
document.getElementById("top").scrollIntoView()
}
selectChangeHandler(value:any){
console.log(value.target.value);
console.log(this.filteredData);
this.filteredData = this.allProducts.filter(key=>{
if(value.target.value === "Man"){
if(key.price > 20)return this.allProducts
}else if(value.target.value === "Women"){
if(key.price < 20) return this.allProducts
}else {
return this.allProducts
}
})
console.log(this.filteredData);
}
}
I appreciate your help, thanks a lot.
allProducts:any[]=[]
filteredData=[...this.allProducts]
collection: any[] = this.filteredData;
you are initializing data before it is called from your api , so it's normal it will never work. You have to reinitialize it inside your method
this._allproducts.getAllproducts().subscribe(res=>{ ..});
component.ts
//all imports are done
ngOnInit() {
this.list = {
'eatList': [{
'class': 'Fruits',
'color': ['Red', 'White', 'Black'],
'imageSrc': ['/assets/images/fruit/red-fruit.png',
'/assets/images/fruit/black-fruit.png',
'/assets/images/fruit/white-fruit.png'],
'weights' : [60, 50]
},
{
'modalName': 'Vegetable',
'color': ['Green', 'Black'],
'imageSrc': ['/assets/images/veg/black-veg.png', /assets/images/veg/green-veg.png'],
'weights' : [40, 50]
}
component.html
<div>
<div *ngFor="let eats of list.eatList" "
>
<h3>{{eats.class}}</h3>
<img src="{{eats.imageSrc[0]}}" alt="image" />
<div>
<ul>
<li *ngFor="let weight of eats.weights;let i = index;">
<input type="radio" [value]="weight" (change)="handleChange($event)" [attr.id]="i"
name="weight" />
<label for="{{i}}">{{weight}}</label>
</li>
</ul>
</div>
</div>
</div>
When I click on the input radio button I need the particular item to be selected in some color.
added the change event but unable to change it.
You can access the element by using the event passed to the function, since it's a click event, the event has a target, which is the input element, so you can do something like:
handleChange(event) {
event.target.style.color = "yellow";
}
This change won't be noticeable, I just gave it as an example, since you didn't specify what does the following means:
particular item to be selected in some color
If you want to change the div for the eats element, you can do something like the following on the template:
<div>
<div *ngFor="let eats of list.eatList" [style.background]="eats.backgroundColor"
>
<h3>{{eats.class}}</h3>
<img src="{{eats.imageSrc[0]}}" alt="image" />
<div>
<ul>
<li *ngFor="let weight of eats.weights;let i = index;">
<input type="radio" [value]="weight" (change)="handleChange($event, eats)" [attr.id]="i"
name="weight" />
<label for="{{i}}">{{weight}}</label>
</li>
</ul>
</div>
</div>
</div>
and in your component:
handleChange(event, parent) {
parent.backgroundColor = "yellow";
}
I've made an assumption here that eats is an object. Also, you can change the binding to be for a class (using ngClass) and make a lot of css changes that way
I have a code block here that is 90% working. The idea is that I have an autocomplete (searches via axios with a LIKE condition on the database but I simplified the result set here). When you type in the input, it searches and returns matching results in a dropdown. If you select an option, it replaces the search text in the input with the actual selected value. Then if you click to add another zone it clones the divs with the input.
The issue is that when I create another div and start searching in the input it glitches and then it also triggers the dropdown on both divs (or more if you add more than two). But it doesn't actually allow text to be entered in the newly added zones.
How can I fix this so that each zone actually has it's own input and results dropdown so that I can send something on save that has distinct values for each div?
new Vue({
components: {},
el: "#commonNameDiv",
data() {
return {
searchString: [],
results: [],
savedAttributes: [],
cards: []
}
},
methods: {
autoComplete() {
this.results = [];
console.log(this.searchString);
if (this.searchString.length > 2) {
this.results = [
{attribute_value:"apple"},
{attribute_value:"banane"}
]
}
},
saveAttribute(result) {
this.savedAttributes = [];
console.log('cool');
this.savedAttributes.push(result.attribute_value);
console.log('here is the attribute');
console.log(this.savedAttributes);
this.searchString = result.attribute_value;
this.results = [];
},
addCard: function() {
this.cards.push({
index: ''
})
}
}
})
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="commonNameDiv">
<div class="uk-grid">
<div class="uk-width-2-10" >
<input size="4" type="text" name="mapNumber">
</div>
<div class="uk-width-6-10">
<input style="width:100%" type="text" placeholder="what are you looking for?" v-model="searchString" v-on:keyup="autoComplete" class="form-control">
<div class="panel-footer componentList" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results">
<a v-on:click="saveAttribute(result)">{{ result.attribute_value }}</a>
</li>
</ul>
</div>
</div>
<div class="uk-width-2-10" style="border: 1px solid black; height:50px; width: 50px; margin: 0 auto;" >
</div>
</div>
<div v-for="(card,index) in cards" class="uk-grid">
<div class="uk-width-2-10">
<input size="4" type="text" name="mapNumber">
</div>
<div class="uk-width-6-10">
<input style="width:100%" type="text" placeholder="what are you looking for?" v-model="searchString[index]" v-on:keyup="autoComplete" class="form-control">
<div class="panel-footer componentList" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results">
<a v-on:click="saveAttribute(result)">#{{ result.attribute_value }}</a>
</li>
</ul>
</div>
</div>
<div class="uk-width-2-10" style="border: 1px solid black; height:50px; width: 50px; margin: 0 auto;">
</div>
</div>
<div style="height: 35px;">
</div>
<div>
<a v-on:click="addCard">Add another zone</a>
</div>
</div>
All your variables and/or properties need to be unique, currently they are not. The easiest way would be in my mind to just store all that you need inside the card object you are using. When you are done, you can just extract the data you need from your array.
Template where we have removed the first div you have, let's just push an empty card object to the array. Our card object will look like this:
{
index: "",
value: "" // the end value
results: [] // the search results will be stored here
}
So in template we use value as v-model for the input and display results for the user to pick their value. Also I suggest you use in all v-for iterations a key. Here I use the index, but that is really not that efficient! But anyway, so the stripped down version of template would look like:
<div v-for="(card, i) in cards" :key="i">
<div>
<input
placeholder="what are you looking for?"
v-model="card.value"
v-on:keyup="autoComplete($event, card)"
>
<div v-if="card.results.length">
<ul>
<li v-for="(result, i) in card.results" :key="i">
<a v-on:click="saveAttribute(result, card)">#{{ result.attribute_value }}</a>
</li>
</ul>
</div>
</div>
</div>
<div>
<a v-on:click="addCard">Add another zone</a>
</div>
The methods would be:
methods: {
autoComplete(ev, card) {
if (ev.target.value.length > 2) {
// here would be actual results...
card.results = [
{ attribute_value: "apple" },
{ attribute_value: "banane" }
];
}
},
saveAttribute(result, card) {
card.value = result.attribute_value;
},
addCard() {
this.cards.push({
index: "",
value: "",
results: []
});
}
}
and lastly, like mentioned, call addCard in a life cycle hook. Here I use created:
created() {
this.addCard();
}
I have three views which posts a JSON-object to backend to generate a PDF. The first in the hierarchy works, not the two others. The code is practically identical, and uses the same method to programmatically add a hidden input-field with the JSON-data.
I cannot for the life of me figure out what the problem is.
First i thought Knockout was creating the problem since the form-elements are added in a foreach-loop, in a AJAX-success-event, but every view is identical in this respect.
I've tried all variations of renaming, also I've tried to create the form and input programatically, appending them to the body and submitting. Still only the first works.
WORKING VIEW:
<div data-bind="visible: rep.messages().length > 0">
<div data-bind="foreach: rep.messages">
<div id="error" data-bind="css: color">
<div><span data-bind="css: icon"></span></div>
<div><div data-bind="html: text"></div></div>
<div>
<form action="/createPDF.html" method="POST" id="individlonpdf" name="loneandringar" target="_blank">
<span onclick=""></span>
Skriv ut
</form>
</div>
</div>
</div>
</div>
NOT WORKING VIEW:
<div data-bind="visible: rem.newEmployeeMessages().length > 0" id="rem-msg-area">
<div data-bind="foreach: rem.newEmployeeMessages">
<div id="error" data-bind="css: color">
<div><span data-bind="css: icon"></span></div>
<div><div data-bind="html: text"></div></div>
<div>
<form action="/createPDF.html" method="POST" id="ateranstallpdf" name="avanmalan" target="_blank">
<span onclick=""></span>
Skriv ut
</form>
</div>
</div>
</div>
</div>
JAVASCRIPT
Each click event-method is then directed towards the same print-method
Not-working:
rem.print = function(){
mainPdfPrint("ateranstallpdf", data, "avanmalan");
}
Working:
rep.print = function () {
mainPdfPrint("individlonpdf", data, "loneandringar");
}
The main-method:
function mainPdfPrint(formid, datasource, name, stringified) {
formid = "#" + formid;
var url = $(formid).attr("action");
var data = stringified ? datasource : JSON.stringify(datasource);
var inp = $("<input>", { "value": data, "name": name, "type": "hidden" });
if ($(formid + " input:hidden").length) {
$(formid + " input:hidden").remove();
}
$(formid).append(inp);
IK.global.logAnalytics(url);
document.forms[name].submit();
}
Both working and non-working form-elements are part of the document.forms-collection as they should be.
Any suggestions would be greatly appreciated!
UPDATE
After suggestions here, I've tried a different approach:
<div class="col-xs-12" id="" data-bind="visible:messages().length > 0">
<div data-bind="foreach: messages">
<div class="col-xs-12" id="error" data-bind="css:color">
<div class="col-xs-12 msg-templates">
<div class="col-xs-1 no-pad-l info-sign">
<span class="glyphicon" data-bind="css:icon"></span>
</div>
<div class="col-xs-9 no-pad-l">
<div class="col-xs-12 no-pad-lr msg" data-bind="html:text"></div>
</div>
<div class="col-xs-2 no-pad-lr text-right print">
<form class="col-xs-12 no-pad-l" action="/foretag/individkort/skapaPdfAvanmalan.html" method="POST" id="avanmalanpdf" name="avanmalan" target="_blank">
<span class="glyphicon glyphicon-print" onclick=""></span>
<input type="hidden" name="avanmalan" data-bind="value:ko.toJSON($root.printableEmployee)" />
Skriv ut
</form>
</div>
</div>
</div>
</div>
</div>
The problem still persists however, but setting a delay in the print-method triggers the submit-event:
self.print = function () {
setTimeout(function () {
document.forms.avanmalan.submit();
}, 600);
}
Your approach indicates that you seem to be not really familiar with knockout and try to do too much in a "jQuery" kind of way.
With knockout, your user interface is a function of your view model. All changes in the DOM have to be a result of changes in the underlying data. The task of manually adding input fields with JS code is not part of this approach.
In the following, there is no custom code that modifies the DOM. Everything is done by knockout.
var vm = {
rep: {
messages: [
{ color: '', icon: '', text: '<b>Message 1</b> - Text', data: 'some data here 1' },
{ color: '', icon: '', text: '<b>Message 2</b> - Text', data: 'some data here 2' },
{ color: '', icon: '', text: '<b>Message 3</b> - Text', data: 'some data here 3' }
]
},
catchPost: function () {
// dummy function to prevent an actual form POST
console.log("...would send the following data to the server:");
console.log(ko.toJSON(this.data, null, 2));
return false;
}
};
ko.applyBindings(vm);
button.submit {
border: 0px none;
padding: 0;
margin: 0;
text-decoration: underline;
background-color: transparent;
color: blue;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div data-bind="with: rep">
<div data-bind="foreach: messages">
<div id="error" data-bind="css: color">
<div><span data-bind="css: icon"></span></div>
<div><div data-bind="html: text"></div></div>
<div>
<form action="/createPDF.html" method="POST" target="_blank" data-bind="submit: $root.catchPost">
<input type="hidden" name="loneandringar" data-bind="value: ko.toJSON(data)">
<button class="submit" id="print-tgl">Skriv ut</button>
</form>
</div>
</div>
</div>
</div>
There is no need for any custom code in such a simple scenario. Unless you have an explicit dependency on jQuery UI (or a similar library), I suggest you remove jQuery from your application altogether. You can use a dedicated Ajax library if you require it - such as reqwest - but generally speaking, jQuery is more of a hindrance than a help in knockout applications.
please am very new new to angular and am trying to do something i don't know if its possible. I have a json data and i want to render the option on click of an item. What i want to achieve is to show models of a phone on click of the name, and on click of a phone model show all the phone parts. But whenever i click on a phone i get undefined in my console.
my app.js file
(function(){
var app=angular.module('repairshop',[]);
app.controller('phoneController',function($scope){
this.phones = [
{
name: 'Apple',
model: [{ name: 'Iphone 5'}, {name: 'Iphone 6'},{name: 'Iphone 6s'}],
part: [{name:'ear phones'},{name:'external speakers'},{name:'Screen Guard'},{name:'Charger'}]
},
{
name: 'Samsung',
model: [{ name: 'S4'}, {name: 'S5'},{name: 'S6'}],
part: [{name:'ear phones'},{name:'external speakers'},{name:'Screen Guard'},{name:'Charger'}]
},
{
name: 'Nokia',
model: [{ name: 'Lumia'}, {name: '3310'},{name: 'Asha'}],
part: [{name:'ear phones'},{name:'external speakers'},{name:'Screen Guard'},{name:'Charger'}]
},
{
name: 'Blackberry',
model: [{ name: 'Passport'}, {name: 'Touch 10'},{name: 'Asha'}],
part: [{name:'ear phones'},{name:'external speakers'},{name:'Screen Guard'},{name:'Charger'}]
}
];
$scope.loadModels=function(phone)
{
var phone=phone.name;
console.log (phone);
}
});
})();
My Html View
<div class="phones_wrapper row">
<!--begin container-->
<div class="container">
<form class="" action="#" method="post" ng-controller="phoneController as phone" >
<div class="row">
<div class="col-md-3 phone_display center" ng-repeat="p in phone.phones">
<label>
<input type="radio" ng-click="loadModels(phone)" ng-model="phone.name" name="phone" ng-value="{{p.name}}" />
<img src="http://placehold.it/200x200">
</label>
<p class="text text-center phone_name">{{p.name}}</p>
</div>
</div>
</form>
</div>
<!--begin container-->
</div>
Here is a simple example plnkr on how you can click on the list of the items you describe to view more details such as models and parts.
<body ng-controller="MainCtrl">
<ul>
<li ng-repeat="phone in phones" ng-click="showModels = true">{{phone.name}}
<ul ng-show="showModels">
<li ng-repeat="model in phone.model" ng-click="showParts = true">{{model.name}}
<ul ng-show="showParts">
<li ng-repeat="part in phone.part">
{{part.name}}
</li>
</ul>
</li>
</ul>
</li>
</ul>
</body>
Basically what you have to do is add repeaters and ng-click events to expand the content. I am pretty sure that from this example you will be able to fix it with your own styling and markup.
Edit:
A better example where you can toggle the viewing of models and parts.
<body ng-controller="MainCtrl">
<ul>
<li ng-repeat="phone in phones" ng-click="showModels = !showModels; $event.stopPropagation()">{{phone.name}}
<ul ng-show="showModels">
<li ng-repeat="model in phone.model" ng-click="showParts = !showParts; $event.stopPropagation()">{{model.name}}
<ul ng-show="showParts">
<li ng-repeat="part in phone.part">
{{part.name}}
</li>
</ul>
</li>
</ul>
</li>
</ul>
</body>
I found it, i changed the loadModels function to
$scope.loadModels=function(phone)
{
phone.parent=phone.name;
console.log (phone);
}
and also changed the phone to p based on #Praneeth Gudumasu recommendation. Thank you
it should be like this
<div class="col-md-3 phone_display center" ng-repeat="p in phone.phones">
<label>
<input type="radio" ng-click="loadModels(p)" ng-model="p.name" name="phone" ng-value="{{p.name}}" />
<img src="http://placehold.it/200x200">
</label>
<p class="text text-center phone_name">{{p.name}}</p>
</div>
you should use the repeating object inside the repeating template and not the scope object(in your case variable "p" not "phone")