Example:
mysite.com/page1 depends on scripts in module1.js
mysite.com/page2 depends on scripts in module2.js
mysite.com/page3 depends on scripts in module3.js
Does anyone have any best practices for only running the Javascript required for that specific page. Before I started using RequireJS I would use only one Javascript file and init only the modules I needed for that page. like this
In page <head>:
var page = "pageTitle";
In Main JS File:
var myModules = {};
myModules.pageTitle = (function(){
return {
init: function(){
alert('this module has been initiated');
}
}
})();
myModules[page].init();
Not really sure sure how a technique like this would work with RequireJS. Would love some feedback and advice on how others are doing this.
I assume you have one main.js file for all your pages?
Anyway, typically you would use the data-main attribute of the script tag as explained in the API documentation, which would mean you have one main js file per page. This way, you can get rid of the literal javascript code in you page, and take full advantage of the RequireJS optimization step.
Example:
Develop you main.js file as a RequireJS module:
define([<dependencies go here>], function(){
return function(pageTitle){
//do you page dependent logic here
}
}
In your html, you'll have something like:
<html>
<head>
<script src="require.js"></script>
<script>
require(["main.js"], function(init){
init("pageTitle");
});
</script>
1) What language do you use at back-end?
You can keep your script-configuration in database or in configuration files. (For example: page page1 has modules: module1, module2, and module4, etc).
I have such a php template file for generating <script> tags on my page:
<script src="http://requirejs.org/docs/release/1.0.1/minified/require.js"></script>
<script>
require([
<?php echo "'". implode("',\n\t'", $this->scripts) . "'\n"; ?>
], function(a){
function run(page) {
if ( window.hasOwnProperty(page) ) {
window[page].start();
}
}
var page = '<?php echo $this->page; ?>';
run('all'); // activating scripts needed for every page
run(page); // and for current page
});
</script>
P.S. the script is asking for window[page] variable. I meant, that every .js script for a page -- for example index.js for index page is making window.index variable. ( I know, it's not so good - read P.P.S ;) )
P.P.S. I'm novice to requireJS (I've knew about it only today), and it my first draft, and I think, I'll make it in another way:
2) As a concept for now :)
You keep your scripts as AMD modules (not as usual scripts, but as modules for requireJS). Modules map you can keep in a .json file:
{
'index' : [ 'news', 'banners' ],
'contacts' : [ 'maps', 'banners', 'donate' ],
'otherpage' : [ 'module1', 'module2' ]
}
You should pass the page name or page id to the main.js (you can pass this value in DOM element - in templates of site, or in template variables ).
So main.js knows the page name, and load your modules.json file. It gets specific modules and requires them.
main.js also can keep dependencies that are need on every page ( for example jquery, some jquery plugins, etc) ( jquery plugins better to wrap as modules )
P.S. sorry for my English
The creator of RequireJS actually made an example project doing excactly this: https://github.com/requirejs/example-multipage
Related
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.
I have downloaded tracking.js and added it to my /src/assets folder
In my angular-cli.json file I have added to my scripts:
"scripts": [
"../src/assets/tracking/build/tracking-min.js"
],
issue here - In my angular component, I import tracking as follows:
import tracking from 'tracking';
and in the chrome inspection window I can hover over 'tracking' and see all of the properties as shown:
I can even call the ColorImage constructor in the console window! :
However when it tries to execute the constructor in my code I get the error about tracking being undefined:
I had assumed it was because I wasn't passing in the tracking object through the constructor in the traditional DI fashion, but when doing so I got the error that the namespace couldn't be used as a type:
The only other thing I could think of was to try and add the external reference in the main index.html file, but I got an error about strict MIME checking.
To clarify: this is all happening in my angular component constructor (when the tracking methods get exercised)
Any ideas?
go to your node_modules folder and find this file : "node_modules/tracking/build/tracking.js" . open the file and add this line of code to end of the file :
module.exports = window.tracking
save file and in use this code to import it :
import * as tracking from 'tracking';
I don't think you can use DI with that external library. However, you should be able to create a new instance in the constructor:
import tracking from 'tracking';
constructor(...) {
this.colors = new tracking.ColorTracker(...);
}
myFunction() {
this.colors.doWhateverIWant();
}
If you only want a single tracking instance throughout your app, then you'll have to create your own trackingService and inject that.
another solution is to reference the tracking.js via script tag :
<html>
<head></head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tracking.js/1.1.3/tracking-
min.js"></script>
</body>
</html>
and in your component.ts write :
(window as any).tracking.ColorTracker(["magenta"]);
Please see attachment.
That's the template structure. login.jade extends layout.jade.
My problem is that I want:
a global.css file loaded in all pages (done)
login.css file loaded only when viewing login.jade (problem)
I already created a working pipeline configuration. The task looks like this:
devStyles: {
options: {
startTag: '<!--STYLES-->',
endTag: '<!--STYLES END-->',
fileTmpl: '<link rel="stylesheet" href="%s">',
appRoot: '.tmp/public'
},
files: {
'.tmp/public/**/*.html': require('../pipeline').frontendGlobalCssFiles,
'views/**/*.html': require('../pipeline').frontendGlobalCssFiles,
'views/**/*.jade': require('../pipeline').frontendGlobalCssFiles,
'views/auth/login.jade': require('../pipeline').frontendLoginCssFiles
}
}
The problem is that the last files rule doesn't work. I'm sure that frontendLoginCssFiles is ok, since if I load it with the 'views/**/*.jade' path, it works. So what's the problem here?
Ok, solved. Basically, the problem is not strictly related to jade inheritance. What sails-linker really does is add the assets to the physical files, no matter if they're parents, children or partials. All it does is take the passed files, search for the proper start/end Tags and add the related html BEFORE compiling the real served pages.
My problem was that I was not adding the start/end tags to the login (child) page.
Anyway, even doing so, we've got then the problem that ALL the compiled pages will load ALL the assets. So for example, the login assets will be also loaded on the home page. Not so good.
To solve, briefly, I used specific start/end tags for page-specific assets.
Following, the 'long' story:
Note: since we're speaking about jade templates, in sails-linker we are looking into the *Jade tasks, and therefore the comments are written with jade syntax (ex. // STYLES and not <!--STYLES-->)
First, we'll isolate 'global assets' > the ones we want to load on every page:
Change all the // STYLES instances to something like // GLOBAL STYLES. Both in sails-linker and eventually on .jade views.
/tasks/pipeline.js: rename default filelists to something like globalCssFiles, jsfiles etc, and change all the instances of that name.
/tasks/config/sails-linker.js: rename all the instances of point 2 modifications.
Second, we'll add 'page assets':
On child jade pages, use something like // PAGE STYLES comments.
/tasks/pipeline.js: create page-specific filelists, so ex:
var frontendLoginCss = ['styles/login.css'];
and below:
module.exports.frontendLoginCss = frontendLoginCss.map(function(path) {
return '.tmp/public/' + path;
});
/tasks/config/sails-linker.js: create page-specific tasks, like:
devPageStylesJade: {
options: {
startTag: '// PAGE STYLES',
endTag: '// PAGE STYLES END',
fileTmpl: 'link(rel="stylesheet", href="%s")',
appRoot: '.tmp/public'
},
files: {
'views/auth/login.jade': require('../pipeline').frontendLoginCss
}
}
/tasks/register/*: add the relevant tasks in all the files where you should to. For example, my linkAssets.js could be like this:
module.exports = function (grunt) {
grunt.registerTask('linkAssets', [
'sails-linker:devJs',
'sails-linker:devStyles',
'sails-linker:devTpl',
'sails-linker:devJsJade',
'sails-linker:devPageJsJade', //added
'sails-linker:devStylesJade',
'sails-linker:devPageStylesJade', //added
'sails-linker:devTplJade'
]);
};
In jade templates, use block syntax for importing the styles/js, so you can use append to append the assets to the block. Ex, the general 'parent' layout.jade will have:
block styles
// GLOBAL STYLES
// GLOBAL STYLES END
while the child template login.jade will have:
append styles
// PAGE STYLES
// PAGE STYLES END
Final tip: it really doesn't matter where you write the append directives in child templates, they'll always appended where the parent template did defined them. So I'll write all appends on the bottom of my child templates (more clean).
Hope will be usefull for people with same issues!
I am starting to use RequireJS now and I was already able to add my project dependencies but I still cannot add a jQuery anonymous function yet.
For example, with my normal_file.js I do something like:
normal_file.js:
define(['dependency1'], function(Dependency) {
var Test1 = ...;
return Test1;
});
Bu from a file that has no module, like the example below, I don't know how to encapsulate it:
lib_file.js:
(function ($) {
// Do stuff...
})(window.jQuery);
the lib_file was not made by me and I'm not sure on how it really works, but I would gess it is an anonymous auto-executed function, is that so?.
Anyway, my goal is to use both files in my main code, like below:
main.js:
requirejs.config({
baseUrl:'/static/editorial/js/',
paths: {
jquery: 'third_party/jquery-1.10.2',
react: 'third_party/react-with-addons'
}
});
var dependencies = [
'third_party/react-with-addons',
'third_party/jquery-1.10.2',
'build/utils/normal_file,
'third_party/lib_file
];
require(dependencies, function(React, $, Test1, ??) {
// do my stuff
});
How should I encapsulate that anonymous function in order to add it as a dependency to my main file?
From the RequireJS docs:
Ideally the scripts you load will be modules that are defined by
calling define(). However, you may need to use some traditional/legacy
"browser globals" scripts that do not express their dependencies via
define(). For those, you can use the shim config. To properly express
their dependencies.
Read this: http://requirejs.org/docs/api.html#config-shim
It has a really good explanation of what you have to do, and gives a nice example.
Basically, you just need to set up a shim config for lib_file.js so Require knows to load the right dependencies before giving you access to that script.
I am using require.js to load my modules which generally works fine. Nevertheless, I do have two additonal questions:
1) If you have a module that is like a helper class and defines additional methods for existing prototypes (such as String.isNullOrEmpty), how would you include them? You want to avoid using the reference to the module.
2) What needs to be changed to use jQuery, too. I understand that jQuery needs to be required but do I also need to pass on $?
Thanks!
1) If you have a module that is like a helper class and defines
additional methods for existing prototypes (such as
String.isNullOrEmpty), how would you include them? You want to avoid
using the reference to the module.
If you need to extend prototypes then just don't return a value and use it as your last argument to require:
// helpers/string.js
define(function() {
String.prototype.isNullOrEmpty = function() {
//
}
});
// main.js
require(['moduleA', 'helpers/string'], function(moduleA) {
});
2) What needs to be changed to use jQuery, too. I understand that
jQuery needs to be required but do I also need to pass on $?
The only requirement for jQuery is that you configure the path correct
require.config({
paths: {
jquery: 'path/to/jquery'
}
});
require(['jquery', 'moduleB'], function($, moduleB) {
// Use $.whatever
});
In my opinion it's unnecessary to use the version of RequireJS that has jQuery built into it as this was primarily used when jQuery didn't support AMD.
Nowadays it does and keeping it separate allows you to swap another library out easily (think Zepto).
2/ For jquery it's really simple :
require(["jquery", "jquery.alpha", "jquery.beta"], function($) {
//the jquery.alpha.js and jquery.beta.js plugins have been loaded.
$(function() {
$('body').alpha().beta();
});
});
More information on require site : http://requirejs.org/docs/jquery.html#get
1/ in my devs for such extension I did it in a global file without require module code.... and I include it in my app with require... not perfect, but it's work fine
global.js
myglobalvar ="";
(...other global stuff...)
myapp.js
// Filename: app.js
define([
(...)
'metrix.globals'
], function(.....){
myApp = {
(...)