We're currently working on a Twitter-esque Client for SwiftUI and we have to convert HTML content to NSAttributedStrings. For that reason, we're using Atributika to style the HTML tags. It seems to be working correctly, but when we try to display the content, the text just goes over other content. We've assumed it is because it does not respect its parent's size, but it's probably something else.
How would we fix this? Is there a better way to render HTML content in SwiftUI that doesn't require using WKWebViews since that will result in an incredible performance drop?
Is there something we're missing?
Thanks in advance.
Our code:
NetworkView.swift
struct NetworkView: View {
#ObservedObject var timeline = NetworkViewModel()
private let size: CGFloat = 300
private let padding: CGFloat = 10
private let displayPublic: Bool = true
var body: some View {
List {
Section {
NavigationLink(destination: Text("F").padding()) {
Label("Announcements", systemImage: "megaphone")
}
NavigationLink(destination: Text("F").padding()) {
Label("Activity", systemImage: "flame")
}
}
.listStyle(InsetGroupedListStyle())
Section(header:
Picker(selection: self.$timeline.type, label: Text("Network visibility")) {
Text("My community").tag(TimelineScope.local)
Text("Public").tag(TimelineScope.public)
} .pickerStyle(SegmentedPickerStyle())
.padding(.top)
.padding(.bottom, 2)) {
if self.timeline.statuses.isEmpty {
HStack {
Spacer()
VStack {
Spacer()
ProgressView(value: 0.5)
.progressViewStyle(CircularProgressViewStyle())
Text("Loading posts...")
Spacer()
}
Spacer()
}
} else {
ForEach(self.timeline.statuses, id: \.self.id) { status in
StatusView(status: status)
.buttonStyle(PlainButtonStyle())
}
}
}
}
.listStyle(GroupedListStyle())
}
}
StatusView.swift
import SwiftUI
import Atributika
/// A structure that computes statuses on demand from a `Status` data model.
struct StatusView: View {
#if os(iOS)
#Environment(\.horizontalSizeClass) private var horizontalSizeClass
#endif
/// In Starlight, there are two ways to display mastodon statuses:
/// - Standard: The status is being displayed from the feed.
/// - Focused: The status is the main post of a thread.
///
/// The ``StatusView`` instance should know what way it is being
/// displayed so that it can display the content properly.
/// In order to achieve this, we pass a bool to ``StatusView``, which when true,
/// tells it that it's content should be displayed as `Focused`.
private var isMain: Bool
/// The ``Status`` data model whose the data will be displayed.
var status: Status
#if os(iOS)
/// Using for triggering the navigation View **only**when the user taps
/// on the content, and not when it taps on the action buttons.
#State var goToThread: Int? = 0
#State var showMoreActions: Bool = false
#State var profileViewActive: Bool = false
#endif
private let rootStyle: Style = Style("p")
.font(.systemFont(ofSize: 17, weight: .light))
private let rootPresentedStyle: Style = Style("p")
.font(.systemFont(ofSize: 20, weight: .light))
private func configureLabel(_ label: AttributedLabel, size: CGFloat = 17) {
label.numberOfLines = 0
label.textColor = .label
label.font = .systemFont(ofSize: size)
label.lineBreakMode = .byWordWrapping
}
/// To easily use the same view on multiple platforms,
/// we use the `body` view as a container where we load platform-specific
/// modifiers.
var body: some View {
// We use this vertical stack to load platform specific modifiers,
// or to load specific views when a condition is met.
VStack {
// Whether the post is focused or not.
if self.isMain {
self.presentedView
} else {
// To provide the best experience, we want to allow the user to easily
// interact with a post directly from the feed. Because of that, we need
// to add a button
ZStack {
self.defaultView
NavigationLink(
destination: ThreadView(mainStatus: self.status),
tag: 1,
selection: self.$goToThread,
label: {
EmptyView()
}
)
}
}
}
.buttonStyle(PlainButtonStyle())
}
/// The status display mode when it is the thread's main post.
var presentedView: some View {
VStack(alignment: .leading) {
NavigationLink(destination:
ProfileView(isParent: false,
accountInfo: ProfileViewModel(accountID: self.status.account.id),
onResumeToParent: {
self.profileViewActive = false
}),
isActive: self.$profileViewActive) {
EmptyView()
}
HStack(alignment: .center) {
ProfileImage(from: self.status.account.avatarStatic, placeholder: {
Circle()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundColor(.gray)
})
VStack(alignment: .leading, spacing: 5) {
VStack(alignment: .leading) {
Text("\(self.status.account.displayName)")
.font(.headline)
Text("\(self.status.account.acct)")
.foregroundColor(.gray)
.lineLimit(1)
}
}
Spacer()
Button(action: { self.showMoreActions.toggle() }, label: {
Image(systemName: "ellipsis")
.imageScale(.large)
})
}
GeometryReader { (geometry: GeometryProxy) in
AttributedTextView(attributedText:
"\(self.status.content)"
.style(tags: rootPresentedStyle),
configured: { label in configureLabel(label, size: 20) },
maxWidth: geometry.size.width)
.fixedSize(horizontal: true, vertical: true)
}
if !self.status.mediaAttachments.isEmpty {
AttachmentView(from: self.status.mediaAttachments[0].url) {
Rectangle()
.scaledToFit()
.cornerRadius(10)
}
}
HStack {
Text("\(self.status.createdAt.getDate()!.format(as: "hh:mm · dd/MM/YYYY")) · ")
Button(action: {
if let application = self.status.application {
if let website = application.website {
openUrl(website)
}
}
}, label: {
Text("\(self.status.application?.name ?? "Mastodon")")
.lineLimit(1)
})
.foregroundColor(.accentColor)
.padding(.leading, -7)
}
.padding(.top)
Divider()
Text("\(self.status.repliesCount.roundedWithAbbreviations) ").bold()
+
Text("comments, ")
+
Text("\(self.status.reblogsCount.roundedWithAbbreviations) ").bold()
+
Text("boosts, and ")
+
Text("\(self.status.favouritesCount.roundedWithAbbreviations) ").bold()
+
Text("likes.")
Divider()
self.actionButtons
.padding(.vertical, 5)
.padding(.horizontal)
}
.buttonStyle(PlainButtonStyle())
.navigationBarHidden(self.profileViewActive)
.actionSheet(isPresented: self.$showMoreActions) {
ActionSheet(title: Text("More Actions"),
buttons: [
.default(Text("View #\(self.status.account.acct)'s profile"), action: {
self.profileViewActive = true
}),
.destructive(Text("Mute #\(self.status.account.acct)"), action: {
}),
.destructive(Text("Block #\(self.status.account.acct)"), action: {
}),
.destructive(Text("Report #\(self.status.account.acct)"), action: {
}),
.cancel(Text("Dismiss"), action: {})
]
)
}
}
var defaultView: some View {
VStack(alignment: .leading) {
Button(action: {
self.goToThread = 1
}, label: {
HStack(alignment: .top) {
ProfileImage(from: self.status.account.avatarStatic, placeholder: {
Circle()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundColor(.gray)
})
VStack(alignment: .leading, spacing: 5) {
HStack {
HStack(spacing: 5) {
Text("\(self.status.account.displayName)")
.font(.headline)
.lineLimit(1)
Text("\(self.status.account.acct)")
.foregroundColor(.gray)
.lineLimit(1)
Text("· \(self.status.createdAt.getDate()!.getInterval())")
.lineLimit(1)
}
}
GeometryReader { (geometry: GeometryProxy) in
AttributedTextView(attributedText:
"\(self.status.content)"
.style(tags: rootStyle),
configured: { label in configureLabel(label, size: 17) },
maxWidth: geometry.size.width)
.fixedSize(horizontal: true, vertical: false)
}
if !self.status.mediaAttachments.isEmpty {
AttachmentView(from: self.status.mediaAttachments[0].previewURL) {
Rectangle()
.scaledToFit()
.cornerRadius(10)
}
}
}
}
})
self.actionButtons
.padding(.leading, 60)
}
.contextMenu(
ContextMenu(menuItems: {
Button(action: {}, label: {
Label("Report post", systemImage: "flag")
})
Button(action: {}, label: {
Label("Report \(self.status.account.displayName)", systemImage: "flag")
})
Button(action: {}, label: {
Label("Share as Image", systemImage: "square.and.arrow.up")
})
})
)
}
/// The post's action buttons (favourite and reblog), and also the amount of replies.
///
/// If the post is focused (``isMain`` is true), the count is hidden.
var actionButtons: some View {
HStack {
HStack {
Image(systemName: "text.bubble")
if !self.isMain {
Text("\(self.status.repliesCount.roundedWithAbbreviations)")
}
}
Spacer()
Button(action: {
}, label: {
HStack {
Image(systemName: "arrow.2.squarepath")
if !self.isMain {
Text("\(self.status.reblogsCount.roundedWithAbbreviations)")
}
}
})
.foregroundColor(
labelColor
)
Spacer()
Button(action: {
}, label: {
HStack {
Image(systemName: "heart")
if !self.isMain {
Text("\(self.status.favouritesCount.roundedWithAbbreviations)")
}
}
})
.foregroundColor(
labelColor
)
Spacer()
Button(action: {
}, label: {
Image(systemName: "square.and.arrow.up")
})
.foregroundColor(
labelColor
)
}
}
}
extension StatusView {
/// Generates a View that displays a post on Mastodon.
///
/// - Parameters:
/// - isPresented: A boolean variable that determines whether
/// the status is being shown as the main post (in a thread).
/// - status: The identified data that the ``StatusView`` instance uses to
/// display posts dynamically.
public init(isMain: Bool = false, status: Status) {
self.isMain = isMain
self.status = status
}
}
struct StatusView_Previews: PreviewProvider {
#ObservedObject static var timeline = NetworkViewModel()
static var previews: some View {
VStack {
if self.timeline.statuses.isEmpty {
HStack {
Spacer()
VStack {
Spacer()
ProgressView(value: 0.5)
.progressViewStyle(CircularProgressViewStyle())
Text("Loading status...")
Spacer()
}
Spacer()
}
.onAppear {
self.timeline.fetchLocalTimeline()
}
} else {
StatusView(isMain: false, status: self.timeline.statuses[0])
}
}
.frame(width: 600, height: 300)
.previewLayout(.sizeThatFits)
}
}
AttributedTextView.swift
import UIKit
import SwiftUI
import Foundation
import Atributika
// Note: Implementation pulled from pending PR in Atributika on GitHub: https://github.com/psharanda/Atributika/pull/119
// Credit to rivera-ernesto for this implementation.
/// A view that displays one or more lines of text with applied styles.
struct AttributedTextView: UIViewRepresentable {
typealias UIViewType = RestrainedLabel
/// The attributed text for this view.
var attributedText: AttributedText?
/// The configuration properties for this view.
var configured: ((AttributedLabel) -> Void)?
#State var maxWidth: CGFloat = 300
public func makeUIView(context: UIViewRepresentableContext<AttributedTextView>) -> RestrainedLabel {
let new = RestrainedLabel()
configured?(new)
return new
}
public func updateUIView(_ uiView: RestrainedLabel, context: UIViewRepresentableContext<AttributedTextView>) {
uiView.attributedText = attributedText
uiView.maxWidth = maxWidth
}
class RestrainedLabel: AttributedLabel {
var maxWidth: CGFloat = 0.0
open override var intrinsicContentSize: CGSize {
sizeThatFits(CGSize(width: maxWidth, height: .infinity))
}
}
}
Related
I'm using latest vue-chartjs package with vue3 to create stackbarchart. I've shown the stackbarchart on my app but it's labels are overlapping. I need to know which property can add in options that can fix my issue.
<template>
<Bar
v-if="chartData != null"
:key="id"
:data="chartData"
:options="chartOptions"
/>
</template>
<script>
import { Bar, getElementAtEvent } from "vue-chartjs";
import ChartJSPluginDatalabels from "chartjs-plugin-datalabels";
import uniqueId from "lodash.uniqueid";
import { drilldown } from "#/views/Reports/js/drilldown";
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
BarElement,
CategoryScale,
LinearScale,
ArcElement
} from "chart.js";
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ArcElement,
ChartJSPluginDatalabels
);
export default {
name: "BarChartStacked",
components: {
Bar,
},
props: ["data", "options", "reportData", "eventInfo", "item", "duringDay"],
data() {
return {
id: null,
};
},
computed:{
chartData() { return this.data; /* mutable chart data */ },
chartOptions() { return this.options; /* mutable chart options */ }
},
mounted() {
this.id = uniqueId();
this.chartOptions.plugins.responsive = true;
if (this.reportData && this.reportData.dataFilter) {
if (this.item.conditions) {
// change cursor to pointer if element is clickable
this.chartOptions.hover = {
onHover: function(e) {
var point =getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
this.chartOptions.onClick = this.handle;
}
} else {
this.chartOptions.hover = {}
}
},
The stackbarchart should display value for the top most graph only like mention in the picture.
I am using Nuxt and vue2-google-maps but I do have the following error
module error: google is not defined
I read the official GitHub FAQ, so I used this.$gmapApiPromiseLazy().then().
But, I do have the aforementioned error.
getCurrentPositionandBathroom method is to get current position and search for convenience store around current position.
<template>
<v-app>
<v-btn class="blue white--text" #click="getCurrentPositionandBathroom"> search for bathroom! </v-btn>
<GmapMap
ref="mapRef"
:center="maplocation"
:zoom="15"
map-type-id="roadmap"
style="height: 300px; width: 900px"
>
<GmapMarker
v-for="m in markers"
:key="m.id"
:position="m.position"
:clickable="true"
:draggable="true"
:icon="m.icon"
#click="center = m.position"
/>
</GmapMap>
</v-app>
</template>
<script>
export default {
data() {
return {
maplocation: { lat: 35.6814366, lng: 139.767157 },
markers: [],
}
},
methods: {
getCurrentPositionandBathroom() {
if (process.client) {
if (!navigator.geolocation) {
alert('Japanese sentences')
return
}
navigator.geolocation.getCurrentPosition(this.success, this.error)
}
},
success(position) {
this.maplocation.lat = position.coords.latitude
this.maplocation.lng = position.coords.longitude
this.$gmapApiPromiseLazy().then(() => {
google.maps.event.addListenerOnce(
this.$refs.mapRef.$mapObject,
'idle',
function () {
this.getBathroom()
}.bind(this),
)
})
},
getBathroom() {
const map = this.$refs.mapRef.$mapObject
const placeService = new google.maps.places.PlacesService(map)
placeService.nearbySearch(
{
location: new google.maps.LatLng(this.maplocation.lat, this.maplocation.lng),
radius: 500,
type: ['convenience_store'],
},
function (results, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
results.forEach((place) => {
const icon = {
url: place.icon,
scaledSize: new google.maps.Size(30, 30),
}
const marker = {
position: place.geometry.location,
icon,
title: place.name,
id: place.place_id,
}
this.markers.push(marker)
})
}
}.bind(this),
)
},
error(errorMessage) {
switch (errorMessage.code) {
case 1:
alert('Japanese sentences')
break
case 2:
alert('Japanese sentences')
break
case 3:
alert('Japanese sentences')
break
default:
alert('Japanese sentences')
break
}
},
},
}
</script>
What I should I do?
PS: I can see the Google Maps. In other words, Google Maps is displayed.
Alright, so there was quite a few configuration to do but I achieved to have a working map. Your this.getBathroom() method was not working for me, but this is related to the API or how you handle the logic I guess.
I basically followed the package README and it all went smooth at the end. Nothing special and google is available as explained in the following section:
If you need to gain access to the google object
Here is the final code of the .vue file
<template>
<div>
<button class="blue white--text" #click="getCurrentPositionandBathroom">
search for bathroom!
</button>
<GmapMap
ref="mapRef"
:center="maplocation"
:zoom="15"
map-type-id="roadmap"
style="height: 300px; width: 900px"
>
<GmapMarker
v-for="m in markers"
:key="m.id"
:position="m.position"
:clickable="true"
:draggable="true"
:icon="m.icon"
#click="center = m.position"
/>
</GmapMap>
</div>
</template>
<script>
import { gmapApi } from 'vue2-google-maps'
export default {
data() {
return {
maplocation: { lat: 35.6814366, lng: 139.767157 },
markers: [],
}
},
computed: {
google: gmapApi,
},
methods: {
getCurrentPositionandBathroom() {
if (process.client) {
if (!navigator.geolocation) {
alert('Japanese sentences')
return
}
navigator.geolocation.getCurrentPosition(this.success, this.error)
}
},
success(position) {
this.maplocation.lat = position.coords.latitude
this.maplocation.lng = position.coords.longitude
// this.$gmapApiPromiseLazy().then(() => { // not needed here anymore
this.google.maps.event.addListenerOnce(
this.$refs.mapRef.$mapObject,
'idle',
function () {
this.getBathroom()
}.bind(this)
)
// })
},
getBathroom() {
const map = this.$refs.mapRef.$mapObject
const placeService = new this.google.maps.places.PlacesService(map)
placeService.nearbySearch(
{
location: new this.google.maps.LatLng(
this.maplocation.lat,
this.maplocation.lng
),
radius: 500,
type: ['convenience_store'],
},
function (results, status) {
if (status === this.google.maps.places.PlacesServiceStatus.OK) {
results.forEach((place) => {
const icon = {
url: place.icon,
scaledSize: new this.google.maps.Size(30, 30),
}
const marker = {
position: place.geometry.location,
icon,
title: place.name,
id: place.place_id,
}
this.markers.push(marker)
})
}
}.bind(this)
)
},
error(errorMessage) {
switch (errorMessage.code) {
case 1:
alert('Japanese sentences')
break
case 2:
alert('Japanese sentences')
break
case 3:
alert('Japanese sentences')
break
default:
alert('Japanese sentences')
break
}
},
},
}
</script>
You can find the useful commit on my github repo here.
This is how it looks at the end, no errors so far.
PS: I didn't saw that you were using Vuetify, so I didn't bother bringing it back later on.
i am using a PhotoGrid Library in react native to populate the list of photo on my apps. how to call a function from the render function ? it show this error when i call a function called "deva" on my OnPress method in <Button onPress={()=>{this.deva()}}><Text>Bondan</Text></Button> . here is my code...
import React from 'react';
import { StyleSheet, Text, View, WebView, TouchableOpacity, Image, Alert, Dimensions} from 'react-native';
import {DrawerNavigator} from 'react-navigation'
import {Container, Header, Button, Icon, Title, Left, Body, Right, Content} from 'native-base'
import PhotoGrid from 'react-native-photo-grid'
import HomeScreen from './HomeScreen'
export default class Recomended extends React.Component {
constructor() {
super();
this.state = { items: [],
nama : ""
}
}
goToBufetMenu(){
this.props.navigation.navigate("BufetMenu");
}
componentDidMount() {
// Build an array of 60 photos
let items = Array.apply(null, Array(60)).map((v, i) => {
return { id: i, src: 'http://placehold.it/200x200?text='+(i+1) }
});
this.setState({ items });
//this.setState({ nama: "Bondan"});
//this.props.navigation.navigate("BufetMenu");
}
deva() {
Alert.alert('deva');
}
render() {
return (
<Container style={styles.listContainer}>
<PhotoGrid
data = { this.state.items }
itemsPerRow = { 3 }
itemMargin = { 3 }
renderHeader = { this.renderHeader }
renderItem = { this.renderItem }
style={{flex:2}}
/>
</Container>
);
}
renderHeader() {
return(
<Button onPress={()=>{this.deva()}}><Text>Bondan</Text></Button>
);
}
renderItem(item, itemSize) {
return(
<TouchableOpacity
key = { item.id }
style = {{ width: itemSize, height: itemSize }}
onPress = { () => {
this.deva();
}}>
<Image
resizeMode = "cover"
style = {{ flex: 1 }}
source = {{ uri: item.src }}
/>
<Text>{item.src}</Text>
</TouchableOpacity>
)
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
height: 587,
},
gridText: {
color: '#fff',
textAlign: 'center',
fontStyle: 'normal',
fontSize : 12
},
listContainer: {
height: Dimensions.get('window').height - (Dimensions.get('window').height*53/100),
}
});
You are loosing context of this. You need to either use arrow functions or bind the functions.
Example
constructor() {
super();
this.state = { items: [],
nama : ""
};
this.renderHeader = this.renderHeader.bind(this);
this.renderItem = this.renderItem.bind(this);
}
OR
renderHeader = () => {
// rest of your code
}
renderItem = (item, itemSize) => {
// rest of your code
}
Either change your deva method definition to an arrow function -
deva= () => {
Alert.alert('deva');
}
Or bind the deva method to this inside your constructor
constructor() {
super();
this.state = { items: [],
nama : ""
}
this.deva = this.deva.bind(this)
}
You get the error because when the deva method is invoked using this.deva(), the javascript runtime cannot find the property/function deva on the this it's called with (which is the anonymous callback passed to onPress in this case). But if you bind this to deva beforehand, the correct this is being searched by the javascript runtime.
When I triggered an event coming from a method via an input it don't render properly the data to the chart here's an example of my pb:
My chart was working until I put it in the child. It looks like the data are not coming trough the child.
parent.html:
<div class="parent" >
<img src="black.png" type="button" (click)="displayChild()"/>
<my-child [displayDetail]="displayMe"></my-child>
</div>
parent.ts:
displayChild() {
this.displayMe = !this.displayMe;
child.html:
<div class="chart-pie">
<chart [options]="options" (load)="saveInstance($event.context)"
</chart>
</div>
child.ts:
#Input() displayDetail: boolean;
options: any;
data: Object[];
chart: any;
dataSubscription: Subscription;
constructor(private userService3: UserService3) {
this.options = {
chart: { type: 'pie',
series: [{
name: 'Dispo',
data: []
}]
};
saveInstance(chartInstance) {
this.chart = chartInstance;
console.log(chartInstance);
}
public ngOnInit () {
this.dataSubscription =
this.userService3.getData().subscribe((data) => {
this.options.series[0].data = data.data.operating_rate;
// Code for the pie
let percentUp = data.data.operating_rate; // 88.14
let percentDown = 100 - percentUp; // 11.86
this.options.series[0].data = [
{
name: 'Up',
y: percentUp,
color: '#648e59'
},
{
name: 'Down',
y: percentDown,
color: 'white'
}
];
console.log(data);
});
}
public ngOnDestroy() {
if (this.dataSubscription) {
this.dataSubscription.unsubscribe();
}
}
}
how to get data from json rather from table-data.ts
iam confused.tried my best cant able to find the solution.where should i do alteration i think from private data:array=TableData;
It will be helpful if anyone give a solution.
Demo.component.ts
export class TableDemoComponent implements OnInit { public rows:Array<any> = [];
public columns:Array<any> = [
{title: 'Company', name: 'name', filtering: {filterString: '', placeholder: 'Filter by name'}},
{
title: 'Position',
name: 'position',
sort: false,
filtering: {filterString: '', placeholder: 'Filter by position'}
},
{title: 'Location', name: 'office', sort: '', filtering: {filterString: '', placeholder: 'Filter by Location'}},
{title: 'Date', className: 'text-warning', name: 'startDate'},];
public page:number = 1;
public itemsPerPage:number = 10;
public maxSize:number = 5;
public numPages:number = 1;
public length:number = 0;
public config:any = {
paging: true,
sorting: {columns: this.columns},
filtering: {filterString: ''},
className: ['table-striped', 'table-bordered']
};
private data:Array<any> = TableData;
public constructor() {
this.length = this.data.length;
}
public ngOnInit():void {
this.onChangeTable(this.config);
}
public changePage(page:any, data:Array<any> = this.data):Array<any> {
let start = (page.page - 1) * page.itemsPerPage;
let end = page.itemsPerPage > -1 (startpage.itemsPerPage):data.length;
return data.slice(start, end);
}
public changeSort(data:any, config:any):any {
if (!config.sorting) {
return data;
}
let columns = this.config.sorting.columns || [];
let columnName:string = void 0;
let sort:string = void 0;
for (let i = 0; i < columns.length; i++) { if(columns[i].sort!==''&&columns[i].sort!==false{columnNamecolumns[i].name;
sort = columns[i].sort;
}
}
if (!columnName) {
return data;
}
// simple sorting
return data.sort((previous:any, current:any) => {
if (previous[columnName] > current[columnName]) {
return sort === 'desc' ? -1 : 1;
} else if (previous[columnName] < current[columnName]) {
return sort === 'asc' ? -1 : 1;
}
return 0;
});
}
public changeFilter(data:any, config:any):any {
let filteredData:Array<any> = data;
this.columns.forEach((column:any) => {
if (column.filtering) {
filteredData = filteredData.filter((item:any) => {
return item[column.name].match(column.filtering.filterString); });
}
});
if (!config.filtering) {
return filteredData;
}
if (config.filtering.columnName) {
return filteredData.filter((item:any) => item[config.filtering.columnName].match(this.config.filtering.filterString));
}
let tempArray:Array<any> = [];
filteredData.forEach((item:any) => {
let flag = false;
this.columns.forEach((column:any) => {
if (item[column.name].toString().match(this.config.filtering.filterString)) {
flag = true;
}
});
if (flag) {
tempArray.push(item);
}
});
filteredData = tempArray;
return filteredData;
}
public onChangeTable(config:any, page:any = {page: this.page,itemsPerPage: this.itemsPerPage}):any {
if (config.filtering) {
Object.assign(this.config.filtering, config.filtering);
}
if (config.sorting) {
Object.assign(this.config.sorting, config.sorting);
}
let filteredData = this.changeFilter(this.data, this.config);
let sortedData = this.changeSort(filteredData, this.config);
this.rows = page && config.paging ? this.changePage(page,sortedData):sortedData;
this.length = sortedData.length;
}
public onCellClick(data: any): any {
console.log(data);
}}
Datatable.ts
export const TableData:Array<any> = [
{
'name': 'Victoria Cantrell',
'position': 'Integer Corporation',
'office': 'Croatia',
'ext': `<strong>0839</strong>`,
'startDate': '2015/08/19',
'salary': 208.178
}, {
'name': 'Pearl Crosby',
'position': 'In PC',
'office': 'Cambodia',
'ext': `<strong>8262</strong>`,
'startDate': '2014/10/08',
'salary': 114.367
}, {
'name': 'Colette Foley',
'position': 'Lorem Inc.',
'office': 'Korea, North',
'ext': '8968',
'startDate': '2015/07/19',
'salary': 721.473
}
];
Table-demo.html
<div class="row">
<div class="col-md-4">
<input *ngIf="config.filtering" placeholder="Filter allcolumns"
[ngTableFiltering]="config.filtering"
class="form-control"
(tableChanged)="onChangeTable(config)"/>
</div>
</div>
<br>
<ng-table [config]="config"
(tableChanged)="onChangeTable(config)"
(cellClicked)="onCellClick($event)"
[rows]="rows" [columns]="columns">
</ng-table>
<pagination *ngIf="config.paging"
class="pagination-sm"
[(ngModel)]="page"
[totalItems]="length"
[itemsPerPage]="itemsPerPage"
[maxSize]="maxSize"
[boundaryLinks]="true"
[rotate]="false"
(pageChanged)="onChangeTable(config, $event)"
(numPages)="numPages = $event">
</pagination>
If you wanted to use the TableData in a different file you would have to import it. I have added an example at the top that show you how to import it. All you do is just create another file and import what you need. I tidied up your code and fixed some syntax errors and put some notes next to bits where stuff was undefined which would throw errors also put some explanations next to the top in how to import things:
import {OnInit} from "#angular/core"
import {TableData} from "./test2" //test2 is the name of the file
// ./ current directory
// ../../ up two directories
export class TableDemoComponent implements OnInit {
public rows: Array<any> = [];
public columns: Array<any> =
[
{
title: 'Company',
name: 'name',
filtering: {
filterString: '', placeholder: 'Filter by name'
}
},
{
title: 'Position',
name: 'position',
sort: false,
filtering: {
filterString: '', placeholder: 'Filter by position'
}
},
{
title: 'Location',
name: 'office',
sort: '',
filtering: {
filterString: '', placeholder: 'Filter by Location'
}
},
{
title: 'Date',
className: 'text-warning',
name: 'startDate'
}
];
public page: number = 1;
public itemsPerPage: number = 10;
public maxSize: number = 5;
public numPages: number = 1;
public length: number = 0;
public config: any = {
paging: true,
sorting: {columns: this.columns},
filtering: {filterString: ''},
className: ['table-striped', 'table-bordered']
};
private data: Array<any> = TableData;
public constructor() {
this.length = this.data.length;
}
public ngOnInit(): void {
this.onChangeTable(this.config);
}
public changePage(page: any, data: Array<any> = this.data): Array<any> {
let start = (page.page - 1) * page.itemsPerPage;
//startpage is not defined
let end = page.itemsPerPage > -1 ? startpage.itemsPerPage : data.length;
return data.slice(start, end);
}
public changeSort(data: any, config: any): any {
if (!config.sorting) {
return data;
}
let columns = this.config.sorting.columns || [];
let columnName: string = void 0;
let sort: string = void 0;
for (let i = 0; i < columns.length; i++) {
if (columns[i].sort !== '' && columns[i].sort !== false) {
//columnNamecolumns is not defined
columnNamecolumns[i].name;
sort = columns[i].sort;
}
}
if (!columnName) {
return data;
}
// simple sorting
return data.sort((previous: any, current: any) => {
if (previous[columnName] > current[columnName]) {
return sort === 'desc' ? -1 : 1;
} else if (previous[columnName] < current[columnName]) {
return sort === 'asc' ? -1 : 1;
}
return 0;
});
}
public changeFilter(data: any, config: any): any {
let filteredData: Array<any> = data;
this.columns.forEach((column: any) => {
if (column.filtering) {
filteredData = filteredData.filter((item: any) => {
return item[column.name].match(column.filtering.filterString);
});
}
});
if (!config.filtering) {
return filteredData;
}
if (config.filtering.columnName) {
return filteredData.filter((item: any) => item[config.filtering.columnName].match(this.config.filtering.filterString));
}
let tempArray: Array<any> = [];
filteredData.forEach((item: any) => {
let flag = false;
this.columns.forEach((column: any) => {
if (item[column.name].toString().match(this.config.filtering.filterString)) {
flag = true;
}
});
if (flag) {
tempArray.push(item);
}
});
filteredData = tempArray;
return filteredData;
}
public onChangeTable(config: any, page: any = {page: this.page, itemsPerPage: this.itemsPerPage}): any {
if (config.filtering) {
Object.assign(this.config.filtering, config.filtering);
}
if (config.sorting) {
Object.assign(this.config.sorting, config.sorting);
}
let filteredData = this.changeFilter(this.data, this.config);
let sortedData = this.changeSort(filteredData, this.config);
this.rows = page && config.paging ? this.changePage(page, sortedData) : sortedData;
this.length = sortedData.length;
}
public onCellClick(data: any): any {
console.log(data);
}
}
I hope this is what you meant for. I am reading my data from external source (not json - but I think this is the same) by this way:
public constructor(private dataService: DataService ){
this.dataService.getUsers().then(users => {this.data = users; this.length = this.data.length;}).
catch(error => this.error = error);
}
public ngOnInit():void {
this.dataService.getUsers().then(users => {this.data = users; this.onChangeTable(this.config);}).
catch(error => this.error = error);
}
in this example my data is users. I am calling it from my data service. I hope it helps.