I'm working on an application that renders many, separate "sites" as subdirectories- e.g. /client/1, /client/2, etc. For each of these, two color values can be specified in the admin portion of the application.
I'd like to know if there's a method to inject the values, which were initially posted to and then retrieved from the back-end API by Ember, into a SCSS file for preprocessing?
I've found no solution thus far.
In our Ember/Rails application, we are generating CSS files for each client based on some settings in the database. For example, our Tenant model has two fields:
{
primary_color: 'ff3300',
secondary_color: '00ff00'
}
We expose routes
scope '/stylesheets', format: 'css' do
get 'tenant/:tenant_id', to: 'stylesheets#tenant_css'
end
And our controller looks something like this:
class StylesheetsController < ApplicationController
layout nil
rescue_from 'ActiveRecord::RecordNotFound' do
render nothing: true, status: 404
end
def tenant_css
# fetch model
tenant = Tenant.find(params[:tenant_id])
# cache the css under a unique key for tenant
cache_key = "tenant_css_#{tenant.id}"
# fetch the cache
css = Rails.cache.fetch(cache_key) do
# pass sass "params"
render_css_for 'tenant', {
primary_color: tenant.primary_color,
secondary_color: tenant.secondary_color
}
end
render_as_css css
end
protected
# our renderer, could also add a custom one, but simple enough here
def render_as_css(css)
render text: css, content_type: 'text/css'
end
# looks for a template in views/stylesheets/_#{template}.css.erb
def render_css_for(template, params = {})
# load the template, parse ERB w params
scss = render_to_string partial: template, locals: { params: params }
load_paths = [Rails.root.join('app/assets/stylesheets')]
# parse the rendered template via Saas
Sass::Engine.new(scss, syntax: :scss, load_paths: load_paths).render
end
end
This way, you can link to /stylesheets/tenant/1.css which will render the CSS for the tenant using the Sass engine.
In this case, in views/stylesheets/_tenant.css.erb, you'd have something like this (it's an ERB file but you can use Sass in there now):
#import "bootstrap-buttons";
<% if params[:primary_color].present? %>
$primary-color: <%= params[:primary_color] %>;
h1, h2, h3, h4, h5, h6 {
color: $primary-color;
}
<% end %>
<% if params[:secondary_color].present? %>
$secondary-color: <%= params[:secondary_color] %>;
a {
color: $secondary-color;
&:hover {
color: darken($secondary-color, 10%);
}
}
<% end %>
You'll note that I can now use #import to import anything in your stylesheet path for the Sass engine (in this case, I can utilize some helpers from Bootstrap Sass lib).
You'll want to have some sort of cache cleaner to wipe the cache when your model backing the CSS is updated:
class Tenant < ActiveRecord::Base
after_update do
Rails.cache.delete("tenant_css_#{id}")
end
end
So that's the Rails side in a nutshell.
In Ember, my guess is you'll want to load the stylesheet based on an ID, so that stylesheet cannot be hard-coded into "index.html". Ember CSS Routes addon might serve you well, but I found that it just appends <link> to the header, so if you need to swap CSS stylesheets at any time, this won't work. I got around this in a route like so:
afterModel(model, transition) {
// dynamically form the URL here
const url = "/stylesheets/tenant/1";
// build link object
const $link = $('<link>', { rel: 'stylesheet', href: url, id: 'tenant-styles' });
// resolve the promise once the stylesheet loads
const promise = new RSVP.Promise((resolve, reject) => {
$link.on('load', () => {
$link.appendTo('head');
resolve();
}).on('error', () => {
// resolve anyway, no stylesheet in this case
resolve();
});
});
return promise;
},
// remove the link when exiting
resetController(controller, isExiting, transition) {
this._super(...arguments);
if (isExiting) {
$('#tenant-styles').remove();
}
}
You could also add a blank element in the <head> and then use Ember Wormhole to format a <link> tag and render into the "wormhole".
Edit
You could also look into rendering Sass directly in the client application. For something as simple as two colors, this wouldn't have much of performance impact, especially if you used a service worker or similar to cache the results.
Related
Is there a way to load controller (product/category) within some contained space, so that the ajax to the custom function within that controller doesn't break?
I'm basically loading (from ajax) a custom function which is inside a core contoller product/category. In this function I need to reload the the product/category controller to get new product list based on ajax data I sent to the function, to then return it as a response to the original ajax.
When I try to do
$this->load->controller('product/category')
it beaks the ajax I set up with the function and in the console I see 404.
I tried using
$foo = $this->load->controller('product/category')
and it works, but I need to also execute
$this->load->view('product/category')
and I don't know how to do it without breaking ajax.
Basically I did what I essentially wanted (see the middle of my question, namely the part about the ultimate need to refresh my product list using ajax) the other way (after reading up How to get products in JSON format from OpenCart using phonegap/jQueryMobile): from the ajax I called the products/category directly and not my custom function inside of the product/category controller as in the question, and when I got reponse back containing the html output of the view, I reloaded a div I made in the product/category.twig with that html using jQuery. The ajax was
$(document).ready(function(){
$.ajax({
url: 'index.php?route=product/category&path=18&json',
type: 'get',
beforeSend: function() {
},
complete: function() {
},
success: function(data) {
console.log('success');
if (data.length > 0) {
console.log(data);
$('#mydiv').html(data);
}
}
});
});
and the code I added to product/category.php was
if(isset($this->request->get['json'])) {
$this->response->setOutput($this->load->view('product/view_for_mydiv', $data));
} else {
$this->response->setOutput($this->load->view('product/category', $data));
}
As you may notice I added a div inside product/category.twig called mydiv, which I placed exactly where I wanted the html to go and then I created a twig called view_for_mydiv.twig inside default/product/category/ folder, the html of which the product/category controller would send back instead of its general twig when it saw that an ajax call had been made to it. The #mydiv div located inside category.twig wraps the html that is the same html that gets produced when view_for_mydiv.twig is used to render product/category.
I have 2 models Country and Language linked with HABTM relation.
I'm using Rails API with ActiveModelSerializer and Ember JS as frontend.
So how is it possible to add a new language to country.languages collection ?
On the Ember side I'm trying to add a new language as follows:
#router
actions: {
saveLanguage(language) {
let controller = this.get('controller');
let country = controller.get('aCountry');
country.get('languages').pushObject(language);
country.save();
}
}
This calls CountriesController#update action in Rails.
Here is how I deserialize params hash in Rails controller:
#countries_controller.rb
def country_params
ActiveModelSerializers::Deserialization.jsonapi_parse!(params)
end
And here is what it returns:
{:code=>"BE", :name=>"BELGIUM", :modified_by=>"XXX", :id=>"5", :language_ids=>["374", "231", "69"]}
So I'm getting all I need:
country ID => id=5
languages IDS => both existing ones (2) and a new one.
How to properly update the country ? Thank you.
I figured out hot to add/delete an item to/from a association.
So on Ember side it looks like that:
actions: {
deleteLanguage(language) {
let controller = this.get('controller');
let country = controller.get('aCountry');
country.get('languages').removeObject(language);
country.set('modifiedBy', this.get('currentUser.user').get('username'))
country.save();
},
saveLanguage(language) {
let controller = this.get('controller');
let country = controller.get('aCountry');
country.get('languages').pushObject(language);
country.set('modifiedBy', this.get('currentUser.user').get('username'))
country.save();
}
And on the Rails side everything happens in the CountriesController:
class CountriesController < ApplicationController
...
def update
if #country.update(country_params)
json_response #country
else
json_response #country.errors, :unprocessable_entity
end
end
private
def find_country
#country = Country.includes(:languages).find(params[:id])
end
def country_params
ActiveModelSerializers::Deserialization.jsonapi_parse!(params,
only: [
:id,
:modified_by,
:languages
])
end
end
Sure, I'll have to add some errors handling on Ember side just to display a confirmation of the update or the errors.
Method json_response is just my custom helper defined as follows in concerns for controllers:
module Response
def json_response(object, status = :ok, opts = {})
response = {json: object, status: status}.merge(opts)
render response
end
end
Hope this helps. You can find more about mode relations in Ember Guide.
I am testing an angularjs directive that manipulates the DOM.
I am trying to get the element in my Jasmine spec, so that I can test the functionality of the directive. However, when I use document.getElementsByClassName or TagName or ID, it doesn't return anything. Does anyone have ideas about this?
html = document.getElementsByClassName('analog');
console.dir(html);
If you create a test in headless browser/chrome etc., you could append a dummy object, for example JQuery node, then remove that node in afterEach.
E.g.
beforeEach(() => {
var mockHtml = $('<div class="form-group" style="position: absolute;left: -10000px;"><input class="testInput" id="some_input"></div>');
$('body').append(mockHtml);
});
afterEach(() => {
$('.form-group').remove();
});
I am preparing SPA website containing hundreds of article-like pages (apart from eCommerce, login etc.). Every article has its own URL. I want to realize it using Angular2.
The only solution I found so far is:
1. to prepare hundreds of Agular2 components, one component for every article...
...with templateUrl pointing to article markup. So I will need hundreds of components similar to:
#core.Component({
selector: 'article-1',
templateUrl: 'article1.html'
})
export class Article1 {}
2. to display an article using AsyncRoute
see Lazy Loading of Route Components in Angular2
#core.Component({
selector: 'article-wrapper',
template: '<router-outlet></router-outlet>'
})
#router.RouteConfig([
new router.AsyncRoute({
path: '/article/:id',
loader: () => {
switch (id) {
case 1: return Article1;
case 2: return Article2;
//... repeat it hundreds of times
}
},
name: 'article'
})
])
class ArticleWrapper { }
In Angular1 there was ngInclude directive, which is missing in Angular2 due to the security issues (see here).
[Edit 1] There is not only problem with the code itself. Problem is also with static nature of this solution. If I need website with sitemap and dynamic page structure - adding a single page needs recompilation of the whole ES6 JavaScript module.
[Edit 2] The concept "markup x html as data" (where markup is not only static HTML but also HTML with active components) is basic concept of whole web (every CMS has its markup data in database). If there does not exist Angular2 solution for it, it denies this basic concept. I believe that there must exist some trick.
All following solutions are tricky. Official Angular team support issue is here.
Thanks to #EricMartinez for pointing me to #alexpods solution:
this.laoder.loadIntoLocation(
toComponent(template, directives),
this.elementRef,
'container'
);
function toComponent(template, directives = []) {
#Component({ selector: 'fake-component' })
#View({ template, directives })
class FakeComponent {}
return FakeComponent;
}
And another similar (from #jpleclerc):
#RouteConfig([
new AsyncRoute({
path: '/article/:id',
component: ArticleComponent,
name: 'article'
})
])
...
#Component({ selector: 'base-article', template: '<div id="here"></div>', ... })
class ArticleComponent {
public constructor(private params: RouteParams, private loader: DynamicComponentLoader, private injector: Injector){
}
ngOnInit() {
var id = this.params.get('id');
#Component({ selector: 'article-' + id, templateUrl: 'article-' + id + '.html' })
class ArticleFakeComponent{}
this.loader.loadAsRoot(
ArticleFakeComponent,
'#here'
injector
);
}
}
A bit different (from #peter-svintsitskyi):
// Faking class declaration by creating new instance each time I need.
var component = new (<Type>Function)();
var annotations = [
new Component({
selector: "foo"
}),
new View({
template: text,
directives: [WordDirective]
})
];
// I know this will not work everywhere
Reflect.defineMetadata("annotations", annotations, component);
// compile the component
this.compiler.compileInHost(<Type>component).then((protoViewRef: ProtoViewRef) => {
this.viewContainer.createHostView(protoViewRef);
});
CURRENT LOGIC
First, I think I have read all the articles on paperclip and I'm still stucked despite all the infos learned... So I can say taht your help is really really precious.
Secondly, I do not use delayed_paperclip, nor s3_direct_upload (but jquery directUpload).
I have users profile with 4 differents pictures : logo, avatar, worka, workb
Each format (logo, avatar, worka, workb) has 3 styles (:small, :thumb, :medium)
User can update its pictures => so update action is concerned
The 4 files fields are in a form with other classic fields (name, email, ...)
Upload is managed by jQuery DirectUpload + Paperclip
When user click the file field to add an image:
jQuery DirectUpload uploads the file into a temp directory on s3
jQuery callbacks with the url(key)
The url is assigned as :temp to an hidden field generated in javascript
When form submit button is pressed:
I assign to paperclip the url of the file uploaded with direct upload, with the help of #user.logo.temp which contains the url(key)
Paperclip generates all styles
<input type="hidden" name="user[logo_attributes][temp]" value="https://bucketname.s3.amazonaws.com/temp/e4b46d01-5d69-483b.jpg">
MY PROBLEM: STYLES GENERATION BRINGS ME TO HEROKU IDLE and TIMEOUT #15 #12
.
ATTEMPTS: I tried to isolate the upload process to put it in a background job
.
I can't figure out how to block paperclip styles generation at the first upload and generate them after in a background job
before_post_process block all post process, even in background job
I didn't use .reprocess! Since paperclip 4, update is triggered and... infinite loop..., so I use .assign and .save instead
The file is correctly assigned from S3 hosted file, then processed by paperclip
I'm not sure about the file from file field, if it is uploaded or not (no trace of that in the console, but since the form is submited, the file too, even if unused.
.
NEED: STYLES BLOCKED THEN PROCESSING IN A BACKGROUND JOB
My Logo Model
class Logo < Document
S3_TEMP_URL_FORMAT = %r{\/\/bucketname\.s3\.amazonaws\.com\/(?<path>temp\/.+\/(?<filename>.+))\z}.freeze
has_attached_file :attachment,
styles: { medium: "300x300#" },
convert_options: { medium: "-quality 75 -strip" },
default_url: ":parent_type/:class/:style/missing.png",
path: "/documents/:parent_type/:id_partition/:class/:style/:basename.:extension"
validates_attachment :attachment,
content_type: { content_type: ["image/gif", "image/png", "image/jpg", "image/jpeg"] },
size: { less_than: 1.megabyte }
validates :temp,
# presence: true,
format: { with: S3_TEMP_URL_FORMAT }
before_save :set_attachment
after_save :set_remote_url
before_post_process :stop_process
def stop_process
false
end
def styles_process
self.attachment.assign(attachment)
self.save
end
def set_attachment
# puts "BEGIN -- SET ATTACHMENT"
tries ||= 5
s3_temp_url_data = S3_TEMP_URL_FORMAT.match(self.temp)
s3 = AWS::S3.new
s3_temp_head = s3.buckets[ENV['S3_BUCKET']].objects[s3_temp_url_data[:path]].head
self.attachment_file_name = s3_temp_url_data[:filename]
self.attachment_file_size = s3_temp_head.content_length
self.attachment_content_type = s3_temp_head.content_type
self.attachment_updated_at = s3_temp_head.last_modified
rescue AWS::S3::Errors::NoSuchKey => e
tries -= 1
if tries > 0
sleep(3)
retry
else
false
end
end
def set_remote_url
s3_temp_url_data = S3_TEMP_URL_FORMAT.match(self.temp)
s3 = AWS::S3.new
self.attachment = URI.parse(self.temp)
self.save
s3.buckets[ENV['S3_BUCKET']].objects.with_prefix(s3_temp_url_data[:path]).delete_all
end
end
My Controller
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
#user = User.find(current_user.id)
if #user.update_attributes(account_update_params)
# Here is the styles processing
# This is where the Resque background job would go
#user.logo.styles_process
set_flash_message :notice, :updated
redirect_to after_update_path_for(#user)
else
render :edit
end
end
My Form
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), method: :put, html: { class: "form-horizontal directUpload", role: "form" }) do |f| %>
<%= f.fields_for :logo do |l| %>
<%= l.file_field(:attachment, accept: 'image/gif,image/png,image/jpg,image/jpeg') %>
<% end %>
<input type="hidden" name="user[logo_attributes][temp]" value="https://bucketname.s3.amazonaws.com/temp/e4b46d01-5d69-483b.jpg">
<% end %>