I am working with the ArcGIS javascript api, which is built on require and the asynchronous module defintion. To create a map, you define all your action inside the callback of the AMD require statement:
require([
"esri/Map",
"esri/views/MapView"
], function(Map, MapView){
const map = new Map({ ... })
const view = new MapView({ ... })
})
I would like to be able to initialize this behavior on command from another module, as well as get access to the map and view objects that are defined within the AMD callback. In order to be able to initialize this on command, I can wrap it in a function, and export that function:
export const makeMap = () => {
require([esri modules], function(Map, MapView){
map = new Map({ ... })
view = new MapView({ ... })
})
}
I can import makeMap into some other module in my code, and call it. This is working nicely. However, I am trying to figure out how I can then access the map and view objects to be able to manipulate them through the UI. First I tried this:
// mapMap.js
export const makeMap = () => {
let mapInstance;
let viewInstance;
require([esri modules], function(Map, MapView){
map = new Map({ ... })
view = new MapView({ ... })
mapInstance = map
viewInstance = view
})
return { mapInstance, viewInstance }
}
// some other module
import { makeMap } from './makeMap'
const { mapInstance, viewInstance } = makeMap()
This obviously does not work - mapInstance and viewInstance are undefined, as the function that defines them inside the AMD callback runs after they are returned from the makeMap() call.
I am not sure how I can get a returned value from the AMD callback. Is this possible? Do I need another paradigm?
One other thing I tried is to pass in a reference-holder object, apply the reference to that object, and then just retrieve them from there when needed. It works, but I feel its not as clean:
// maprefs.js
export const maprefs = {}
// makeMap.js
import { maprefs } from './maprefs'
export const makeMap = (maprefs, mapname) => {
require([esri modules], function(Map, MapView){
map = new Map({ ... })
view = new MapView({ ... })
maprefs[mapname] = { map, view }
})
}
// some module
import { maprefs } from './maprefs'
import { makeMap } from './makeMap'
makeMap(maprefs, "map1")
someButton.addEventListener('click', () => {
// maprefs.map1 is properly defined as { mapInstance, viewInstance } and I can take action on it
maprefs.map1.doSomething
})
I took a look at How do I return the response from an asynchronous call?, which is about retuning values from ajax calls, but I'm struggling to relate that to AMD callbacks.
Is it possible to return the value from the AMD callback to be used elsewhere? I am hoping for a const { map, value } = makeMap() type syntax.
In the scenario that you mention, I think that the best solution is to use esri-loader. Is a tiny library built by ESRI for this purpose exactly, that is, load the ArcGIS JS API library modules at runtime.
ArcGIS Docs - esri-loader
Github ESRI - esri-loader usage
Related
Let's say we are creating a module called app by constructing a new vm.SourceTextModule object:
const context = {
exports: {},
console, // custom console object
};
const sandbox = vm.createContext(context);
const app = new vm.SourceTextModule(
`import path from 'path';
console.log(path.resolve('./src'));`,
{
context: sandbox,
}
);
According to the Node.js documentation to obtain the default export from path module we should "link" the imported dependencies of app module to it.
To achieve this we should pass linker callback to app.link method:
async function linker(specifier, referencingModule) {
// the desired logic...
}
await app.link(linker);
How to implement linker function properly so that we could import path module in newly created app module and use it:
await app.evaluate(); // => /home/user/Documents/project/src
P.S. We are using TypeScript, so I checked if we have installed types for path package.
package.json:
"#types/node": "^17.0.31",
I found https://github.com/nodejs/node/issues/35848 where someone posted a code snippet.
From there I've adapted the following linker callback:
const imports = new Map();
async function linker(specifier, referencingModule) {
if (imports.has(specifier))
return imports.get(specifier);
const mod = await import(specifier);
const exportNames = Object.keys(mod);
const imported = new vm.SyntheticModule(
exportNames,
() => {
// somehow called with this === undefined?
exportNames.forEach(key => imported.setExport(key, mod[key]));
},
{ identifier: specifier, context: referencingModule.context }
);
imports.set(specifier, imported);
return imported;
}
The code snippet from the GitHub issue didn't work for me on Node 18.7.0 as is, because the evaluator callback passed to the constructor of SyntheticModule is somehow called with this set to undefined. This may be a Node bug.
I also cached the imported SyntheticModules in a Map because if they have internal state, creating a new SyntheticModule every time will reset that state.
I'm currently trying to write some test with jest (newbie). Regarding to my latest post, I've got the following situation:
I've got multiple price fields. Each price field has its own renderer. A renderer has a reference to its pricefield. A pricefield can determine some html snippet from the dom.
// priceField.js
class PriceField {
constructor() {
const renderer = new PriceFieldRenderer(this);
// do something with renderer
}
/**
* Determines Node in DOM for this price field
*/
determinePriceFieldNode() {
return document.querySelector(".price-field");
}
}
export default PriceField;
// priceFieldRenderer.js
class PriceFieldRenderer {
constructor(priceField) {
this.priceFieldNode = priceField.determinePriceFieldNode();
}
}
export default PriceFieldRenderer;
Now I want to test PriceFieldRenderer (with jest). Therefore, I need to mock PriceField and its determinePriceFieldNode() function.
However, I'm not able to pass a mocked price field instance as constructor argument. I've read the official documentation with its sound-player example and googled about 2 hours. Also i tested different implementations, but I'm not able to solve it. Here is one code snippet which throws following error: TypeError: priceField.determinePriceFieldNode is not a function
jest.mock("./priceField");
import PriceFieldRenderer from "./priceFieldRenderer";
import PriceField from "./priceField";
describe("Price field renderer", () => {
test("with calculated price", () => {
const priceField = PriceField.mockImplementation(() => {
return {
determinePriceFieldNode: () => "<html/>"
};
});
const sut = new PriceFieldRenderer(priceField);
expect(sut).toBeDefined();
});
});
I'm sure the way I did is not the best and there is some help out there :)
The problem here is that priceField is lowercase and makes an assumption that it's an instance, while it's spy function that should create an instance. It can be fixed with:
PriceField.mockImplementation(...);
const priceField = new PriceField();
const sut = new PriceFieldRenderer(priceField);
priceField module mock and and associated PriceField spy serve no good purpose. They would be needed if it were imported and used in another module, this is the said example from Jest documentation shows.
That PriceFieldRenderer uses dependency injection makes testing simpler, this is one of benefits of the pattern:
const priceField = { determinePriceFieldNode: jest.fn(() => "<html/>") });
const sut = new PriceFieldRenderer(priceField);
Maybe someone has had a similar situation and can help with this:
Very simplistic view:
I have a Vuex module that I am using with two different stores
This module uses a "normalizer" function that maps data from an API into the format that's used by the components consuming the data
The components in the two stores need the data in slightly different formats
My current idea is to have two different "normalizer" functions that share the same "interface" but produce slightly different outcomes, and "inject" them as a dependency into the module before attaching it to the store:
Existing code - "fixed" normalizer function (plain old Vuex module):
// module.js
import { normalize } from './utils';
const actions = {
getData({ commit }) {
MyApi.getData().then((data) => {
commit('setData', data.map(normalize));
});
},
};
// ...
export default {
actions,
// ...
}
// my-store.js
import MyModule from './my-module';
export default new Vuex.Store({
modules: {
'my-module': MyModule,
},
// ...
});
New code that can accept different functions:
// module.js
export default (inject) => {
const { normalize } = inject;
const actions = {
getData({ commit }) {
MyApi.getData().then((data) => {
commit('setData', data.map(normalize));
});
},
};
//...
return {
actions,
// ...
};
}
// store-type-1.js
import getMyModule from './my-module';
import normalizerType1 from './normalizer-type-1';
export default new Vuex.Store({
modules: {
'my-module': getMyModule({ normalize: normalizerType1 }),
},
// ...
})
Thing is, something about this implementation feels like a sore thumb, and I'm pretty sure there are smarter ways of doing this, I'm just not smart enough to think of them.
I can provide more details about what this "normalizer" function is expected to do, maybe I'm barking up the wrong tree.
What's the industry standard for this sort of thing?
Thank you!
I see patterns which make use of a singleton pattern using ES6 classes and I am wondering why I would use them as opposed to just instantiating the class at the bottom of the file and exporting the instance. Is there some kind of negative drawback to doing this? For example:
ES6 Exporting Instance:
import Constants from '../constants';
class _API {
constructor() {
this.url = Constants.API_URL;
}
getCities() {
return fetch(this.url, { method: 'get' })
.then(response => response.json());
}
}
const API = new _API();
export default API;
Usage:
import API from './services/api-service'
What is the difference from using the following Singleton pattern? Are there any reasons for using one from the other? Im actually more curious to know if the first example I gave can have issues that I am not aware of.
Singleton Pattern:
import Constants from '../constants';
let instance = null;
class API {
constructor() {
if(!instance){
instance = this;
}
this.url = Constants.API_URL;
return instance;
}
getCities() {
return fetch(this.url, { method: 'get' })
.then(response => response.json());
}
}
export default API;
Usage:
import API from './services/api-service';
let api = new API()
I would recommend neither. This is totally overcomplicated. If you only need one object, do not use the class syntax! Just go for
import Constants from '../constants';
export default {
url: Constants.API_URL,
getCities() {
return fetch(this.url, { method: 'get' }).then(response => response.json());
}
};
import API from './services/api-service'
or even simpler
import Constants from '../constants';
export const url = Constants.API_URL;
export function getCities() {
return fetch(url, { method: 'get' }).then(response => response.json());
}
import * as API from './services/api-service'
The difference is if you want to test things.
Say you have api.spec.js test file. And that your API thingy has one dependency, like those Constants.
Specifically, constructor in both your versions takes one parameter, your Constants import.
So your constructor looks like this:
class API {
constructor(constants) {
this.API_URL = constants.API_URL;
}
...
}
// single-instance method first
import API from './api';
describe('Single Instance', () => {
it('should take Constants as parameter', () => {
const mockConstants = {
API_URL: "fake_url"
}
const api = new API(mockConstants); // all good, you provided mock here.
});
});
Now, with exporting instance, there's no mocking.
import API from './api';
describe('Singleton', () => {
it('should let us mock the constants somehow', () => {
const mockConstants = {
API_URL: "fake_url"
}
// erm... now what?
});
});
With instantiated object exported, you can't (easily and sanely) change its behavior.
Both are different ways.
Exporting a class like as below
const APIobj = new _API();
export default APIobj; //shortcut=> export new _API()
and then importing like as below in multiple files would point to same instance and a way of creating Singleton pattern.
import APIobj from './services/api-service'
Whereas the other way of exporting the class directly is not singleton as in the file where we are importing we need to new up the class and this will create a separate instance for each newing up
Exporting class only:
export default API;
Importing class and newing up
import API from './services/api-service';
let api = new API()
Another reason to use Singleton Pattern is in some frameworks (like Polymer 1.0) you can't use export syntax.
That's why second option (Singleton pattern) is more useful, for me.
Hope it helps.
Maybe I'm late, because this question is written in 2018, but it still appear in the top of result page when search for js singleton class and I think that it still not have the right answer even if the others ways works. but don't create a class instance.
And this is my way to create a JS singleton class:
class TestClass {
static getInstance(dev = true) {
if (!TestClass.instance) {
console.log('Creating new instance');
Object.defineProperty(TestClass, 'instance', {
value: new TestClass(dev),
writable : false,
enumerable : true,
configurable : false
});
} else {
console.log('Instance already exist');
}
return TestClass.instance;
}
random;
constructor() {
this.random = Math.floor(Math.random() * 99999);
}
}
const instance1 = TestClass.getInstance();
console.log(`The value of random var of instance1 is: ${instance1.random}`);
const instance2 = TestClass.getInstance();
console.log(`The value of random var of instance2 is: ${instance2.random}`);
And this is the result of execution of this code.
Creating new instance
The value of random var of instance1 is: 14929
Instance already exist
The value of random var of instance2 is: 14929
Hope this can help someone
I'm running into an issue with an Ember CLI project where I can't get an injected ember service from a controller action function.
The really strange thing is that this totally works on my models and custom adapters:
the controller:
export default Ember.Controller.extend({
node: Ember.inject.service(),
azureStorage: Ember.computed.alias('node.azureStorage'),
actions: {
myAction: function () {
// this returns null
var x = this.get('azureStorage');
}
}
});
// The service code (azureStorage and fs are NOT null)
if (window.requireNode) {
azureStorage = window.requireNode('azure-storage');
fs = window.requireNode('fs');
}
export default Ember.Service.extend({
azureStorage: azureStorage,
fs: fs,
getActiveAccount: function (store) {
return new Ember.RSVP.Promise(function (resolve, reject) {
var accounts = store.all('account'),
length = accounts.get('length'),
i = 0;
accounts.forEach(function (account) {
if (account.get('active') === true) {
return Ember.run(null, resolve, account);
}
i += 1;
if (i >= length) {
return Ember.run(null, reject, 'could not find any active accounts');
}
});
});
}
});
// the controller test code
var controller = this.subject();
controller.send('myAction');
I would have expected this to return the service and the azureStorage object. On my models & adapters the same pattern works just fine:
export default DS.Adapter.extend({
serializer: serializer.create(),
node: Ember.inject.service(),
azureStorage: Ember.computed.alias('node.azureStorage'),
findQuery: function () {
// this returns the value correctly
var x = this.get('azureStorage');
}
});
Any reason this would work on models & adapters but NOT on a controller?
I'm not familiar with the Ember.inject.service() pattern, but is there a reason you're not using the pattern outlined in http://guides.emberjs.com/v1.10.0/understanding-ember/dependency-injection-and-service-lookup/ ?
Also, why are you injecting node and azureStorage into the controller if you've already abstracted them into an adapter? You should just be using this.store.find('whatever', 123) from the controller to get your data. If your azureStore is different than your normal Ember Data store, then you should create a new store and register it with the application's container. If you inject it in your controller, you can access it with this.azureStore, or alternatively this.container.lookup('store:azure').
Also, not a good practice to start injecting stuff into your models. I would really take a look at the Ember-friendly ways of doing service/dependency injection, because this doesn't look very elegant and you're duplicating a lot of code to get access to something you already have.