I have an Angular 7 app, using CDK Drag-n-Drop to drag and drop rows in a very long list.
What should I do to allow the long list to auto scroll when the dragged item out of the current view?
Any sample code I can refer to?
I have faced the same issue, It happens anytime an outside element is scrollable. This is the open issue - https://github.com/angular/components/issues/16677. - I have slightly modified the solution mentioned in this link.
import { Directive, Input, ElementRef, AfterViewInit } from '#angular/core';
import { CdkDrag } from '#angular/cdk/drag-drop';
#Directive({
selector: '[cdkDrag],[actualContainer]',
})
export class CdkDropListActualContainerDirective {
#Input('actualContainer') actualContainer: string;
originalElement: ElementRef<HTMLElement>;
constructor(cdkDrag: CdkDrag) {
cdkDrag._dragRef.beforeStarted.subscribe( () => {
var cdkDropList = cdkDrag.dropContainer;
if (!this.originalElement) {
this.originalElement = cdkDropList.element;
}
if ( this.actualContainer ) {
const element = this.originalElement.nativeElement.closest(this.actualContainer) as HTMLElement;
cdkDropList._dropListRef.element = element;
cdkDropList.element = new ElementRef<HTMLElement>(element);
} else {
cdkDropList._dropListRef.element = cdkDropList.element.nativeElement;
cdkDropList.element = this.originalElement;
}
});
}
}
Template
<div mat-dialog-content class="column-list">
<div class="column-selector__list">
<div cdkDropList (cdkDropListDropped)="drop($event)">
<div
*ngFor="let column of data"
cdkDrag
actualContainer="div.column-list"
>
</div>
</div>
</div>
</div>
As mentioned here you just need to add cdkScrollable to your list container.
Related
as I'm on my Vue spree (started recently but so far I'm really enjoying learning this framework) couple of questions rised up. One of which is how to post form from multiple components. So before I continue forward I wanted to ask you what are you thinking about this way of structuring and point me in right direction if I'm wrong.
Here it goes.
I'm working on a SPA project using ASP.NET CORE 2.1 and Vue JS Template (with webpack)(https://github.com/MarkPieszak/aspnetcore-Vue-starter) and my project is structured in several containers, something like this:
In my app-root i registered several containers
<template>
<div id="app" class="container">
<app-first-container></app-first-container>
<app-second-container></app-second-container>
<!--<app-third-container></app-third-container>-->
<app-calculate-container></app-calculate-container>
<app-result-container></app-result-container>
</div>
</template>
<script>
// imported templates
import firstContainer from './first-container'
import secondContainer from './second-container'
import calculateContainer from './calculateButton-container'
//import thirdContainer from './third-container'
import resultContainer from './result-container'
export default {
components: {
'app-first-container': firstContainer,
'app-second-container': secondContainer,
// 'app-third-container': thirdContainer,
'app-calculate-container': calculateContainer,
'app-result-container': resultContainer
}
}
</script>
In my first container I'm having several dropdowns and two input fields with my script file where I'm fetching data from API and filling dropdowns and input fields with fetched data.
Something like this ( entered some dummy code for demonstration)
<template>
<div>
<h1>Crops table</h1>
<p>This component demonstrates fetching data from the server. {{dataMessage}}</p>
<div class="form-row">
<div class="form-group col-md-6">
<label for="exampleFormControlSelect1" class="col-form-label-sm font-weight-bold">1. Some text</label>
<select class="form-control" id="exampleFormControlSelect1" v-model="pickedCropType" #change="getCropsByType()">
<option v-for="(cropType, index) in cropTypes" :key="index" :value="cropType.id" :data-imagesrc="cropType.imgPath">{{ cropType.name }}</option>
</select>
</div>
<div class="form-group col-md-6">
<label for="exampleFormControlSelect2" class="col-form-label-sm font-weight-bold">2. Some text</label>
<select class="form-control" id="exampleFormControlSelect2">
<option v-for="(crop, index) in cropSelectList" :key="index" :value="crop.id">{{ crop.name }}</option>
</select>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
data() {
return {
cropTypes: null,
cropSelectList: null,
crops: null,
pickedCropType: null,
}
},
methods: {
loadPage: async function () {
try {
//Get crop types and create a new array with crop types with an added imgPath property
var cropTypesFinal = [];
let responseCropTypes = await this.$http.get(`http://localhost:8006/api/someData`);
responseCropTypes.data.data.forEach(function (element) {
cropTypesFinal.push(tmpType);
});
} catch (err) {
window.alert(err)
console.log(err)
}
},
getCropsByType: async function () {
//Get crops by crop type
let responseCrops = await this.$http.get(`http://localhost:8006/api/crop/Type/${this.pickedCropType}`);
var responseCropsData = responseCrops.data.data;
this.cropSelectList = responseCropsData;
}
},
async created() {
this.loadPage()
}
}
</script>
And in my second container I have different dropdowns and different input fields with different scripts etc.
So, my questions are:
1.) I'm having required data form field in first container and in second container I'm having additional data and my submit button is separated in third container (app-result-container). So, is this proper and logical way of structuring containers if not can you point me in right direction?
2.) Is it smart to input script tag in every container where I'm processing/fetching/submitting some data for that particular container? Should I put scripts tag in separated file and keep structure clean, separating html from js file.
Example:
import { something } from 'something'
export default {
data () {
return {
someData: 'Hello'
}
},
methods: {
consoleLogData: function (event) {
Console.log(this.someData)
}
}
}
3.) Can I send input values from one container to another (In my particular case from first and second container to app-calculate-container(third container))?
How to on submit return results container with calculated imported values
If you want components to communicate or share data with one another, you will need to either emit an event from one component up to the parent and pass it down via props, or use some kind of state management model, like Vuex, where each of your components can listen to the store.
Take a look at this code sandbox: https://codesandbox.io/s/8144oy7xy2
App.vue
<template>
<div id="app">
<child-input #input="updateName" />
<child-output :value="name" />
</div>
</template>
<script>
import ChildInput from "#/components/ChildInput.vue";
import ChildOutput from "#/components/ChildOutput.vue";
export default {
name: "App",
components: {
ChildInput,
ChildOutput
},
data() {
return {
name: ""
};
},
methods: {
updateName(e) {
this.name = e.target.value;
}
}
};
</script>
ChildInput.vue
<template>
<input type="text" #input="changeHandler">
</template>
<script>
export default {
name: "ChildInput",
methods: {
changeHandler(e) {
this.$emit("input", e);
}
}
};
</script>
ChildOutput.vue
<template>
<p>{{ value }}</p>
</template>
<script>
export default {
name: "ChildOutput",
props: {
value: {
type: String,
default: ""
}
}
};
</script>
What's going on?
The ChildInput component is a text field and on every change inside it, fires an event (emits using this.$emit() and passes the whole event up).
When this fires, App is listening to the change, which fires a method that updates the name data property.
Because name is a reactive data property and is being passed down as a prop to the ChildOutput component, the screen re-renders and is updated with the text written.
Neither ChildInput nor ChildOutput knows about one another. It's the parent that listens to the event passed to it, then passes the new prop down.
This way of working is fine and simple to understand, but I would strongly recommend looking at Vuex, as this method can get messy and complicated when you go beyond trivial tasks.
I have to change the color of the navbar of one page when scrolling a bit.
Here we have part of my xml file:
<ion-header no-border>
<ion-navbar color="{{ toolbar_color }}">
<ion-title (click)="change()">{{userdata.Name}}</ion-title>
</ion-navbar>
</ion-header>
<ion-content fullscreen class="container" (ionScrollEnd)="scrollHandler($event)">
I tryed first by changing it using a click event and it worked fine.
change() {
if ( this.toolbar_color == "danger" ) {
this.toolbar_color = "light"
} else {
this.toolbar_color = "danger"
}
}
And this is the ionScrollEnd listener, that does not work. The event is fired correctly, but the changes on toolbar_color are not taking any effect on the navbar.
scrollHandler(event) {
if ( event.scrollTop > 100 ) {
console.log("ScrollEvent --> "+JSON.stringify(event));
this.toolbar_color = "light"
// this.toolbar_change = true;
} else {
this.toolbar_color = "danger"
// this.toolbar_change = false;
}
}
How the hell can I do this?
Thank you :)
Add #ViewChild(Content) content: Content in the TS file and subscribe to scroll end event. refer this link for working version. Also see the ionic forum discussion on this issue
import { Component, ViewChild, ChangeDetectorRef } from '#angular/core';
import { NavController, Content } from 'ionic-angular';
#Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
#ViewChild(Content) content: Content;
Arr = Array; //Array type captured in a variable
num:number = 1000;
toolbar_color: string;
constructor(public navCtrl: NavController, public ref : ChangeDetectorRef) {
this.toolbar_color="secondary";
}
changeColor(){
this.toolbar_color="primary";
this.ref.detectChanges();
}
ionViewDidLoad() {
//this.content.enableJsScroll();
this.content.ionScrollEnd.subscribe(() => {
this.changeColor();
});
}
}
HTML file
<ion-header>
<ion-navbar [color]="toolbar_color">
<ion-title>Home</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<h2>Welcome to Ionic!</h2>
<p>
This starter project comes with simple tabs-based layout for apps
that are going to primarily use a Tabbed UI.
</p>
<p>
Take a look at the <code>pages/</code> directory to add or change tabs,
update any existing page or create new pages.
</p>
<div *ngFor="let i of Arr(num).fill(1)">{{i}}</div>
</ion-content>
Update-1
Added code to change color on scrolling
Sometimes angular will not run changeDetector automatically. we can manually trigger it by using ChangeDetectorRef. it's added to detect the changes while scrolling.
Working version is also updated. Please check the above link
I'm looking to push items from one page to another, based on the category ID. Then I went to push all the contents from the array(who have the category ID that matches) to the next page.
I'm caught between using an if statement to pass the parameters to the second page, or to call the array in the second page and use an NgIf. Below is my code for a better visual. I'm fairly new to Ionic. Thank you for the help!
First Page
if(id = 1) {
category_id: this.referenceList.category_id = 1;
this.navCtrl.push(ReferencePage,{category_id:"category_id"});
}
else {
console.log("nope")
}
Second Page
<div *ngfor = let reference of referenceList></div>
<div *ngif = category.id === 1> {{reference.referenceField1}} {{reference.referenceField2}} {{reference.media}} </div>
Not sure which one to use and why. Thanks again! Happy to clarify if there's any confusion.
You stored the category_id in a variable, so use this:
this.navCtrl.push(ReferencePage,{category_id:category_id});
In the second page controller you need:
import ( NavParams ) from 'ionic-angular';
category_id: number;
constructor(public navParams: NavParams) {
...
}
ionViewDidLoad() {
this.category_id = this.navParams.get('category_id');
}
Also, your html should be (if I understand what you are doing):
<div *ngFor = let reference of referenceList>
<div *ngIf = category_id === 1>
{{reference.referenceField1}};
{{reference.referenceField2}};
{{reference.media}};
</div>
</div>
I am having an issue getting select boxes on my Angular2 application to show a default option '--please select--' on page load. I have managed to get this working before but I cannot seem to get this working in this particular instance. I'll show my code then explanations as I show it.
Here is my relevant controller code:
import {Component} from "#angular/core";
import {ProductService} from "../../services/product.service";
import {Subscription} from "rxjs";
import {ActivatedRoute} from "#angular/router";
#Component({
selector : 'product',
moduleId : module.id,
templateUrl : '/app/views/products/product-view.html'
})
export class ProductComponent {
private id:number;
private _subscription: Subscription;
public product;
private price;
private quantity = 0;
constructor(
private _productService: ProductService,
private _activatedRoute: ActivatedRoute
) {
}
getProduct(productId: number) {
this._productService.getProduct(productId)
.subscribe((response) => {
response.success.product.options.forEach((option) => {
this[option.name] = {
name: '-- please select --',
product_option_value_id: 0,
price: 0,
price_prefix: '+'
};
option.product_option_value.unshift({
name: '-- please select --',
product_option_value_id: 0,
price: 0,
price_prefix: '+'
});
});
this.product = response.success.product;
this.generatePrice();
});
}
changeOption(optionValueId, option) {
if(optionValueId != 0) {
let selectedOptionValue = option.product_option_value.filter((option) => {
return option.product_option_value_id == optionValueId;
});
this[option.name] = selectedOptionValue[0];
} else {
this[option.name] = {
name: '-- please select --',
product_option_value_id: 0,
price: 0,
price_prefix: '+'
};
}
this.generatePrice();
}
.....
Here I am getting back information about a product which includes 'options' in a form of an array. This array of objects is iterated over to create the select boxes in the view code which will come later. I add a default '-- please select --' object for each option and put it to the front of the array using unshift. I then also set the controller value for this in the line:
this[option.name] = {
name: '-- please select --',
product_option_value_id: 0,
price: 0,
rice_prefix: '+'
};
The relevant view code is as follows:
<div class='product-options'>
<div class='option' *ngFor='let option of product.options; let i = index'>
<p class='option-name' [innerHTML]='option.name'></p>
<select name='option.name' [ngModel]='option.name' (ngModelChange)='changeOption($event, option)' required>
<option *ngFor='let productOptionValue of option.product_option_value; let j = index;' [value]='productOptionValue.product_option_value_id'>{{ productOptionValue.name }}</option>
</select>
</div>
<div class='price' ngDefaultControl [(ngModel)]="price">{{ price | currency:'GBP':true:'1.2-2' }}</div>
<div class='add-to-basket-wrap'>
<button class='add-to-basket'>add to basket</button>
<button class='increment' (click)="changeQuantity('down')">-</button>
<input type='text' name='quantity-to-add' [(ngModel)]="quantity" (click)='addToBasket()' />
<button class='increment'(click)="changeQuantity('up')">+</button>
</div>
</div>
Here I loop through the options then through the values for these options to generate the select boxes. I set the [ngModel] attribute for the select to the same as the one that was saved in my controller. I was under the impression that Angular2 would detect this binding, spot the value was the same as the controller value and then automatically set that as the 'selected' default option.
Can anyone see why this isn't working?
Thanks
Looks like your code is not working because your template and your class are not binding to the same properties.
In your component template, when you write this:
<div *ngFor="let option of product.options; let i = index">
<select [ngModel]="option.name">...</select>
</div>
... you're effectively binding each <select> to a class property named product.options[i].name.
On the other hand, in your component class, when you write this:
changeOption(optionValueId, option) {
this[option.name] = selectedOptionValue[0];
}
... you're writing to a class property named after whatever string is contained in option.name, e.g. foo.
As you can see, product.options[i].name and foo don't match. Even by changing foo to another string, you won't be able to access the property you want.
A few remarks/questions that might help:
It's a bit strange to store options inside "dynamic" class properties — this[option.name] = .... Why not store them in a dedicated this.options property that you can declare, type, and log out for debugging purposes: this.options[option.name] = ....
Why did you decide to use <select [ngModel]="..." (ngModelChange)="..."> vs the more compact <select [(ngModel)]="...">?
Any reason why you're using simple quotes on your HTML attributes, e.g. <div class='price'> vs <div class="price">? This is not the usual style.
I'm trying to wrap Semantic UI Modal component using portal approach described here
Here is my take at it http://jsfiddle.net/mike_123/2wvfjpy9/
I'm running into issue though, when obtaining a DOM reference and Rendering new markup into it there seem to be old reference still maintained.
render: function() {
return <div className="ui modal"/>; <-- the idea at first was to only return <div/>
},
...
React.render(<div > <----------- originally this element had className="ui modal", but this.node doesn't seem to overtake the original node reference
<i className="close icon"></i>
<div className="header">test</div>
<div className="content">
{props.children}
</div>
</div>, <-----------
this.node);
Any pointers how fix this test case http://jsfiddle.net/mike_123/2wvfjpy9/
You will lose correct vertical positioning and probably animations with approaches mentioned above.
Instead, you can just place your modal's component inside your app's root component and call .modal() with detachable: false. With this option, semantic wouldn't make any DOM manipulations and you won't lose your React DOM event listeners.
Example using Webpack/Babel:
import React, { Component } from 'react'
import $ from 'jquery'
if (typeof window !== 'undefined') {
window.jQuery = $
require('semantic-ui/dist/semantic.js')
}
class App extends Component {
state = {
showModal: false
}
_toggleModal = (e) => {
e.preventDefault()
this.toggleModalState()
}
toggleModalState = () => {
this.setState({ showModal: !this.state.showModal })
}
render() {
return (
<div>
<a href="" onClick={this._toggleModal}></a>
{this.state.showModal
? <Modal toggleModalState={this.toggleModalState}/>
: ''}
</div>
)
}
}
class Modal extends Component {
componentDidMount() {
$(this.modal)
.modal({ detachable: false })
.modal('show')
}
componentWillUnmount() {
$(this.modal).modal('hide')
}
_close = (e) {
e.preventDefault()
alert("Clicked")
this.props.toggleModalState()
}
render() {
return (
<div ref={(n) => this.modal = n} className="ui modal">
<div class="content">
<a onClick={this._close} href="">Click Me</a>
</div>
</div>
)
}
}
When you call this.$modal.modal('show'), it will actually restructure your DOM, and React will not be happy about it. Plus, if you try to put control in your modal, the control will not work.
What you should do is to React.render an already shown modal, i.e. a modal with markup as if $('.ui.modal').modal('show') has been called.
Here is my attempt using "React-Portal" to help with rendering a react component at body level. You can still use your method if you prefer.
// modal.jsx
import React, { Component } from 'react';
import Portal from 'react-portal';
class InnerModal extends Component {
constructor(props) {
super(props);
this.state = { modalHeight: 0 };
}
componentDidMount() {
let modalHeight = window.$('#reactInnerModal').outerHeight();
this.setState({modalHeight: modalHeight});
}
render() {
return (
<div id='reactInnerModal' className='ui standard test modal transition visible active' style={{'margin-top': - this.state.modalHeight / 2}}>
<i className='close icon' onClick={this.props.closePortal}></i>
{this.props.children}
</div>
);
}
}
class Modal extends Component {
render() {
let triggerButton = <button className='ui button'>Open Modal</button>;
return (
<Portal className='ui dimmer modals visible active page transition' openByClickOn={triggerButton} closeOnEsc={true} closeOnOutsideClick={true}>
<InnerModal>
{this.props.children}
</InnerModal>
</Portal>
);
}
}
export default Modal;
Notice that my modal has already been rendered in the markup.
You can then consume the modal as below:
// index.jsx
import React, { Component } from 'react';
import Modal from './modal';
class ModalDemo extends Component {
render() {
return (
<Modal>
<div className='header'>
Profile Picture
</div>
<div className='image content'>
<div className='ui medium image'>
<img src='http://semantic-ui.com/images/avatar2/large/rachel.png' />
</div>
<div className='description'>
<div className="ui header">We've auto-chosen a profile image for you.</div>
<p>We've grabbed the following image from the <a href='https://www.gravatar.com' target='_blank'>gravatar</a> image associated with your registered e-mail address.</p>
<p>Is it okay to use this photo?</p>
</div>
</div>
<div className='actions'>
<div className='ui black deny button'>
Nope
</div>
<div className='ui positive right labeled icon button'>
Yep, that's me
<i className='checkmark icon'></i>
</div>
</div>
</Modal>
);
}
}
React.render(<ModalDemo />, document.getElementById('content'));
With this you don't have to hack your way into DOM manipulation with jQuery, and the control in the modal (button, link, etc, to invoke functions) still works.
Hope this help!
Khanetor answered this question thoroughly and correctly, I just want to contribute one additional tidbit about how to position the Modal. It would be best as a comment, but unfortunately, I don't have the reputation to do so.
Anyways, the first child element of the Portal element needs to be positioned absolutely in order to make the dimmer and resulting modal sit on top of the page content rather than get put beneath it.
First, add style={position:'absolute'} to the Portal declaration in Modal's render method so the dimmer gets set at the top of the page. You end up with:
<Portal className='ui dimmer modals visible active page transition' openByClickOn={triggerButton} closeOnEsc={true} closeOnOutsideClick={true} style={position:'absolute'}>
<InnerModal>
{this.props.children}
</InnerModal>
</Portal>
Next, set the InnerModal's position to relative and decide on a distance from the top of the screen. I used an eighth (or 0.125) of the browser's viewport and got:
class InnerModal extends Component {
constructor(props){
super(props);
this.state = {
modalId : _.uniqueId('modal_'),
style: {}
}
}
componentDidMount() {
this.setState({
style : {
position : 'relative',
top : $(window).height() * 0.125 + 'px'
}
});
}
render(){
return (
<div id={this.state.modalId} className='ui standard modal transition visible active'
style={this.state.style}>
<i className='close icon' onClick={this.props.closePortal}></i>
{ this.props.children }
</div>
);
}
}
With those edits made, I've finally got some working modals in React! Hope this is helpful to someone else running into some of the same issues I've been.