Best practices for importing deeply nested Javascript components - javascript

I am working on a Ruby/React project. We are using React components and CoffeeScript and final JS is assembled by Sprockets:
#= require org/components/whatever
{ Whatever } = Org.Components
It is okay when there is not too much nesting and then you are wrtiting something like this:
#= require org/components/whatever
#= require org/components/something/else/whatever
{ Whatever1 } = Org.Components
{ Whatever2 } = Org.Components.Something.Else
Today I was trying to find where Org.Components.Image.Upload is used. But sometimes it is imported as { Upload } or used as Image.Upload and it doesn't make things easier.
Now I am thinking maybe don't go further than Org.Components for imports. So if you need Image.Upload — get { Image } = Org.Components, and use Image.Upload. If it gets too long - assign to a variable.
#= require org/components/image
{Image} = Org.Components
Upload = Image.Super.Complex.Upload
# Or use it like this for explicitness
render: ->
Image.Super.Complex.Upload
What is the best practice here? I want code to be searchable.

If you are in a CommonJS environment (Node), and probably using a module bundler like Webpack or Browserify you can take advantage of the direct module imports. For example:
Instead of doing this Org.Components.Image, you can do that:
import Upload from 'org/components/Image/Super/Complex/Upload'
// or var Image = require('org/components/Image/Super/Complex/Upload');
In your original strategy you load the entire library (org) to further filter it down to Upload.
In the proposed solution above, you only load the Image module and nothing else. This will probably save you a lot of bits in your final footprint, specially if org contains a big pile of components used inside your company.

To stop fighting against the sprockets I have defined a Root component that tells Sprockets where to look for the subcomponents:
# components/branding.coffee
#= require_tree ./branding
Org.Components.Branding = {}
So now if I need anything from the branding subtree I simply do the following:
#= require org/components/branding
{div} = React.DOM
{Branding} = Org.Components
Org.defineComponent "Settings.Branding",
render: ->
div {},
Branding.Edit.ContactUs {}
Branding.Config {},
Branding.Edit
This way I don't need to worry about dependencies and found it to be much more pleasant to work with.
I would suggest that this approach helps refactoring as you don't have to change multiple requires everywhere.
Branding.Config is a data-wrapped that loads and syncs settings. In the example above it is used to load settings for the Brading.Edit page. And here it is loading branding for in 'Layouts.Default'.
And again I only require branding
# apps/src/org/components/layouts/default.coffee
#= require org/components/branding
{Branding, TenantStateBar, UnsupportedBrowserBar} = Org.Components
Org.defineComponent 'Layouts.Default',
render: ->
div {},
Branding.Config {},
Branding.Style

Related

Rails 6 Webpacker calling javascript function from Rails view

I have following structure for Javascript in my Rails 6 app using Webpacker.
app/javascript
+ packs
- application.js
+ custom
- hello.js
Below shown is the content in the above mentioned JS files
app/javascript/custom/hello.js
export function greet(name) {
console.log("Hello, " + name);
}
app/javascript/packs/application.js
require("#rails/ujs").start()
require("jquery")
require("bootstrap")
import greet from '../custom/hello'
config/webpack/environment.js
const { environment } = require('#rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Popper: ['popper.js', 'default']
})
)
module.exports = environment
Now in my Rails view I am trying to use the imported function greet like shown below
app/views/welcome/index.html.haml
- name = 'Jignesh'
:javascript
var name = "#{name}"
greet(name)
When I load the view I am seeing ReferenceError: greet is not defined error in browser's console.
I tried to search for a solution to this problem and found many resources on web but none turned out to help me. At last when I was drafting this question in the suggestions I found How to execute custom javascript functions in Rails 6 which indeed is close to my need however the solution shows a workaround but I am looking for a proper solution for the need because I have many views which needs to pass data from Rails view to JS functions to be moved custom files under app/javascript/custom folder.
Also I would highly appreciate if anybody can help me understand the cause behind the ReferenceError I am encountering.
Note:
I am not well-versed in Javascript development in Node realm and also new to Webpacker, Webpack, Javascript's modules, import, export, require syntax etc so please bear with me if you find anything silly in what I am asking. I have landed up in above situation while trying to upgrade an existing Rails app to use version 6.
Webpack does not make modules available to the global scope by default. That said, there are a few ways for you to pass information from Ruby to JavaScript outside of an AJAX request:
window.greet = function() { ... } and calling the function from the view as you have suggested is an option. I don't like have to code side effects in a lot of places so it's my least favorite.
You could look at using expose-loader. This would mean customizing your webpack config to "expose" selected functions from selected modules to the global scope. It could work well for a handful of cases but would get tedious for many use cases.
Export selected functions from your entrypoint(s) and configure webpack to package your bundle as a library. This is my favorite approach if you prefer to call global functions from the view. I've written about this approach specifically for Webpacker on my blog.
// app/javascript/packs/application.js
export * from '../myGlobalFunctions'
// config/webpack/environment.js
environment.config.merge({
output: {
// Makes exports from entry packs available to global scope, e.g.
// Packs.application.myFunction
library: ['Packs', '[name]'],
libraryTarget: 'var'
},
})
// app/views/welcome/index.html.haml
:javascript
Packs.application.greet("#{name}")
Take a different approach altogether and attach Ruby variables to a global object in your controller, such as with the gon gem. Assuming you setup the gem per the instructions, the gon object would be available both as Ruby object which you can mutate server-side and in your JavaScript code as a global variable to read from. You might need to come up with some other way to selectively call the greet function, such as with a DOM query for a particular selector that's only rendered on the given page or for a given url.
# welcome_controller.rb
def index
gon.name = 'My name'
end
// app/javascript/someInitializer.js
window.addEventListener('DOMContentLoaded', function() {
if (window.location.match(/posts/)) {
greet(window.gon.name)
}
})
#rossta Thanks a lot for your elaborate answer. It definitely should be hihghly helpful to the viewers of this post.
Your 1st suggestion I found while searching for solution to my problem and I did referenced it in my question. Like you I also don't like it because it is sort of a workaround.
Your 2nd and 3rd suggestions, honestly speaking went top of my head perhaps because I am novice to the concepts of Webpack.
Your 4th approach sounds more practical to me and as a matter of fact, after posting my question yesterday, along similar lines I tried out something and which did worked. I am sharing the solution below for reference
app/javascript/custom/hello.js
function greet(name) {
console.log("Hello, " + name)
}
export { greet }
app/javascript/packs/application.js
require("#rails/ujs").start()
require("bootstrap")
Note that in above file I removed require("jquery"). That's because it has already been made globally available in /config/webpack/environment.js through ProvidePlugin (please refer the code in my question). Thus requiring them in this file is not needed. I found this out while going through
"Option 4: Adding Javascript to environment.js" in http://blog.blackninjadojo.com/ruby/rails/2019/03/01/webpack-webpacker-and-modules-oh-my-how-to-add-javascript-to-ruby-on-rails.html
app/views/welcome/index.html.haml
- first_name = 'Jignesh'
- last_name = 'Gohel'
= hidden_field_tag('name', nil, "data": { firstName: first_name, lastName: last_name }.to_json)
Note: The idea for "data" attribute got from https://github.com/rails/webpacker/blob/master/docs/props.md
app/javascript/custom/welcome_page.js
import { greet } from './hello'
function nameField() {
return $('#name')
}
function greetUser() {
var nameData = nameField().attr('data')
//console.log(nameData)
//console.log(typeof(nameData))
var nameJson = $.parseJSON(nameData)
var name = nameJson.firstName + nameJson.lastName
greet(name)
}
export { greetUser }
app/javascript/packs/welcome.js
import { greetUser } from '../custom/welcome_page'
greetUser()
Note: The idea for a separate pack I found while going through https://blog.capsens.eu/how-to-write-javascript-in-rails-6-webpacker-yarn-and-sprockets-cdf990387463
under section "Do not try to use Webpack as you would use Sprockets!" (quoting the paragraph for quick view)
So how would you make a button trigger a JS action? From a pack, you add a behavior to an HTML element. You can do that using vanilla JS, JQuery, StimulusJS, you name it.
Also the information in https://prathamesh.tech/2019/09/24/mastering-packs-in-webpacker/ helped in guiding me to solve my problem.
Then updated app/views/welcome/index.html.haml by adding following at the bottom
= javascript_pack_tag("welcome")
Finally reloaded the page and the webpacker compiled all the packs and I could see the greeting in console with the name in the view.
I hope this helps someone having a similar need like mine.

include javascript files in marionette project

I have a marionette project, and I have my main js file which looks like:
define(["marionette", "handlebars"], function(Marionette, Handlebars){
...
});
Now I want to make a structure of my big app. So I would like to separate the content of this function to different files. So how can I do something like:
define(["marionette", "handlebars"], function(Marionette, Handlebars){
include routes.js
include menu/model.js
include menu/views.js
...
});
I think require.js can help me, but I don't know how. Any ideas?
To require a file in the AMD structure (like the one you describe), you can give the path to the file in the array and get the exported value as argument of the callback like this.
define(
[
"marionette",
"handlebars",
"routes",
"menu/model",
"menu/views"
],
function(Marionette, Handlebars, Routes, Model, MenuView){
// ...
});
Since this is a very basic thing in AMD, you should really read the documentation of Require.JS (here) and explanation about AMD modules (like this one). You will find lot of information about the AMD structure, etc
If you prefer, browserify lets you to load front-end javascript modules using the require syntax. It looks like this.
// library includes
var Marionette = require('backbone.marionette');
var handlebars = require('handlebars');
// custom includes
var routes = require('./routes.js');
var models = require('./menu/models.js');
var views = require('./menu/views.js');
var App = new Marionette.Application();
It includes modules recursively and there is less code. It implements the Common.js format so it looks and feels the same as node.js.

How to share objects/methods between controllers without circular references?

Pretty straightforward question. Currently, what I do when I need to access objects' methods throughout most of the application, I do this in app.js
Ext.define('Utils', {
statics: {
myMethod: function() {
return 'myResult';
}
}
});
This works, but when I build the application, I get a warning about a circular reference (app.js of course needs all the other controller classes, but then said classes refer back to app.js).
I thought of using a package including a .js file that has all the objects/methods, but sometimes, within these methods I'll need access to the Ext namespace so that won't work.
Is there any better way to do this ?
Thanks!
You should not define the class Utils inside app.js. Each Ext.define statement should be in it's own file. Also the classname should be prefixed with your app name.
Ext.define('MyApp.Utils', {
statics: {
myMethod: function() {
return 'myResult';
}
}
});
should therefore be found in the file app/Utils.js. When compiling your sources into a compiled app.js, Sencha Cmd will take care of the proper ordering in the final file. The requires and uses directives give you enough flexibility to define any dependences between your classes.
Read all the docs about MVC to get a clear understanding.

Ordering files with grunt-neuter

I've created an ember app with yeoman and am trying to get the build to order the scripts. I have an app.coffee which contains require 'scripts/controllers/*' but I also need to order the scripts within controllers. I have something like this:
controllers/foo_controller.js.coffee
require "./bar_controller"
App.FooController = App.BarController.extend()
controllers/bar_controller.js.coffee
App.BarController = Ember.Controller.extend()
But it isn't ordering these files. Am I doing it correctly?
I fixed this, I had the relative path wrong. It should have looked like this:
require "scripts/controllers/bar_controller"
App.FooController = App.BarController.extend()

require.js: how can I load a module that defines a name under a different name?

I'm trying to load underscore.js with require.js like this:
require(["libs/underscore-1.2.3.js"], function(_) {
...
});
But this doesn't work because underscore.js exports a module name: define('underscore', function() { ... }).
Without renaming lib/underscore-1.2.3.js, how can I load it with require.js?
Alright, after some more googling, I've found: https://github.com/documentcloud/underscore/pull/338#issuecomment-3245213
Where
#dvdotsenko all AMD loaders allow mapping a module ID to a partial path, usually the configuration is called 'paths', so to do what you want:
requirejs.config({
paths:
underscore: 'js/libs/underscore-1.2.3.min'
}
});
require(['underscore'], function () {});
Since underscore is used by other higher-level modules, like backbone, a common dependency name needs to be used to communicate a common dependency on underscore, and it makes sense to call that dependency 'underscore'. The paths config gives a way to do the mapping to a specific URL you want to use for that dependency.
This doesn't answer my question (ie, I still don't know how I'd go about loading underscore if all I had was a URL), but at least it's a functional workaround.
While this doesn't strike me as the most ideal solution, you can require your external files, and then require their registered module names in the inner block.
JSFiddle Example
require(
['require','http://documentcloud.github.com/underscore/underscore-min.js'],
function(require){
require(['underscore'],function(_){
var a = _.intersection([1,2,3],[2,3,4]);
document.write("Underscore is available in the closure : " + a);
})
}
)
It might not look pretty, but that might be a recommended pattern for loading up initial assets so that they can be required intuitively in dependent modules.

Categories