This makes sense - AA uses simple textareas, whereas we render the field as HTML on the front end. What we wanted was a Live Preview feature, where admin users could see EXACTLY how explanations were being displayed to users, within active admin. I thought, why not inject Angular and render the exact same directive we use?
Let's get into it.
To be specific, here is the template for the Angular tag I used for explanations:
<p>{{response.question.combined_letter_choices}}</p>
<p ng-bind-html="visibleExplanation"></p>
<p id="reference" ng-bind-html="reference"></p>
Here is the directive for that tag:
angular.module("exam.directives")
.directive('explanationText', ['$sce', function($sce) {
return {
restrict: "E",
templateUrl: 'exams/explanation_text.html',
link: function($scope) {
$scope.sanitizeAndSet = function(text) {
$scope.visibleExplanation = $sce.trustAsHtml(text);
// Used in ActiveAdmin to set explanation with jquery from outside.
}
}
}
}])
I wanted to render this tag inside ActiveAdmin, beneath the explanation name field. To inject it, first I required my angular manifest inside active_admin.js.coffee, the manifest used by AA:
//= require active_admin/base
//= require angular_dash.js
//= require lodash
(I used coffeescript before I switched to angular...forgive me, haven't swapped it out yet.)
Angular_dash.js is pretty simple:
//= require angular
//= require angular-route
//= require angular-rails-templates
//= require_tree ../templates
//= require lodash
//= require dashboard/exam/exam.js
//= require_tree ./dashboard/exam
This would load the angular library in active admin. Next I had to place it in the page. But how to do that? AA uses a DSL from the FormTastic library, it won't let you edit HTML directly. You could inject it with a $.append call after the fact, but then you have to compile the Angular app and bootstrap it yourself. That sucks.
The solution is a custom input field. FormTastic lets you create these custom input classes with custom to_html methods and then you can use them in the forms. Create a folder called app/inputs, and add this file to it, called angular_input.rb:
For ActiveAdmin before Rails 4.2:
class AngularInput
include Formtastic::Inputs::Base
include Formtastic::Inputs::Base::Placeholder
include Formtastic::Inputs::Base::Placeholder
def to_html
"<label class='label'><b>Live Preview</b></label>" +
"<div ng-app='exam' class='admin_explanation' id='explanation'>" +
"<explanation-text></explanation-text></div>"
end
end
For ActiveAdmin Rails 4.2+:
For ActiveAdmin Rails 4.2+:
class AngularInput
include Formtastic::Inputs::Base
def to_html
html = "<label class='label'><b>Live Preview</b></label>" +
"<div ng-app='exam' class='admin_explanation' id='explanation'>" +
"<explanation-text></explanation-text></div>"
input_wrapping do
builder.template.content_tag(:preview, html, { :id => "live_preview" }, false)
end
input_wrapping do
builder.template.content_tag(:preview, html, { :id => "live_preview" }, false)
end
end
end
Then you can add this angular input to your form by doing:
f.inputs :name => "Explanation", :for => [:explanation, f.object.explanation || Explanation.new] do |explanation_form|
explanation_form.input :name
explanation_form.input :preview, :as => :angular
....
As you can see, I just render it like any other input, and FormTastic injects my HTML and my angular app for me. It gets bootstrapped normally.
Finally, I want to tie the explanation name field to the angular rendered tag. I do this in active_admin.js.coffee:
# Automatically sets the field inside the explanation element in angular with the changes from the field.
# Uses a clone of the gsub logic on production-side to mimic the changes that happen there.
if($("#question_explanation_attributes_name"))
adjustAngular = (e) ->
visibleExplanation = $("#question_explanation_attributes_name").val().replace(/[\r\n]/g,"<br/>\n")
angular.element('#explanation explanation-text').scope().$apply((scope) ->
scope.sanitizeAndSet(visibleExplanation)
)
$("#question_explanation_attributes_name").on('keypress', adjustAngular);
adjustAngular();
Notice my use of the sanitizeAndSet method I defined in the directive. This is because you can't access the $sce service from $apply, but you can from the linking function.
The admin_explanation class I use in the video is this:
ol div.admin_explanation {
width: 76%;
margin-left: 20.5%;
margin-bottom: 20px;
background-color: white;
box-shadow: 0 1px 1px;
border-color:#dddddd;
border: 1px solid transparent;
border-radius: 4px;
}
if you're interested.
I made a screencast view of the result:
http://screencast.com/t/YPUenkiFjf (You'll have to copy paste this link)