I have a component that has several functions that subscribes to data returned from an observable in my service. This is working properly, but I would like to refactor my code so I can access that data in my constant and simply loop through the data in my view.
The problem I'm having is when I want to read a property on my object from my constant e.g. value: this.price.market_price_usd, I get a undefined error in my console. I've also tried to interpolate data by doing something like value: '${this.price.market_price_usd}',. This simply just returns a string. It appears the problem is that my constant is not aware of this data. How can I refactor my code so I can access this data in my constant and read it in my template? Below is the relevant code.
Component
constructor(private data: DataService) {
this.loadstate = data.loadstate;
this.subscription = data.nameChange.subscribe((value) => {
this.loadstate = value;
});
}
getPrice() {
this.data.getData(this.API_Price)
.subscribe(
price => this.price = price,
error => this.errorMessage_price = <any>error);
}
getBlockSize() {
this.data.getData(this.API_Block_Size)
.subscribe(
size => this.size = size.toFixed(2),
error => this.errorMessage_size = <any>error);
}
getTransactions() {
this.data.getData(this.API_Transactions)
.subscribe(
transactions => this.transactions = transactions.values.slice(-1)[0].y.toLocaleString(),
error => this.errorMessage_transactions = <any>error);
}
getMempool() {
this.data.getData(this.API_Mempool)
.subscribe(
mempool => this.mempool = Math.trunc(mempool.values[0].y).toLocaleString(),
error => this.errorMessage_mempool = <any>error);
}
Constant
export const statsConstant = {
STATS: {
ERROR: {
message_1: 'There is a problem connecting to the API.',
message_2: 'Please wait a moment and try again.'
},
ROW_1: [
{
title: 'Market Price USD',
value: this.price.market_price_usd,
error: 'errorMessage_price',
errorMessage_1: 'There is a problem connecting to the API.',
errorMessage_2: 'Please wait a moment and try again.',
subtitle: 'Average USD market price across major bitcoin exchanges',
symbol: 'USD'
},
{
title: 'Average Block Size',
value: this.size,
error: 'errorMessage_size',
errorMessage_1: 'There is a problem connecting to the API.',
errorMessage_2: 'Please wait a moment and try again.',
subtitle: 'The 24 hour average block size in MB.',
symbol: 'Megabytes'
}
],
ROW_2: [
{
title: 'Transactions per Day',
subtitle: 'The aggregate number of confirmed Bitcoin transactions in the past 24 hours.'
},
{
title: 'Mempool Size',
subtitle: 'The aggregate size of transactions waiting to be confirmed.'
}
]
}
}
Template
<div class="row">
<div
class="col-sm-6 stats"
*ngFor="let stat of stats.STATS.ROW_1">
<div class="u-centerX">
<h5>{{stat.title}}</h5>
</div>
<div class="u-centerX">
<div
class="alert alert-danger" role="alert"
*ngIf="stat.error === true">
<p>{{stat.errorMessage_1}}.</p>
<p>{{stat.errorMessage_2}}.</p>
</div>
</div>
<div class="u-centerX">
<div
*ngIf="loadstate"
class="loader">
</div>
<div
*ngIf="price.market_price_usd"
class="stats-data">
<a href="https://blockchain.info/charts/market-price">
<h1>${{stat.value}}</h1>
<span>{{stat.symbol}}</span>
</a>
</div>
</div>
<div class="u-centerX">
<p>{{stat.subtitle}}</p>
</div>
</div>
</div>
You can't use a constant for dynamically changing data. A constant - as its name tells - is constant and can't receive value changes. But as you are requesting your values asynchronously, you have value changes for sure.
But the solution should be simple:
Don't declare statsConstant as const but as a property of your Components' class. Now values can be mutated when your async calls return.
Initialize the dynamic values (e.g. stats.value) with empty values. I would choose null here.
Overwrite the emtpy values with the actual values as soon as your async calls return with the data. After that your view should update automatically.
After that there should be no more need to additionally assign this.price and this.size.
You spoke about refactoring. My suggestion would be to move all that stats data structure and fetching it to an own service. Your component could then only use this single service API to get the whole stats data. Your service would be responsible for building it up. This would be cleaner in my opinion.
Related
Background:
I'm a Python/Vue developer; I've been using Vue since 2016.
I have a client who runs a weight loss / meal planning business: clients pay her to prepare weekly single-page PDF menus that tell them (the clients) exactly what to eat for breakfast, lunch, and dinner of every day of the week. (image of an example menu)
Each meal is shown as a list of ingredients.
Right now she's preparing these menus in Excel, and she hired me to reproduce and extend the functionality of what she has in Excel, but in a Python/Vue app.
The app I'm building for her has many "pages" ("top-level" components) to allow her to add/modify/delete objects like clients, ingredients, and recipes (image), but the most complicated part of the UI is the component in which she can define the meals for every meal of every day of the week (image). That component is named WeeklyMenu.vue.
WeeklyMenu.vue itself contains seven DailyMenu.vue children, one for each day of the week (Monday, Tuesday, etc.). (image)
Each DailyMenu.vue component itself contains four Meal.vue components, one for each of four meal types: Breakfast, Lunch, Dinner, and Snacks. (image)
Important: At the moment, the DailyMenu.vue and Meal.vue components themselves contain their data rather than accessing it from the Vuex store.
For example, the list of ingredients for each meal is contained within the Meal.vue component as a mealIngredients variable within the component's data attribute. (image)
Side-note: This means that there are lots of HTTP requests being sent to the back-end when the page loads as all of the meals are requesting their own data, rather than a single request being sent via a Vuex action (for example). This seems like it can't be best practice.
The problem:
The problem is that she is now asking me to add features in which a change to the data in one subcomponent should update the data in a different subcomponent.
For example, she wants the app to work so that when she has the same recipe in several different Meals of the week, then a change to an ingredient in one of the meals will propagate to the other meals that have the same recipe. (image explanation)
My question:
What is the best practice for handling a situation like this? Should I move the ingredient data into the Vuex store or (in the same vein) the lowest-common-ancestor WeeklyMenu.vue component? If so, how exactly should it work? Should there be a separate variable for each meal? Or should I have an object that contains data for all of the different meals? If I use a single object, do I need to worry that a watcher on that object in the Meal.vue component would be triggering even when a change was made to a different meal's data?
If I store all the meal ingredients in separate variables, I would need to pass all of those to every meal (so every meal would need to receive every other meal's ingredients as separate props). So that doesn't seem like the right way to go.
If a user is making a particular change to a particular meal, how would I only have the other meals with the same name react?
Related links:
Communication between sibling components in VueJs 2.0
I'm looking into whether it would make sense to move the ingredient data up to the level of the WeeklyMenu.vue component as described in the "Lowest Common Ancestor" approach (here and here).
Simplified example of the situation I'm trying to handle:
Without Vuex: https://codepen.io/NathanWailes/pen/zYBGjME
Using Vuex: https://codepen.io/NathanWailes/pen/WNxWxWe
With everything working (including the state being kept in Vuex) except the propagation: https://codepen.io/NathanWailes/pen/KKMYNVZ
Yes, problem domain seems complex enough to more than justify use of Vuex. I would not go with keeping data in components and sharing by props - that doesn't scale well
Keep each Recipe as an object in single object recipes - you don't need to worry about watchers. If one particular Recipe object will change, Vue will re-render only components using same Recipe object (and if done properly you don't even need watchers for that)
Create a "weekly menu" object inside the store
In leaf nodes (Meals) of that object just use some kind of reference (by name or unique ID if you have one) into recipes. As a result multiple Meal.vue components on a menu will use same object in the store and update automatically
I ended up getting it working in a simple example in CodePen, which I'm going to use as a guide when trying to get it working on the actual site.
The summary of my findings with this solution is, "Vue will actually update when the nested entries of a Vuex state object are updated; you don't need to worry about it not detecting those changes. So it's OK to just keep all the data in a single big Vuex store object when you have many duplicate sibling components that need to react to each other."
Here's the CodePen: https://codepen.io/NathanWailes/pen/NWRNgNz
Screenshot
Summary of what the CodePen example does
The data used to populate the menu all lives in the Vuex store in a single weeklyMenu object, which has child objects to break up the data into the different days / meals.
The individual meals have computed properties with get and set functions so that it can both get changes from the store and also update the store.
The DailyMenu and WeeklyMenu components get their aggregate data by simply having computed properties that iterate over the Vuex weeklyMenu object, and it "just works".
I have same-named meals update to match each other by iterating over the meals in the Vuex mutation and looking for meals with the same "Ingredient Name".
The code
HTML
<html>
<body>
<div id='weekly-menu'></div>
<h3>Requirements:</h3>
<ul>
<li>Each row should have all the numbers in it summed and displayed ('total daily calories').</li>
<li>The week as a whole should have all the numbers summed and displayed ('total weekly calories').</li>
<li>If two or more input boxes have the same text, a change in one numerical input should propagate to the other same-named numerical inputs.</li>
<li>Ideally the data (ingredient names and calories) should be stored in one place (the top-level component or a Vuex store) to make it more straightforward to populate it from the database with a single HTTP call (which is not simulated in this example).</li>
</ul>
</body>
</html>
JavaScript
const store = new Vuex.Store(
{
state: {
weeklyMenu: {
Sunday: {
Breakfast: {
name: 'aaa',
calories: 1
},
Lunch: {
name: 'bbb',
calories: 2
},
},
Monday: {
Breakfast: {
name: 'ccc',
calories: 3
},
Lunch: {
name: 'ddd',
calories: 4
},
}
}
},
mutations: {
updateIngredientCalories (state, {dayOfTheWeekName, mealName, newCalorieValue}) {
state.weeklyMenu[dayOfTheWeekName][mealName]['calories'] = newCalorieValue
const ingredientNameBeingUpdated = state.weeklyMenu[dayOfTheWeekName][mealName]['name']
for (const dayOfTheWeekName of Object.keys(state.weeklyMenu)) {
for (const mealName of Object.keys(state.weeklyMenu[dayOfTheWeekName])) {
const mealToCheck = state.weeklyMenu[dayOfTheWeekName][mealName]
const ingredientNameToCheck = mealToCheck['name']
if (ingredientNameToCheck === ingredientNameBeingUpdated) {
mealToCheck['calories'] = newCalorieValue
}
}
}
},
updateIngredientName (state, {dayOfTheWeekName, mealName, newValue}) {
state.weeklyMenu[dayOfTheWeekName][mealName]['name'] = newValue
}
}
}
)
var Meal = {
template: `
<td>
<h4>{{ mealName }}</h4>
Ingredient Name: <input v-model="ingredientName" /><br/>
Calories: <input v-model.number="ingredientCalories" />
</td>
`,
props: [
'dayOfTheWeekName',
'mealName'
],
computed: {
ingredientCalories: {
get () {
return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['calories']
},
set (value) {
if (value === '' || value === undefined || value === null) {
value = 0
}
this.$store.commit('updateIngredientCalories', {
dayOfTheWeekName: this.dayOfTheWeekName,
mealName: this.mealName,
newCalorieValue: value
})
}
},
ingredientName: {
get () {
return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['name']
},
set (value) {
this.$store.commit('updateIngredientName', {
dayOfTheWeekName: this.dayOfTheWeekName,
mealName: this.mealName,
newValue: value
})
}
}
}
};
var DailyMenu = {
template: `
<tr>
<td>
<h4>{{ dayOfTheWeekName }}</h4>
Total Daily Calories: {{ totalDailyCalories }}
</td>
<meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Breakfast" />
<meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Lunch" />
</tr>
`,
props: [
'dayOfTheWeekName'
],
data: function () {
return {
}
},
components: {
meal: Meal
},
computed: {
totalDailyCalories () {
let totalDailyCalories = 0
for (const mealName of Object.keys(this.$store.state.weeklyMenu[this.dayOfTheWeekName])) {
totalDailyCalories += this.$store.state.weeklyMenu[this.dayOfTheWeekName][mealName]['calories']
}
return totalDailyCalories
}
}
};
var app = new Vue({
el: '#weekly-menu',
template: `<div id="weekly-menu" class="container">
<div class="jumbotron">
<h2>Weekly Menu</h2>
Total Weekly Calories: {{ totalWeeklyCalories }}
<table class="table">
<tbody>
<daily_menu day-of-the-week-name="Sunday" />
<daily_menu day-of-the-week-name="Monday" />
</tbody>
</table>
</div>
</div>
`,
data: function () {
return {
}
},
computed: {
totalWeeklyCalories () {
let totalWeeklyCalories = 0
for (const dayOfTheWeekName of Object.keys(this.$store.state.weeklyMenu)) {
let totalDailyCalories = 0
for (const mealName of Object.keys(this.$store.state.weeklyMenu[dayOfTheWeekName])) {
totalDailyCalories += this.$store.state.weeklyMenu[dayOfTheWeekName][mealName]['calories']
}
totalWeeklyCalories += totalDailyCalories
}
return totalWeeklyCalories
}
},
components: {
daily_menu: DailyMenu
},
store: store
});
I'm using this article:
https://cloud.google.com/identity-platform/docs/admin/manage-mfa-users
I added code snippet for update and create user.
In both cases I get error:
Object literal may only specify known properties, and 'phoneNumber'
does not exist in type 'UpdateMultiFactorInfoRequest'.
I checked in code and enrolled factors is UpdateMultiFactorInfoRequest type.
And this interface doesn't have any phoneNumber.
However, there is UpdatePhoneMultiFactorInfoRequest interface, that extends previous one.
And this interface has phoneNumber.
This is my piece of code:
admin.auth().updateUser('123456789', {
multiFactor: {
enrolledFactors: [
{
// uid will be auto-generated.
phoneNumber: '+16505550003', //here is an error
displayName: 'Spouse\'s phone',
factorId: 'phone',
}
],
},
})
.then((userRecord) => {
console.log(userRecord.multiFactor.enrolledFactors);
})
.catch((error) => {
console.log(error);
})
Does someone did it?
Maybe I'm doing some wrong?
But I just copied snippet from docs.
I'm trying to display data retrieved from a server (using Angular 6, Rxjs and Chartjs), and render a chart using the data.
If I use local mock data, everything renders just fine. But if I use get the data from the servers, the necessary data to render the graphs isn't available so the charts render as blank charts.
Summary:
A component makes a service call, and prepares an object to pass down to a child component using the response from the service call. However, by the time the response is ready, the object is already sent without the necessary information.
Service code snippet:
getAccountsOfClientId(clientID: string): Observable<Account[]> {
return this.http.get<Account[]>(`${this.BASE_URL}/accounts?client=${clientID}`)
.pipe(
tap(accounts => console.log('fetched client\'s accounts')),
catchError(this.handleError('getAccountsOfClientId', []))
);
}
In client-info.component.ts (component to make the service call, and prepare and pass the object to child component)
#Input() client; // received from another component, data is filled
constructor(private clientAccountService: ClientAccountService) { }
ngOnInit() {
this.getAccountsOfClientId(this.client.id);
}
ngAfterViewInit() {
this.updateChart(); // render for pie chart
this.updateBarChart(); // render for bar chart
}
getAccountsOfClientId(clientID: string): void {
this.clientAccountService.getAccountsOfClientId(this.client.id)
.subscribe(accounts => this.clientAccounts = accounts);
}
updateBarChart(updatedOption?: any): void {
/* unrelated operations above ... */
// Create new base bar chart object
this.barChart = {};
this.barChart.type = 'bar';
this.setBarChartData();
this.setBarChartOptions('Account', 'Balance');
}
setBarChartData(): void {
// field declarations..
console.log('clientAccounts has length: ' + this.clientAccounts.length); // prints 0
this.clientAccounts.map((account, index) => {
// do stuff
});
dataset = {
label: 'Balance',
data: data,
...
};
datasets.push(dataset);
// since clientAccounts was empty at the time this function ran, the "dataset" object doesn't contain
// the necessary information for the chart to render
this.barChart.data = {
labels: labels,
datasets: datasets
};
}
I'm looking for changes using ngOnChanges (in the child component), however the chart data is NOT updated in the child component after the "clientAccounts" array is filled with the response.
#Input() chart: Chart;
#Input() canvasID: string;
#Input() accountBalanceStatus: string;
ngOnChanges(changes: SimpleChanges) {
if (changes['accountBalanceStatus'] || changes['chart']) {
this.renderChart();
}
}
renderChart(): void {
const element = this.el.nativeElement.querySelector(`#${this.canvasID}`);
if (element) {
const context = element.getContext('2d');
if (this.activeChart !== null) {
this.activeChart.destroy();
}
this.activeChart = new Chart(context, {
type: this.chart.type,
data: this.chart.data,
options: this.chart.options
});
} else {
console.log('*** Not rendering bar chart yet ***');
}
}
Can you point me to how I should continue my research on this?
Sorry for the long question, and thanks!
EDIT: Upon request, the templates are below
Parent (client-info):
<div class='client-info-container'>
<div class='info-container'>
<li>Date of Birth: {{ client.birthday | date: 'dd/MM/yyyy' }}</li>
<li>Name: {{ client.name }}</li>
<li>First Name: {{ client.firstname }}</li>
</div>
<div class='more-button'>
<button (click)='openModal()'>More</button>
</div>
<div class='chart-container'>
<div *ngIf='pieChart && client'>
<app-balance-pie-chart
[chart]='pieChart'
[canvasID]='accountBalancePieChartCanvasID'
(updateChart)='handlePieChartOnClick($event)'>
</app-balance-pie-chart>
</div>
<div class='bar-chart-container'>
<div class='checkbox-container'>
<div *ngFor='let option of cardTypeCheckboxOptions' class='checkbox-item'>
<input
type='checkbox'
name='cardTypeCheckboxOptions'
value='{{option.value}}'
[checked]='option.checked'
[(ngModel)]='option.checked'
(change)="updateCardTypeCheckboxSelection(option, $event)"/>
<p>{{ option.name }} {{ option.checked }}</p>
</div>
</div>
<div *ngIf='barChart && client'>
<!-- *ngIf='client.accounts.length === 0' -->
<div class="warning-text">This client does not have any accounts.</div>
<!-- *ngIf='client.accounts.length > 0' -->
<div>
<app-balance-account-bar-chart
[chart]='barChart'
[canvasID]='accountBarChartCanvasID'
[accountBalanceStatus]='accountBalanceStatus'>
</app-balance-account-bar-chart>
</div>
</div>
</div>
</div>
</div>
Chart:
<div class='bar-chart-canvas-container' *ngIf='chart'>
<canvas id='{{canvasID}}' #{{canvasID}}></canvas>
</div>
ngOnChanges(changes: SimpleChanges) {
if (changes['accountBalanceStatus'] || changes['chart']) {
this.renderChart();
}
}
ngOnChanges's argument value is type of SimpleChanges for each Input()
prop:
class SimpleChange {
constructor(previousValue: any, currentValue: any, firstChange: boolean)
previousValue: any
currentValue: any
firstChange: boolean
isFirstChange(): boolean
}
You should check you data by previousValue, currentValue.
Something like:
if(changes.accountBalanceStatus.previousValue != changes.accountBalanceStatus.currentValue
|| changes.chart.previousValue != changes.chart.currentValue){
this.renderChart();
}
StackBlitz Demo
I saw that, you are not assigning the data directly to this.barChart instead you are assigning it as this.barChart.data, which means you are modifying the property directly, which might not invoke the ngOnChanges of the child component. This is due to the explanation that you have given in your comments.
I read that it may be because angular change detection checks the
differences by looking at the object references
And it will not get to know when the property of object gets changed
The variable that is bound to #Input() property is this.barChart and not this.barChart.data.
Instead of
this.barChart.data = {
labels: labels,
datasets: datasets
};
You try this
this.barChart = {
data : {
labels: labels,
datasets: datasets
}};
here you are directly modifying this.barChart which should trigger ngOnChanges().
EDIT :
You should be invoking this.updateChart(); inside subscribe block of
this.clientAccountService.getAccountsOfClientId(this.client.id)
.subscribe((accounts) => {
this.clientAccounts = accounts;
this.updateChart();
})
That is why you also have this.clientAccounts.length as 0
Your component needs to have the data before rendering. You may use resolve, a built in feature that Angular provides to handle use-cases like the ones you described.
Also look here. may be a useful resource in a tutorial form.
You need to interact with child components from parent this you nned to use input binding.
Refer:
https://angular.io/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding
My issue is solved and I'd like to share the solution in case anyone needs it in the future. As Amit Chigadani suggested (in the comments), invoking my chart updating functions in the subscribe block worked.
getAccountsOfClientId(clientID: string): void {
this.clientAccountService.getAccountsOfClientId(this.client.id)
.subscribe(accounts => {
this.clientAccounts = accounts;
this.updateChart();
this.updateBarChart();
});
}
I'm Vue.js newbie and my task is:
make an ajax call (GET) to server, using RESTful API (Laravel on background)
retrieve a (JSON) list of Form CRUD items in array (like checkbox, input text, textarea...) with their properties (value, checked, custom classes...)
render CRUD form with these form items maybe using Vue's loop
I'm wondering if it could be rendered using components somehow. But I don't know the correct way.
Frankly, I exactly don't know how to solve this problem with Vue.js - rendering items from array and each item has it's own markup and properties (checkbox has it's own, textbox, select, textarea...).
I'm building a web application based on CRUD operations and I'm trying to write universal components. The easiest way is to do a special component with hard-written sub-components for each subpage, but I don't like this way if not needed.
Thank you!
EDIT: I don't have much code yet, but this is where I am...
<script>
// ./components/CrutList.vue
export default {
mounted() {},
data() {
return {
items: []
}
},
props: ['resource'],
methods: {
getItems() {
var resource = this.$resource('api/'+this.resource+'{/id}');
resource.get({}).then(function(items){
if(items.body.status == 'success'){
this.items = items.body.items;
}
}).bind(this);
},
deleteItem(item) {
// perform CRUD operation DELETE
alert('delete action');
}
}
}
</script>
My idea is using CrudList component to CRUD listing...
<crud-list resource="orders">
In laravel I do something like this:
return response()->json([
'status' => 'success',
'items' => [
[
'itemComponent' => 'checkbox',
'props' => [
'checked' => true,
'label' => "Checkbox č.1",
'name' => 'checkbox1'
]
],
[
'itemComponent' => 'checkbox',
'props' => [
'checked' => true,
'label' => "Checkbox č.2",
'name' => 'checkbox2'
]
],
[
'itemComponent' => 'checkbox',
'props' => [
'checked' => true,
'name' => 'checkbox3'
]
],
],
]);
...it's very simplified, but it's just example of what I'm doing.
Now the problem is:
take the 'itemComponent' part from the returned array item (this is in a loop),
if it's a checkbox, take (for example) Checkbox.vue component, fill it with properties ('props' part of the array item)
I read about slots, but it's not what I'm looking for. Is there something I can use for dynamic components?
Check out this jsFiddle working example for dynamic forms:
https://jsfiddle.net/mani04/kr8w4n73/1/
You can do it easily by using a lot of v-ifs for each and every form element type you might get from server. It is a bit cumbersome but I can't find any other way.
In the above example, I have the form structure as follows:
var formItems = [{
input_type: "text",
input_label: "Login",
values: {
value: "your_name#example.com"
}
},
{...},
{...}];
Once you have that data, then it is a matter of iterating through formItems, checking input_type and activating the relevant form control.
Here is how my dynamic form template looks like, for the above input:
<div v-for="formItem in formValues">
<div v-if="formItem.input_type == 'text'">
<input type="text" v-model="formItem.values.value">
</div>
<div v-if="formItem.input_type == 'password'">
<input type="password" v-model="formItem.values.value">
</div>
<div v-if="formItem.input_type == 'checkbox'">
<input type="checkbox" v-model="formItem.values.checked">
{{formItem.values.label}}
</div>
</div>
My jsFiddle example uses form-horizontal from bootstrap, and I am also able to display the labels well. If I put that in the example above, it will get cluttered and will not let you see how it works.
Hope it helps! You can change the formItems data structure to meet your needs, and modify the template accordingly.
I have a factory, which goes into a controller, and I am trying to get data from that display on an HTML page. I am having trouble specifying an Object's pathway however.
My Factory:
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
apis:
[{
accounts: [
{
v1: [
{
uri: Head+"/v1/accounts/",
item1: "AccountNumber",
item2: "MoneyInAccount"
}],
v2: [
{
uri: Head+"/v2/accounts/",
item1: "AccountNumber",
item2: "MoneyInAccount"
}]
}
],
customers: [
{
v1: [
{
uri: Head+"/v1/customers/",
item1: "CustomerName",
item2: "CustomerID",
item3: "CustomerEmail"
}]
}
]
}]
};
});
My Controller:
app.controller('APIController', function($scope, APIMethodService) {
$scope.title = "API";
$scope.apiList = APIMethodService;
$scope.accountList = $scope.apiList.accounts.v1;
$scope.accountList2 = $scope.apiList[0][0];
});
My HTML
<div ng-controller="APIController">
<div id="api" class="row">
<div class="col-xs-12">
<div class="row" style="font-size:20px">
{{title}} Page!
<table class="table table-striped">
<tr ng-repeat="api in apiList | orderBy:'uri' | filter:search">
<td>{{api.uri}}</td>
<td>{{api.item1}}</td>
<td>{{api.item2}}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
The errors I get are in regards to the Controller trying to parse out the individual objects I wish to grab, like accounts or customers, and then any version v#, they may have.
So it will say something such as
TypeError: Cannot read property 'v1' of undefined
I just need some help specifying the proper pathways into my factory service.
You have a few problems. First, you are referring to the object returned from the factory incorrectly. APIMethodService is the factory that you're injecting, so you need to first reference the object that that factory is returning like this:
APIMethodService.apis
This will give you your entire JSON object.
From there, the rest of your object is made up of arrays of objects, so referring to 'v1' won't do you any good. You need to specify an index instead. If you want v1, you'll need:
APIMethodService.apis[0].accounts[0].v1
This will give you the v1 array, which again is an array of objects.
Customers would be:
APIMethodService.apis[0].customers[0].v1
The first problem you have is that the factory returns an object with a single property called apis. So basically this $scope.apiList.accounts.v1 should be $scope.apiList.apis.accounts.v1. Bu that's not all as this won't either work since dotting(.) into apis is an array you'd have to use the index. In this case it would be $scope.apiList.apis[0] and then you could .accounts[0].v1 which is also an array containing a single object.
Now if you can I would suggest to you that you'd change how you represent this data structure.
This is how you could do it.
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
accounts: {
v1: {
uri: Head+"/v1/accounts/",
items: ["AccountNumber","MoneyInAccount"]
},
v2: {
... // skipped for brevity
}
},
customer: {
... // code skipped for brevity
}
};
});
And then it's just a matter of dotting into your APIMethodService-object like APIMethodService.accounts.v1.items[0] if you want the AccountNumber method name.
Constructing your url could then be done like this.
var baseUrl = APIMethodService.accounts.v1.uri; // 'api.example.com'
var url = baseUrl + APIMethodService.accounts.v1.items[0]; // 'AccountNumber'
// url = "api.example.com/v1/accounts/AccountNumber"
Again, this is one way you could do it but this can be further enhanced upon. The examples I provided are simply for demo purposes and this is not in any way the only way to do it.
Expanding upon recieved comments/questions your service (and data representation) could now look like this.
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
accounts: {
v1: {
uri: Head+"/v1/accounts/",
items: [
{
name:'AccountNumber',
description:'Show the account number'
},
{
name:'AccountOwner',
description:'Show information about the owner of the account'
},
{
name:'MoneyInAccount',
description:'Show money in the Account'
}
]
},
v2: {
... // skipped for brevity
}
},
customer: {
... // code skipped for brevity
}
};
});
// Get descriptions
var accountNumberDescription = APIMethodService.accounts.v1.items[0].description; // 'Show the account number'
var accountOwnerDescription = APIMethodService.accounts.v1.items[1].description; // 'Show information about the owner of the account'
var moneyInAccountDescription = APIMethodService.accounts.v1.items[2].description; // 'Show money in the Account'
By using objects with properties like this it's alot easier to understand what you are trying to do. With arrays with indexes you'd have to know or take a look at the source to see what's going on. Here, someone viewing your code they can instantly understand that it is the description you are getting.