Skip to content

Instantly share code, notes, and snippets.

@anaxamaxan
Last active August 29, 2015 14:03
Show Gist options
  • Save anaxamaxan/7e31832d1eb039931fd7 to your computer and use it in GitHub Desktop.
Save anaxamaxan/7e31832d1eb039931fd7 to your computer and use it in GitHub Desktop.
Proessing Blade Templates for Consumption by Angular (or other JS framework)
<!-- ng-controller="createOrganizationModalCtrl" -->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="$dismiss()" aria-hidden="true">&times;</button>
<h4 class="modal-title">
<?= trans('admin.organizations.create_new_organization') ?>
</h4>
</div>
<div class="modal-body">
@include('partials.editOrganizationForm')
<div ng-show="errors.length > 0" class="alert alert-danger">
<h6><?=trans('admin.organizations.failed_create_because')?></h6>
<span ng-repeat="error in errors">
@{{error}}<br/>
</span>
</div>
</div>
<div class="modal-footer">
<button ng-click="$dismiss()" class="btn btn-default">
<?=Icon::times_circle().' '.trans('application.cancel')?>
</button>
<button ng-click="dirtify(); submit();" class="btn btn-success">
<?=Icon::check().' '.trans('application.save')?>
</button>
</div>
</div>
var gulp = require('gulp');
var minifyCSS = require('gulp-minify-css');
var less = require('gulp-less');
var path = require('path');
var notify = require('gulp-notify');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var livereload = require('gulp-livereload');
var sys = require('sys');
var exec = require('child_process').exec;
var hold = false;
gulp.task('phpunit', function () {
if (!hold) {
hold = true;
exec('phpunit', function (error, stdout) {
sys.puts(stdout);
hold = false;
});
}
});
gulp.task('less', function () {
gulp.src('public/assets/less/app.less')
.pipe(less({
paths: [ path.join(__dirname, 'less', 'includes') ]
}))
//.pipe(notify("app.less compiled!"))
.pipe(gulp.dest('./public/assets/css'));
});
var endsWithDone = function(str) {
return str.indexOf('Done') == str.length - 'Done\n'.length;
};
gulp.task('concatLaravelViews', function () {
sys.puts('exec \"php artisan view:make-angular\"');
exec('php artisan view:make-angular', function (error, stdout) {
if (!endsWithDone(stdout))
sys.puts(stdout);
});
});
gulp.task('concatLaravelJs', function () {
sys.puts('exec \"php artisan asset:concat-js\"');
exec('php artisan asset:concat-js', function (error, stdout) {
if (!endsWithDone(stdout))
sys.puts(stdout);
});
});
gulp.task('concatLaravelLang', function () {
sys.puts('exec \"php artisan lang:make-angular\"');
exec('php artisan lang:make-angular', function (error, stdout) {
if (!endsWithDone(stdout))
sys.puts(stdout);
});
});
gulp.task('watch', function() {
// Create LiveReload server
var server = livereload();
//compile less files to /public/assets/css/app.css
gulp.watch('public/assets/less/*.less', ['less']).on('change', function(file) {
server.changed(file.path);
});
//compile angular views to /public/tpl/ng.html
gulp.watch('app/views/**/*.php',['concatLaravelViews']);
//compile js to /public/assets/js/app.js
gulp.watch('app/config/**/assets.php',['concatLaravelJs']);
gulp.watch(['public/assets/js/**/*.js', '!public/assets/js/app.js'], ['concatLaravelJs']);
//compile /app/lang/* to public/lang/*.json (all.json, en.json, es.json, etc)
gulp.watch('app/lang/**/*.php',['concatLaravelLang']);
});
//default task runs tasks 'less', 'concat-*' and 'watch'
gulp.task('default', ['less', 'concatLaravelViews', 'concatLaravelJs', 'concatLaravelLang', 'watch']);
<script type="text/ng-template" id="/templates/modals/createOrganization">
<!-- ng-controller="createOrganizationModalCtrl" -->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="$dismiss()" aria-hidden="true">&times;</button>
<h4 class="modal-title">
Create New Organization </h4>
</div>
<div class="modal-body">
<form accept-charset="UTF-8" class="form-horizontal" name="scope.editOrganizationForm" novalidate="novalidate" ng-submit="dirtify(); submit();">
<!-- submit button (invisible) required to make ng-submit fire -->
<button type="submit" style="display: none;"></button>
<!-- logo -->
<div class="form-group" ng-show="org.id">
<label for="address" class="control-label col-md-4">Logo</label>
<div class="col-md-8">
<img alt="{{org.name}}"
class="img-responsive"
style="margin-bottom: 10px;"
ng-src="{{(org.logo_url)?replaceDomain(org.logo_url, 'http://d3czs6rp6bbd7s.cloudfront.net')+'/convert?h=250&w=400&fit=max&format=jpg&quality=90':'/assets/img/no_logo.png'}}"
ng-click="changeLogoPic()"
/>
<a href="javascript:void(0);" ng-click="changeLogoPic()">
<i class="fa fa-cloud-upload"></i>&nbsp;Upload Logo </a>
<a href="javascript:void(0);" ng-show="org.logo_url != ''" ng-click="deleteLogoPic('Are you sure you want to delete the organization logo?')">
&nbsp;&nbsp;|&nbsp;&nbsp;<i class="fa fa-trash-o"></i>&nbsp;&nbsp;Delete Logo </a>
</div>
</div>
<!-- name -->
<div class="form-group">
<label for="name" class="control-label col-md-4">Name</label>
<div class="col-md-8">
<input id="name" name="name" type="text" ng-model="org.name" class="form-control" required />
<span class="text text-error" ng-show="editOrganizationForm.name.$dirty && editOrganizationForm.name.$error.required">
The Name field is required. </span>
</div>
</div>
<!-- description -->
<div class="form-group">
<label for="description" class="control-label col-md-4">Description</label>
<div class="col-md-8">
<textarea id="description" name="description" type="text" class="form-control" style="height: 100px;"
ng-model="org.description"></textarea>
</div>
</div>
<!-- phone -->
<div class="form-group">
<label for="phone" class="control-label col-md-4">Contact Phone</label>
<div class="col-md-8">
<input id="phone" name="phone" type="text" ng-model="org.phone" placeholder="###-###-####" class="form-control" required ng-pattern="/^(1[\- ]?)?(\([2-9]\d{2}\)|[2-9]\d{2})[\- ]?[2-9]\d{2}[\- ]?\d{4}$/" />
<span class="text text-error" ng-show="editOrganizationForm.phone.$dirty && editOrganizationForm.phone.$error.required">
The Contact Phone field is required. </span>
<span class="text text-error" ng-show="editOrganizationForm.phone.$dirty && editOrganizationForm.phone.$error.pattern">
Please enter a valid phone number of the form: ###-###-#### </span>
</div>
</div>
<!-- url -->
<div class="form-group">
<label for="url" class="control-label col-md-4">Url</label>
<div class="col-md-8">
<input id="url" name="url" type="text" class="form-control" ng-model="org.url" placeholder="www.example.com" required />
<span class="text text-error" ng-show="editOrganizationForm.url.$dirty && editOrganizationForm.url.$error.required">
The Url field is required. </span>
<span class="text text-error" ng-show="!isValidatingOrgUrl && !isValidOrgUrl && editOrganizationForm.url.$dirty && !editOrganizationForm.url.$error.required">
Please enter a valid URL. </span>
<span class="text text-warning" ng-show="isValidatingOrgUrl">
Checking URL is valid... </span>
</div>
</div>
<!-- address -->
<div class="form-group">
<label for="address" class="control-label col-md-4">Address</label>
<div class="col-md-8">
<textarea id="address" name="address" type="text" class="form-control" style="height: 100px;"
ng-model="org.mailing_address"></textarea>
</div>
</div>
<!-- owner -->
<div class="form-group">
<label for="owner" class="control-label col-md-4">Owner</label>
<div class="col-md-8">
<input type="text" ng-model="org.owner" id="owner" name="owner" title="{{org.owner.name}}" placeholder="Owner's name"
typeahead-template-url="/templates/modals/partials/ownerTypeahead"
typeahead-min-length="1"
typeahead="owner as owner.name for owner in possibleOwners | filter:$viewValue | limitTo:8"
autocomplete="off"
class="form-control input-sm" /><br/>
<span class="text text-error" ng-show="org.owner == undefined || (!org.owner.id && org.owner.length > 0)">
Specified owner not found.<br/>
<a href="javascript:void(0);" class="btn btn-xs btn-success" ng-click="openCreateOwnerAccountModal()">
Create Owner Account </a>
</span>
</div>
</div>
<!-- type -->
<div class="form-group">
<label for="type" class="control-label col-md-4">Type</label>
<div class="col-md-8">
<select id="type" name="type" class="form-control input-small" ng-model="org.type">
<option value="academic">Academic</option>
<option value="clinical">Clinical</option>
</select>
</div>
</div>
<!-- curriculum -->
<div class="form-group">
<label for="curriculum" class="control-label col-md-4">Curriculum</label>
<div class="col-md-8">
<select id="curriculum" name="curriculum" class="form-control input-small"
ng-model="org.website_id"
ng-options="site.id as site.name for site in sites">
<!--
this option is added as first element of $scope.sites[]
<option value="0">All Curricula</option>
-->
</select>
</div>
</div>
<!-- active -->
<div class="form-group" ng-show="org.active !== undefined">
<label class="col-md-4 form-label">
Active </label>
<div class="col-md-8 form-controls">
<input type="checkbox" ng-model="org.active">
</div>
</div>
<!-- verified -->
<div class="form-group" ng-show="org.verified !== undefined">
<label class="col-md-4 form-label">
Verified </label>
<div class="col-md-8 form-controls">
<input type="checkbox" ng-model="org.verified" ng-disabled="org.wasAlreadyVerified">
</div>
</div>
</form>
<div ng-show="errors.length > 0" class="alert alert-danger">
<h6>Failed to create organization because:</h6>
<span ng-repeat="error in errors">
{{error}}<br/>
</span>
</div>
</div>
<div class="modal-footer">
<button ng-click="$dismiss()" class="btn btn-default">
<i class="fa fa-times-circle"></i> Cancel </button>
<button ng-click="dirtify(); submit();" class="btn btn-success">
<i class="fa fa-check"></i> Save </button>
</div>
</div>
</script>
Basically, we compile all view templates into a single ng.html template that our frontend app consumes. This app is currently in private development, so I can't give a link to demo it, but the process is simple:
1. Create a Command to process the relevant Blade templates and concatenate into a single static HTML file, with each view encapsulated in a <script type="text/html"> element. That static file gets served by webserver from the public directory, gzipped by server and cached by browser. Could be served via CDN.
2a. On dev machines, add a gulp watcher to call this command whenever a file within the view directory changes.
2b. On production, call the process command in deploy script.
In our case, we put all blade templates intended for the front end in app/views/angular -- this keeps other blade templates separate, such as email messages.
I've included 5 files here:
1. The actual Command script that processes the Angular view files.
2. Our gulpfile.js - As you can see this also does a few other things, like process the less, js and lang files; so it looks a little more complex than it really is.
3. app/views/layouts/view.blade.php - a "layout" for the container views. Not necessary, just including here to show that layouts can be useful in this scenario too.
4. createOrganization.blade.php - An example blade template that gets processed into an HTML template for Angular. As you can see, we have a mix of blade includes, php helper calls and angular code.
5. A snippet from our ng.html file showing the result of processing the example blade template.
{{-- This is the simple view layout for Angular view templates --}}
@section('aboveContainer')
@stop
<div class="container public wrapper">
<div class="row">
@yield('content')
</div>
</div>
<?php namespace Sa\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Config;
use File;
use View;
class ViewProcessAngular extends Command {
/**
* The console command name.
*
* @var string
*/
protected $name = 'view:make-angular';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process the angular template views into static HTML files';
/**
* The path where we'll store the processed view HTML.
* Make sure this is read-write enabled for webserver user.
*
* @var string
*/
protected $tplCacheDir;
/**
* The path to the views we're going to process
*
* @var string
*/
protected $viewDir;
/**
* The name of the static output file.
*
* @var string
*/
protected $cacheFileName = 'ng.html';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->tplCacheDir = public_path('tpl');
$this->viewDir = app_path('views/angular');
}
/**
* Execute the console command.
*
* @return mixed
*/
public function fire()
{
if (! (File::isWritable($this->tplCacheDir) or File::makeDirectory($this->tplCacheDir,0777))) {
$this->error("\n\nTemplate cache directory ".$this->tplCacheDir." is not writable.\n\n");
return;
}
$this->info('Finding angular view files...');
// Get the list of files to process
$allFilePaths = $this->getTemplateFiles();
$this->info(count($allFilePaths).' view files found.');
// Path to our output file
$cacheFilePath = $this->tplCacheDir.'/'.$this->cacheFileName;
$this->initCacheFile($cacheFilePath);
$this->info('Writing to '.$cacheFilePath);
//iterate over each file path, process the view, and append to output
foreach ($allFilePaths as $idx => $path) {
$outputHtml = "\n".'<script type="text/ng-template" id="/templates/'.$path.'">'."\n"
. $this->getProcessedView($path)
. "\n</script>\n";
File::append($cacheFilePath,$outputHtml);
$this->info($idx.'. '.$path.' processed.');
}
$this->info('Done');
}
/**
* Recursively iterate through the view directory and return an array of all the view file paths.
*
* @return array
*/
protected function getTemplateFiles()
{
$allFilePaths = File::allFiles($this->viewDir);
foreach ($allFilePaths as $idx => $val) {
$allFilePaths[$idx] = str_replace([$this->viewDir.'/','.blade.php'],['',''],$val);
}
return $allFilePaths;
}
protected function getTimestamp()
{
$tz = Config::get('sa.default_timezone');
$now = Carbon::now()->setTimezone($tz);
return $now.' ('.$tz.' timezone)';
}
/**
* Process the view and return as a string
*
* @param $viewPath
* @return string
*/
protected function getProcessedView($viewPath)
{
try {
return View::make('angular/'.$viewPath)->render();
} catch (\Exception $e) {
$this->error($e->getMessage()."\n\n".$e->getTraceAsString()."\n\n");
exit;
}
}
/**
* Initialize the cache file with a simple header comment
*
* @param $cacheFilePath
*/
protected function initCacheFile($cacheFilePath)
{
// Create the file, with an initial content header
$outputHtml = "<!-- ====== Template HTML generated at ".$this->getTimestamp()." ====== -->\n";
File::put($cacheFilePath,$outputHtml);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment