Sprockets > How to specify a binding for Erb evaluation / rendering? - ruby-on-rails-3

I've spent a lot of time digging into sprockets' and tit's source code, trying to figure out how to pass variables / bindings to the Erb evaluation context. Here's what I'm trying to do: I need to serve a JS file whose contents change on a per-request basis. The portions that change depend on data stored in the DB, hence the need to route requests through the Rails app and the need to pass variables / bindings. On top of that the JS file uses the require directives to insert other JS files, hence the need to use sprockets.
Here's the code snippet that isn't working:
Controller file:
def ever_changing_js
#foobars = Foobar.all
MyApp::Application.assets.instance_eval do
def foobars
#foobars
end
end
render :text => MyApp::Application.assets.find_asset('ever_changing.js').to_s, :content_type => "application/javascript"
end
ever_changing.js:
//= require file1.js
//= require file2.js
// Some code that uses #foobars
How can I get this done? Any help would be appreciated.

JavaScript files should be completely static; Sprockets is not meant to do what you are trying to do.
Any data that changes on a per-request basis should be written to a <script> tag at the bottom of the template you are rendering.
app/assets/javascripts/user.js
(function(exports) {
function User(name) {
this.name = name;
}
User.prototype.speak() {
console.log(this.name + ' says, "Hello!"');
};
exports.User = User;
})(this);
app/views/users/show.html.erb
...
<%= javascript_include_tag('user') %>
<script>
(function() {
var user = new User(<%= #user.name %>);
$('#speak-button').click(function() {
user.speak();
});
})();
</script>
</html>
If you can give more context around your specific use case, I can give a more specific example.

I am trying to accomplish the same thing you are. I see a couple problems with your controller code snippet. Rather than doing an instance_eval on the Sprockets::Environment, you should class_eval the context_class, as shown in the Sprockets::Context documentation.
MyApp::Application.assets.context_class.class_eval do
def foobars
#foobars
end
end
Then foobars will be available to your ERb template.
As a sidenote, you can do
render js: MyApp::Application.assets.find_asset('ever_changing.js').to_s
instead of setting the content type yourself.

Related

How can I pass content to a parent template from a child template in Silverstripe

I want to pass some content from a elemental block up to a parent template (to change the page header) - Is this possible? I don't have any example code for what I'm trying because I don't have any idea how to implement it.
This would be similar to the content_for facility in Rails / ERB templates.
Parent:
Page Title <%= yield :title %>
Child Template:
<% content_for :title do %>
<b>, A simple page</b>
<% end %>
Is there anything like this or some other way to do this in SS templates?
Silverstripe Templates are a one-way street - you can't pass values upstream.
What you can do in this case is add a method to the page object to fetch the correct title from the block:
public function getTitleForTemplate()
{
return $this->MyBlock->PageTitle;
}
And then in your template you can simply call that method or as in this example treat it like a property:
Page Title $TitleForTemplate
If you want to use a template to render out this property (e.g. if you have different styling or markup for different blocks which belong to different pages) you can render the template in that method. There are many ways to do this but perhaps the most sensible would be to call renderWith() on the block object:
public function getTitleForTemplate()
{
return $this->MyBlock->renderWith(['type' => 'Includes', 'templates' => ['MyBlockTitle']);
}
and then have a template in your theme's templates/Includes directory called MyBlockTitle.ss which renders out the title with any styling and markup you need for it.
In other words: data belongs in models, the template is just there to display that data. So if you need some specific data to be displayed in the page template, the page object should have that data available.
Disclaimer: I haven't tested this code, it was done from memory. Some of the syntax especially for the second example may be slightly different in reality.
To expand on Guy's answer, to find the block i wanted to use to add to the page title and breadcrumbs, I used the ElementalArea and it's Elements.
public function getTitleForTemplate(){
$output = "" ;
foreach( $this->ElementalArea->Elements() as $element ){
if($element->ClassName == 'MySite\AwardsElement'){
if ( $element->Award() ){
$output .= "{$element->Award()->title } ";
}
}
}
return $output;
}

Global variable is undefined if assign variable in the TargetConnected method in stimulus.js

This is the location controller file that is going to access by the html code.
export default class extends Controller {
static targets = [ "visible", "map" ]
mapTargetConnected(element) {
this.name = "aaa"
}
add(event) {
console.log(this.name) // this line is logged that variable is undefined.
}
}
here is the HTML code
<%= form_with(model: #location, local: false, url: location_path(), data: {controller: 'location', action: 'ajax:beforeSend->location#add'}) do |form| %>
....
<% end %>
This is the code regarding form submit via ajax request. if i access the this.name variable inside the add method or click event its says the variable is undefined… but if i same name variable assign it in connect() method than it’s working…
but i want to assign variable at targetConnected method and use it in the add action method.Please suggest any solution or let me know if i'm doing wrong.
Most likely the add event is being triggered before the mapTargetConnected has run.
Stimulus will go through the DOM and match elements and their targets and then trigger the relevant someTargetConnected and connect lifecycle methods once the controller is set up.
However, this is not instant and there may be some nuance to how the timing works when you are working with other events.
You will need to work out when the actual map target is being added to the DOM and possibly do some logging to check that timing compared to when the ajax:beforeSend event triggers.
Sometimes, adding a setTimeout can assist as it will ensure that the code provided to it runs 'last' (there is some nuance to this, technically it is the next event cycle).
For example
add(event) {
// here the mapTargetConnected may not have run
setTimeout(() => {
// by this point, mapTargetConnected has hopefully now run
console.log(this.name);
});
}
It is hard to offer more help without a bit more specifics on what ajax:beforeSend is and when it triggers, along with what actually adds the map target to the DOM. It may be more helpful to write this question with the initially rendered HTML output (with the minimum parts to help guide the question).
In general, it is good to remember that in the browser, things do not happen instantly, while they may be fast there can be timing issues to be aware of.

Riotjs - Front-end page Structure

I'm using the riot for the system. but I have a problem using the common tag in every place. because I have to copy the all common tag each page.
I added all tags like this. Does anyone have the solution for this ?
<st-service>
<st-alert></st-alert>
<st-header></st-header>
<st-body></st-body>
<st-footer></st-footer>
</st-service>
<st-profile>
<st-alert></st-alert>
<st-header></st-header>
<st-body></st-body>
<st-footer></st-footer>
</st-profile>
I found a solution, I'm using this method to handle these common tags. like this
<st-common>
<st-alert></st-alert>
<st-header></st-header>
<yeild></yeild>
<st-footer></st-footer>
</st-common>
service-page.tag // page
<st-service-page>
<st-common>
<st-service></st-service>
</st-common>
<st-service-page>
profile-page.tag // page
<st-profile-page>
<st-common>
<st-profile></st-profile>
</st-common>
<st-profile-page>
service-view.tag
<st-service>
// html / code body related to module
</st-service>
profile-view.tag
<st-profile>
// html / code body related to module
</st-profile>
If needed in details about this I can explain.
I'd have to know more about how you're routing to say for sure, but I think you should avoid using a different outer tag for each page. If your HTML looks something like this:
<body>
<st-app />
<script>
const pages = {
"/": "st-home",
"/about/": "st-about",
}
const content_tag = pages[window.location.pathname] || "st-notfound"
riot.mount("st-app", {
content_tag: content_tag
})
</script>
</body>
Then <st-app> would be defined something like:
<st-app>
<st-alert></st-alert>
<st-header></st-header>
<div data-is={this.opts.content_page}></div>
<st-footer></st-footer>
</st-app>
The important thing here being that you're controlling which tag should be used via the data-is attribute and the mounting options for <st-app>. In this example <st-home>, <st-about>, and <st-notfound> are riot components defined elsewhere.

using dijit.byId w dojox.mobile widgets

I'm dynamically building a series of dojox.mobile.ListItem widgets under a statically defined dojox.mobile.RoundRectList widget via this function...
function displayOpps(items) {
// Create the list container that will hold application names
var rrlOppsContainer = dijit.byId("rrlOpps");
// Add a new item to the list container for each element in the server respond
for (var i in items){
// Create and populate the list container with applications' names
var name = items[i].CustName + " - " + items[i].OppNbr;
var liOpps = new dojox.mobile.ListItem({
label: name,
moveTo: "sv3OppDetail"
});
// Add the newly created item to the list container
rrlOppsContainer.addChild(liOpps);
}
}
When I run this code during onLoad() in my html file, I get the following error using Chrome's dev tools...
Uncaught TypeError: Object # has no method 'byId'
I've read numerous articles around this topic and it appears that lots of folks have this problem, but each that I have found are in relation to some other technology (e.g., Spring MVC, etc) and I'm attempting to use it w a dojox.mobile based app. That said, I attempted to mimic some of the solutions brought up by others by including this in my html file, and it still doesn't work...
<script type="text/javascript"
data-dojo-config="isDebug: true, async: true, parseOnLoad: true"
src="dojo/dojo.js">
dojo.require("dojox.mobile.RoundRectList")
</script>
What am I doing wrong?
Thank you in advance for your time and expertise.
If you are using Dojo 1.7+, you probably just forgot to require the "dijit/registry" module. This where the byId function is defined. When you use desktop widgets, this is loaded indirectly by other base modules, but with dojox/mobile you must load it explicitly (because dojox/mobile loads only a very minimal set of modules by default, to minimize code footprint).
Depending on how you wrote your application, do this:
dojo.require("dijit.registry"); // legacy (pre-1.7) loader syntax
...
var rrlOppsContainer = dijit.byId("rrlOpps");
...
or this:
require(["dijit/registry", ...], function(registry, ...){ // 1.7+ AMD loader syntax
...
var rrlOppsContainer = registry.byId("rrlOpps");
...
});
Note also that your second code sample tries to use asynchronous loading (async: true) while it uses the legacy loader syntax. This won't work, to get async loading you must use the AMD syntax.

How to use ruby on rails to load slickgrid

I'd like to have a grid on a rails application using slickgrid.
My initial problem I think is not having a best practice on where or how to load the grid with data from the sql database.
In the *.html.erb file, do I use javascript and embed ruby code (is this even possible)?
Is anyone out there using slickgrid, or anything comparable, with a ruby on rails application?
Any simple coding examples are highly appreciated!
I used this post to generate a JavaScript file dynamically. It looks like:
var columns = ...
var options = ...
$(function() {
var data = [];
data[0] = {...};
data[1] = {...};
data[2] = {...};
...
var grid = new Slick.Grid($("#my_grid"), data, columns, options);
});
I was be able to load data from database into the SlickGrid. However, currently I do experience a hang problem with the columnpicker (I hope you don't need it).
There are a couple of ways to get the data into SlickGrid. The easiest is probably an AJAX call. If the data is static you could embed it in the page, but that's probably less useful and more difficult than just doing it with AJAX.
The first thing you'll need is a route you can access that will return your data to the browser, preferably in JSON format so it's easy to handle on the client side.
Assuming the data you want to return is just a simple collection we'll use the example of a Users list.
class UsersController < ApplicationController
def index
#users = User.all
respond_to { |format|
format.json { render :json => #users }
}
end
end
We'll assume you can get to this at the URL http://example.com/users/
Now that we have a way to get the data we can pull the data on the browser side using JavaScript. My example is going to use jQuery, but any JavaScript framework is going to have an easy way to make an AJAX call.
Note that SlickGrid needs a place to put your data. So I'll assume you have a line like this in your HTML: <div id="slickGrid"></div>
# We need to wait for the DOM to be loaded so we wrap our AJAX call in a
# jQuery call that's the equivalent of document.ready()
jQuery(function() {
# getJSON is a jQuery convenience function for doing an AJAX call
# that fetches some JSON data.
jQuery.getJSON('http://example.com/users', function(data) {
grid = new Slick.Grid("#slickGrid", data, columns, options);
$("#slickGrid").show();
});
});
You can get more examples of how to use SlickGrid from the Github repository:
https://github.com/mleibman/SlickGrid/wiki/_pages
For more on how to do AJAX calls with jQuery here's the docs for the more general jQuery.ajax() function:
http://api.jquery.com/jQuery.ajax/
And the getJSON function that I used specifically:
http://api.jquery.com/jQuery.getJSON/
We can load data in browser in Json format. So, I used it to parse into "user_data" array.
eg. localhost:3000/users.json
var user_data = <%= #users.to_json.html_safe %>;