$30 off During Our Annual Pro Sale. View Details »

Life on the edge between AngularJS and Symfony2

Life on the edge between AngularJS and Symfony2

Describes some tools for Symfony2 developers that might automate processes on Front-End side. Also solves some AngularJS and Symfony2 integration problems.

Armen Mkrtchyan

October 31, 2014
Tweet

More Decks by Armen Mkrtchyan

Other Decks in Technology

Transcript

  1. Symfony2 and
    AngularJS
    SensioLabs

    View Slide

  2. Introduction

    View Slide

  3. Armen Mkrtchyan
    @iamtankist
    ├─ #Symfony2
    ├─ #AngularJS
    ├─ #WebDev
    └─ #DevOps

    View Slide

  4. Why AngularJS
    • Bidirectional Data-Binding
    • Directives
    • Dependency Injection
    • Enables TDD

    View Slide

  5. Why Symfony2
    • Components
    • Twig, Doctrine, Monolog integration
    • Dependency Injection
    • Enables TDD

    View Slide

  6. 1. Assetic - no more*
    2. Introduce Bower + Gulp + Browserify
    3. Solve Template issues
    4. Solve Routing issues
    5. Forms
    6. Translations
    * applies only to specific target audience
    The Plan

    View Slide

  7. Why (not) Assetic?
    • Assetic is an asset management
    framework
    • Inspired by webassets (Python)
    • 30+ built in filters

    View Slide

  8. Removing Assetic
    • Remove dependency from composer.json
    "symfony/assetic-bundle": "~2.3"
    • Remove Bundle from app/AppKernel.php
    new Symfony\Bundle\AsseticBunle\AsseticBundle();
    • Remove assetic sections from config.yml
    and config_dev.yml
    • Run composer update and you’re done.

    View Slide

  9. Bower

    View Slide

  10. Why Bower
    • Package manager
    • Powerful Console tool
    • De-facto industry standard

    View Slide

  11. Installing Bower
    • System wide installation
    npm install -g bower
    • Create .bowerrc in the root of your project
    {
    "directory": "vendors_js"
    }
    • Create bower.json in the root of your project
    • Execute bower install

    View Slide

  12. bower.json
    {
    "name": "smoov.io",
    "version": "0.0.1",
    "dependencies": {
    "jquery": "~2.1.1",
    "bootstrap": "~3.1.1",
    "angular": "~1.2.16"
    }
    }

    View Slide

  13. Gulp

    View Slide

  14. Why Gulp?
    • Speed (Node Streams vs File System)
    • 700+ registered plugins
    • JS Syntax

    View Slide

  15. gulpfile.js
    // Plugins
    var gulp = require('gulp'),
    less = require('gulp-less');
    // Tasks
    gulp.task('stylesheets', function() {
    gulp.src(['path/to/main.less'])
    .pipe(less({ compress: true }))
    .on('error', function (){ console.log(); })
    .pipe(gulp.dest('web/css'));
    });
    // Watchers
    gulp.task('watch', function() {
    gulp.watch(['path/to/*.less'], ['stylesheets']);
    });
    // Entry Point
    gulp.task('default', ['stylesheets', 'watch'])

    View Slide

  16. Browserify

    View Slide

  17. Modules in Web
    • Global Variables
    var MyModule = function() {};
    • AMD
    define(['my-module'],
    function(MyModule){
    return function() {};
    });

    View Slide

  18. Modules in the Wild
    • CommonJS
    var MyModule = require('my-module');
    module.exports = function() {};
    • ES6
    import { MyModule } from 'my-module';
    export function mymodule() {}

    View Slide

  19. UMD
    (function (root, factory) {

    if (typeof define === 'function' && define.amd) {

    // AMD

    define(['jquery'], factory);

    } else if (typeof exports === 'object') {

    // Node, CommonJS-like

    module.exports = factory(require('jquery'));

    } else {

    // Browser globals (root is window)

    root.returnExports = factory(root.jQuery);

    }

    }(this, function ($) {

    // methods

    function myFunc(){};


    // exposed public method

    return myFunc;

    }));

    View Slide

  20. Browserify Usage
    • Export
    // greet.js

    module.exports = function (name) {

    console.log('Hello ' + name);

    };
    • Usage
    // main.js

    var greet = require('./greet.js');

    greet('World');
    • Generation
    browserify main.js -o bundle.js

    View Slide

  21. Browserify + Gulp
    gulp.task('scripts', function() {
    gulp.src('main.js')
    .pipe(browserify())
    .pipe(concat('bundle.js'))
    .pipe(gulp.dest('web/js'));
    });

    View Slide

  22. Browserify + AngularJS
    // app.js
    var app = angular.module('myApp', []);
    app.factory('myService', ['$http',
    require('./services/myService')
    ]);
    // services/myService.js
    module.exports = function ($http)
    {
    //awesome service goes here
    }

    View Slide

  23. Template Conflicts

    View Slide

  24. Template Conflicts
    AngularJS
    {{ object.property }}
    TWIG
    {{ object.property }}

    View Slide

  25. Template Conflicts
    • Changing {{ }} to [[ ]] in AngularJS
    var myApp = angular.module('myApp', [])
    .config(function($interpolateProvider){
    $interpolateProvider
    .startSymbol('[[')
    .endSymbol(']]');
    });
    • Using {% verbatim %} in TWIG
    {# outputs AngularJS foo.bar expression #}
    {% verbatim %}{{foo.bar}}{% endverbatim %}

    View Slide

  26. Route Generation

    View Slide

  27. Routing
    • Route Definition in Symfony2
    # app/config/routing.yml
    foo_bar_buzz:
    path: /foo/{id}
    defaults: {…}
    • Generated Url for dev front controller
    /app_dev.php/foo/1243?limit=10
    • Generated Url for prod front controller
    /foo/1243?limit=10&offset=20

    View Slide

  28. FOSJsRoutingBundle
    • Exposes routes through whitelisting
    • Generates JSON structure with exposed routes
    • Comes with a command to dump routes into a file

    View Slide

  29. Exposing Routes
    • routing.yml
    my_route_to_expose:
    pattern: /buzz/{id}
    defaults: { _controller: FooBundle:Bar:buzz }
    options:
    expose: true
    • Annotations
    /**
    * @Route("/buzz/{id}", name="foo_bar_buzz",
    options={"expose"=true})
    */
    public function buzzAction($id)
    • config.yml
    fos_js_routing:
    routes_to_expose: [ foo_bar_buzz, ... ]

    View Slide

  30. Exposing Routes
    • Before
    # app/config/config.yml
    fos_js_routing:
    routes_to_expose: [ foo_bar_buzz, ... ]
    • After
    # app/config/config.yml
    imports:
    - { resource: routes_exposed.yml }
    # app/config/routes_exposed.yml
    fos_js_routing:
    routes_to_expose:
    - foo_bar_buzz

    View Slide

  31. Dump, Gulp’em all
    // plugin
    var shell = require('gulp-shell');
    // task
    gulp.task('router',
    shell.task(
    'php app/console fos:js-routing:dump —target="./r.js"'
    )
    );
    // watcher
    gulp.task('watch', function() {
    gulp.watch('app/config/routes_exposed.yml','router');
    });

    View Slide

  32. Default Usage
    • Route Definition in Symfony2
    # app/config/routing.yml
    foo_bar_buzz:
    path: /foo/bar/{id}
    defaults: {…}
    • Route usage in JavaScript
    Routing.generate('foo_bar_buzz', { id: 2 });

    View Slide

  33. AngularJS Service
    • Service Definition
    // public/app.js
    smoovio.factory('$url',
    require('./common/service/url')
    );
    • Service Itself
    // public/common/service/url.js
    module.exports = function () {
    return {
    generate: function (name, parameters, isAbsolute) {
    return Routing.generate(name, parameters, isAbsolute);
    }
    };
    }

    View Slide

  34. AngularJS Usage
    • Inside some service
    // fooRepository.js
    return $http.get(
    $url.generate('foo_bar_buzz', {'id': id});
    );

    View Slide

  35. Forms

    View Slide

  36. View Slide

  37. Form Itself
    class RegistrationType extends AbstractType

    {

    public function buildForm(FormBuilderInterface $builder, array $options)

    {

    $builder->add('username', 'text');

    $builder->add('email', 'email');

    $builder->add('password', 'repeated', [

    'first_name' => 'password',

    'second_name' => 'confirm',

    'type' => 'password',

    ]);

    $builder->add(

    'terms',

    'checkbox',

    ['property_path' => 'termsAccepted']

    );

    $builder->add('Sign Up!', 'submit');

    }


    public function getName()

    {

    return 'registration';

    }

    }

    View Slide

  38. Symfony2 Controller
    /**

    * @Template()

    */

    public function registerAction(Request $request)

    {

    $form = $this->createForm(new RegistrationType(), new Registration());

    $userManager = $this->getUserManager();


    $form->handleRequest($request);


    if ($form->isValid()) {

    $userManager->createUser($form->getData());

    return $this->redirect($this->generateUrl('portal_home'));


    }


    return ['form' => $form->createView()];

    }

    View Slide

  39. Rendering The Form
    {% block content %}

    {{ form(form) }}

    {% endblock %}

    View Slide

  40. Rendered Form




    Username


    id="registration_username"

    name="registration[username]"

    required="required">




    Sign up!


    id="registration__token"

    name="registration[_token]"

    value="the_token">



    View Slide

  41. AngularJS Friendly Form




    Username


    ng-model="data['registration[username]']"

    id="registration_username"

    name="registration[username]"

    required="required">




    Sign up!


    ng-init="data['registration[_token]'] = 'the_token'"

    id="registration__token"

    name="registration[_token]"

    value="the_token">



    View Slide

  42. Form Themes - Start
    {# app/Resources/views/form_angular_layout.html.twig #}

    {% extends 'form_div_layout.html.twig' %}
    {% block form_start -%}

    {% set method = method|upper %}

    {%- if method in ["GET", "POST"] -%}

    {% set form_method = method %}

    {%- else -%}

    {% set form_method = "POST" %}

    {%- endif -%}

    {%- if form_method != method -%}


    {%- endif -%}

    {%- endblock form_start %}

    View Slide

  43. Form Themes - Attributes
    {# app/Resources/views/form_angular_layout.html.twig #}

    {% extends 'form_div_layout.html.twig' %}
    {% block form_start -%}

    ...
    {%- endblock form_start %}
    {% block widget_attributes -%}

    {% if "hidden" in form.vars.block_prefixes %}

    ng-init="data['{{ full_name }}'] = '{{value}}'"

    {% else %}

    ng-model="data['{{ full_name }}']"

    {% endif %}

    id="{{ id }}" name="{{ full_name }}"
    {# rest #}
    {%- endblock widget_attributes %}


    View Slide

  44. Rendering - AngularJS Way
    {% form_theme form "form_angular_layout.html.twig" %}
    {% block content %}

    {{ form(form) }}

    {% endblock %}

    View Slide

  45. Including Form
    id="formContainer"

    ng-controller="myFormRenderCtrl"
    ng-include="formUrl"

    >

    View Slide

  46. Form Controller
    app.controller('myFormRenderCtrl', ['$scope', '$url', '$http', '$compile',

    function ($scope, $url, $http, $compile) {

    $scope.formUrl = $url.generate('my_form_route_name');


    $scope.data = {};


    $scope.submit = function () {

    $http.post($scope.formUrl, $scope.data, {

    headers: {
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
    },

    transformRequest: function (data) {

    return $.param(data);

    }

    }).success(

    function (response) {

    $('#formContainer').html($compile(response)($scope));

    }

    );

    }

    }]);

    View Slide

  47. i18n and l10n

    View Slide

  48. View Slide

  49. angular-translate
    • http://angular-translate.github.io/

    View Slide

  50. Features
    • Fallback language support
    • JSON format support
    • Custom Loaders
    • Loader Cache
    • Highly customisable

    View Slide

  51. Config
    app.config(['$translateProvider', function ($translateProvider) {
    $translateProvider

    .preferredLanguage('en')

    .fallbackLanguage('en');
    $translateProvider.useLoaderCache(true);
    $translateProvider.useStaticFilesLoader({

    prefix: '/translations/locale-',

    suffix: '.json'

    });
    $translateProvider.storageKey('locale');

    $translateProvider.storagePrefix('app_');

    $translateProvider.useStorage('customStorage');
    }])

    View Slide

  52. Config
    app.
    $translateProvider

    .preferredLanguage('en')

    .fallbackLanguage('en');
    $translateProvider.useStaticFilesLoader({
    }])

    View Slide

  53. Config
    app.
    .
    .
    $translateProvider.useLoaderCache(true);
    $translateProvider.useStaticFilesLoader({

    prefix: '/translations/locale-',

    suffix: '.json'

    });
    }])

    View Slide

  54. /translations/locale-de.json
    {

    APP_HEADLINE: 'Großartige App',

    NAV_HOME: 'Zur Startseite',

    NAV_ABOUT: 'Über',

    APP_TEXT: 'Irgendein Text'

    }

    View Slide

  55. Copying the translations
    gulp.task('translations', function () {

    gulp.src([

    'path_to_bundle/Resources/translations/**'

    ])

    .pipe(gulp.dest('web/translations/'));

    });
    gulp.watch([

    'path_to_bundle/Resources/translations/**'

    ], ['templates']);
    gulp.task('default', [..., 'translations'])

    View Slide

  56. Config
    app.
    .
    .
    $translateProvider.useStaticFilesLoader({
    $translateProvider.storagePrefix('app_');
    $translateProvider.storageKey('locale');

    $translateProvider.useStorage('customStorage');
    }])

    View Slide

  57. Custom Service
    app.factory('customStorage', function ($window) {

    return {

    set: function (name, value) {

    },

    get: function (name) {

    return $window.appSettings[name];

    }

    };

    });

    var appSettings = {"app_locale": "de"}

    View Slide

  58. Q & A
    SensioLabs

    View Slide

  59. Thanks!
    http://tinyurl.com/angular-symfony
    SensioLabs

    View Slide