How to properly pass a variable to an Ember's class?
Controller:
import Controller from '#ember/controller';
import Object from '#ember/object';
function totalVotes(company) {
return company.upvotes + company.downvotes;
}
function calcPercent(company) {
return (company.upvotes * 100 / (company.upvotes + company.downvotes)).toFixed(2);
}
function percentComparator(a, b) {
return calcPercent(b) - calcPercent(a);
}
var Company = Object.extend({
score: function() {
return (this.get('upvotes') * 100 / totalVotes(this)).toFixed(2);
}.property('upvotes', 'downvotes')
});
var AppModel = Object.extend({
topCompanies: function() {
return this.get('companies')
.sort(percentComparator)
.slice(0, 8);
}.property('companies.#each.upvotes', 'companies.#each.downvotes'),
});
var appModel = AppModel.create({
companies: getCompaniesJSON().map(function(json) {
return Company.create(json);
})
});
export default Controller.extend({
topCompanies: appModel.topCompanies,
});
Template:
<ul>
{{#each topCompanies as |company|}}
<li>{{company.title}} {{company.score}}%</li>
{{/each}}
</ul>
The result of the above in the browser console:
jquery.js:3827 Uncaught TypeError: Cannot read property 'sort' of undefined
this.get('companies') is undefined. Why? I'm passing companies to AppModel.create. What am I doing wrong?
var appModel = AppModel.create({
companies: getCompaniesJSON().map(function(json) {
return Company.create(json);
})
});
should something like this (untested):
var appModel = AppModel.create({
init() {
this._super(...arguments);
this.set('companies', getCompaniesJSON().map((json) => {
return Company.create(json);
});
}
});
i'm assuming the code is written this way (with global variables) for illustrative purposes and so i will ignore the other issues with the code sample that are likely not present in your real code, such as the property in your controller needing to be a computed.alias instead of direct assignment etc.
Related
I have component MyComponent.vue where I have data value that constantly changes. I want to pass this value to javascript file(js file should know about changes of value everytime)
Why do I do that? Because my regular js file is a service layer for axios methods. I can import this file in many other components. The file contains axios methods and urls are dynamic.
I want those urls depend on data variable. This data variable comes from MyComponent.js
So the main goal is to make dynamic urls of axios that depend on data variable
I tried some code but it doesn't work, because js file(CategoryService.js) know nothing about this.categoryNumber.
MyComponent.vue:
<script>
export default {
data() {
return {
categoryNumber: 1
}
}
}
</script>
CategoryService.js
import http from "../../../http-common";
let category = "category1";
if (this.categoryNumber === 1) {
category = "category1";
} if (this.categoryNumber === 2) {
category = "category2";
}
class CategoryService {
get(id) {
return http.get(`/${category}/${id}`);
}
update(id, data) {
return http.put(`/${category}/${id}`, data);
}
create(data) {
return http.post(`/${category}`, data);
}
delete(id) {
return http.delete(`/${category}/${id}`);
}
getAll() {
return http.get(`/${category}/all`);
}
}
export default new CategoryService();
So with a bit of refactoring, you could easily get this working.
First of all, I would put the if/else logic of your class into it.
For convenience and scalability, I would use a Vuex store that will keep track of your categoryNumber and share it accross all your components.
Then I would bind my service to my Vue instance so I can easily access it in all my components as well as the store and I would pass the latter to my class as a parameter.
For the last part, I don't know the logic in the http-common file so the code I will show you is a bit nasty. But depending on wether or not you bound 'http' to axios, you could make use of axios interceptors to call the getCategoryNumber() method in every request.
Here's an idea of the implementation I would go for:
const CategoryService = class CategoryService {
constructor(store) {
this._store = store;
this.category = "category1";
}
getCategoryNumber() {
if (this._store.state.categoryNumber === 1) {
this.category = "category1";
}
if (this._store.state.categoryNumber === 2) {
this.category = "category2";
}
console.log(this.category); // for demo puprose
}
get(id) {
this.getCategoryNumber(); // We could use axios request interceptor instead of calling that in every route, but that works !
return http.get(`/${this.category}/${id}`);
}
update(id, data) {
this.getCategoryNumber();
return http.put(`/${this.category}/${id}`, data);
}
create(data) {
this.getCategoryNumber();
return http.post(`/${this.category}`, data);
}
delete(id) {
this.getCategoryNumber();
return http.delete(`/${this.category}/${id}`);
}
getAll() {
this.getCategoryNumber();
return http.get(`/${this.category}/all`);
}
}
const store = new Vuex.Store({
state: {
categoryNumber: 1
},
mutations: {
setCategoryNumber(state, payload) {
state.categoryNumber = payload;
}
}
});
// Bind your service to the Vue prototype so you can easily use it in any component with 'this.$service'
// pass it the store instance as parameter
Vue.prototype.$service = new CategoryService(store);
new Vue({
el: "#app",
store, // dont forget to bind your store to your Vue instance
methods: {
updateCategoryNumber() {
// Put here any logic to update the number
this.categoryNumber = this.categoryNumber === 1 ? 2 : 1;
this.checkServiceCategoryValue();
},
checkServiceCategoryValue() {
// for demonstration purpose
this.$service.getCategoryNumber();
}
},
computed: {
// Look for the store value and update it
categoryNumber: {
get() {
return this.$store.state.categoryNumber;
},
set(value) {
this.$store.commit("setCategoryNumber", value);
}
}
}
});
<div id="app">
<h2>number: {{ categoryNumber }}</h2>
<button type="button" #click="updateCategoryNumber()">
updateCategoryNumber
</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vuex#2.0.0"></script>
Thanks to #Solar
I just added one more parameter for all urls and put the number of category to it
CategoryService.js:
class CategoryOneService {
get(id, category) {
return http.get(`/${category}/${id}`);
}
getAll(category) {
return http.get(`/${category}/all`);
}
}
functions.js:
let catNum = "";
function getQuestion() {
if (this.categoryNumber === 1) {
catNum = "category1";
}
if (this.categoryNumber === 2) {
catNum = "category2";
}
let questionId = this.questionNumber;
CategoryOneService.get(questionId, catNum)
.then(response => {
this.question = response.data.question;
this.answer = response.data.answer;
})
.catch(error => {
console.log(error);
});
}
var app = angular.module('testApp', []);
class Component {
constructor(app, name, template, as, bindings) {
this.bindings = bindings;
this.config = {}
this.config.template = template;
this.config.controllerAs = as;
// pre-create properties
this.config.controller = this.controller;
this.config['bindings'] = this.bindings;
app.component(name, this.config);
console.log("Inside Component ctor()");
}
addBindings(name, bindingType) {
this.bindings[name] = bindingType;
}
controller() {
}
}
class App extends Component {
constructor(app) {
var bindings = {
name: "<"
};
super(app, "app", "Hello", "vm", bindings);
}
controller() {
this.$onInit = () => this.Init(); // DOESN'T WORK
/*
var self = this;
self.$onInit = function () { self.Init(); }; // DOESN'T WORK
*/
/*
this.$onInit = function () { // WORKS
console.log("This works but I don't like it!");
};
*/
}
Init() {
console.log("Init");
}
onNameSelected(user) {
this.selectedUser = user;
}
}
var myApp = new App(app);
<div ng-app="testApp">
<app></app>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.5/angular.js"></script>
I'm trying to "classify" angular 1.5's .component(). I can get most of it figured out but when I try to assign a class method for $onInit it doesn't work. I've tried assigning to it and using arrow notation to call back to the class method but neither work. It does work if I assign an anonymous function directly but I don't want to do that. I want those functions to point to class methods because I find it cleaner.
So ultimately I want my App classes Init() method to get called for $onInit(). Is it possible?
I'm very new to JavaScript MVC logic. I'm trying to create a simple app.
I've set up everything like in the example (In seperate files ofc). Data.js which is not shown contains an object with some data, and template.js contains my html template strings.
// CONTROLLER
import Model from './models/model';
import View from './views/view';
import Controller from './controllers/controller';
import { $on } from './utility/utility';
export default class Controller {
constructor(Model, View) {
this.model = Model;
this.view = View;
};
loadInit() {
console.log("controller render");
this.model.test();
// this.view.render(this.model.data);
};
}
// MODEL
import data from '../config/data'
data.initialize();
export default class Model {
constructor() {
this.data = data;
};
test() {
console.log("Test works");
};
generateLists() {
window.infiniteList = [];
var articlesList = 'http://lib.lrytas.lt/api/articlesList/article.php?term=/lrytas/' + data.blockTag.slug + '/*&domain=' + data.blockTag.domain;
$.get(articlesList, function(response) {
for(let i = 0; i < response.blockTop7.length; i++) {
if (response.blockTop7[i].n18 == "0") {
window.infiniteList.push(response.blockTop7[i].main_id);
}
};
for(let i = 0; i < response.newBlock.length; i++) {
if (response.newBlock[i].n18 == "0") {
window.infiniteList.push(response.newBlock[i].main_id);
}
};
});
console.log(infiniteList);
};
loadArticle(data) {
$.get(lr_config.kolumbusApi + 'query/?kpm3id=' + infiniteList[0] + '&ret_fields=props', function(response) {
let result = response.result["0"].props;
this.data.update(result);
});
window.infiniteList.splice(0,1);
};
}
// VIEW
import config from '../config/config'
import Template from '../template/template'
import data from '../config/data'
data.initialize();
export default class View {
constructor() {
this.el = config.mainWrapper;
};
render(Data) {
console.log("view render");
this.el.innerHTML += Template(Data);
};
}
// APP.JS
class App {
constructor() {
this.model = new Model();
this.view = new View();
this.controller = new Controller(Model, View);
};
}
const app = new App();
const init = () => {
app.controller.loadInit();
}
$on(window, 'load', init);
Heres JSBIN.
Now the problem is when I'm trying to call init function on load in main.js. It goes to controller, calls the loadInit() function which then should call this.model.test(), but it wouldnt work and it gives me the following error
"Uncaught TypeError: this.model.test is not a function".
I've been trying to find a solution for past couple hours but I'm really lost here.
Any help would be greatly appreciated. Thanks!
i want to share data between components, so im implemented a Service which has an EventEmitter.
My Service looks like this:
#Injectable()
export class LanguageService {
constructor() {
this.languageEventEmitter = new EventEmitter();
this.languages = [];
this.setLanguages();
}
setLanguages() {
var self = this;
axios.get('/api/' + api.version + '/' + api.language)
.then(function (response) {
_.each(response.data, function (language) {
language.selected = false;
self.languages.push(language);
});
self.languageEventEmitter.emit(self.languages);
})
.catch(function (response) {
});
}
getLanguages() {
return this.languages;
}
toggleSelection(language) {
var self = this;
language.selected = !language.selected;
self.languages.push(language);
self.languageEventEmitter.emit(self.languages);
}
}
I have to components, which are subscribing to the service like this:
self.languageService.languageEventEmitter.subscribe((newLanguages) => {
_.each(newLanguages, function (language) {
self.updateLanguages(language);
});
});
When both components are loaded, the language arrays get filled as i wish.
This is the first component:
export class LanguageComponent {
static get parameters() {
return [[LanguageService]];
}
constructor(languageService) {
var self = this;
this.languageService = languageService;
this.languages = [];
this.setLanguages();
}
setLanguages() {
var self = this;
self.languageService.languageEventEmitter.subscribe((newLanguages) => {
_.each(newLanguages, function (language) {
self.updateLanguages(language);
})
});
}
updateLanguages(newLanguage) {
var self = this;
if (!newLanguage) {
return;
}
var match = _.find(self.languages, function (language) {
return newLanguage._id === language._id;
});
if (!match) {
self.languages.push(newLanguage);
}
else {
_.forOwn(newLanguage, function (value, key) {
match[key] = value;
})
}
toggleLanguageSelection(language) {
var self = this;
self.languageService.toggleSelection(language)
}
}
When LanguageComponent executes the function toggleLanguageSelection() which triggered by a click event, the other component, which subscribes like this:
self.languageService.languageEventEmitter.subscribe((newLanguages) => {
_.each(newLanguages, function (language) {
self.updateLanguages(language);
})
});
doesn't get notfiefied of the change. I think this happens because both component get a different instance of my LanguageService, but i'm not sure about that. I also tried to create a singleton, but angular'2 di doesn't work then anymore. What is the reason for this issue and how can i solve this ?
You need to define your shared service when bootstrapping your application:
bootstrap(AppComponent, [ SharedService ]);
and not defining it again within the providers attribute of your components. This way you will have a single instance of the service for the whole application. Components can leverage it to communicate together.
This is because of the "hierarchical injectors" feature of Angular2. For more details, see this question:
What's the best way to inject one service into another in angular 2 (Beta)?
I am having some issues with Ember.js and loading resources asynchronously (aka I don't know what I'm doing). This is what I have. It currently doesn't update the list parameter after receiving the data. The template does not render anything. Thanks for any help.
Utilities
import Ember from 'ember';
export var page = Ember.Object.extend({
type: null,
list: Ember.computed('type', function() {
var type = this.get('type');
var url = "/test/" + type + "/test2";
if (type) {
getArray(url).then(function(list) {
return list;
});
} else {
return [];
}
})
});
export function get(url) {
return Ember.$.ajax({
url: url,
type: 'GET',
dataType: 'text',
cache: true,
xhrFields: {
withCredentials: true
}
});
}
export function getArray(url) {
return get(url).then(
function(file) {
var array = file.split("\n");
array.pop();
return array;
},
function() {
return ["Error!"];
}
);
}
Route
import util from 'app/utils/utilities';
export default Ember.Route.extend({
model(params) {
var p = util.page.create({
type: params.log_type
});
return p;
}
});
Template
{{#each model.list as |item|}}
<li>{{item}}</li>
{{/each}}
Consider the following part of your code:
if (type) {
getArray(url).then(function(list) {
return list;
});
} else {
return [];
}
This is not going to do what you think it does. return list returns list as the value of the promise, but you are not then doing anything with that promise. In particular, be clear that the computed property list will not take on that value. When the if (type) branch is taken, the computed property list will have the value undefined (since it's not returning anything in that case).
Remember that model wants you to return a promise (at least, if you want it to do its thing, which is to wait for the promise to resolve, then proceed with the transition, then use the resolved value of the promise to call afterModel and setupController etc.) Therefore, instead of making list a computed property, make it a regular method which returns a promise for the model hook on your route to consume:
import Ember from 'ember';
export var page = Ember.Object.extend({
type: null,
list: function() {
var type = this.get('type');
var url = "/test/" + type + "/test2";
if (!type) return [];
return getArray(url);
});
Then in your route
import util from 'app/utils/utilities';
export default Ember.Route.extend({
model(params) {
var p = util.page.create({
type: params.log_type
});
return p.list();
}
});
Your model will then be the list, so in the template:
{{#each model as |item|}}
<li>{{item}}</li>
{{/each}}