Membangun Aplikasi Web Modern dengan AngularJS dan Play Framework
Diterbitkan: 2022-03-11Memilih alat yang tepat untuk tujuan yang tepat sangat membantu, terutama dalam hal membangun aplikasi web modern. Banyak dari kita yang akrab dengan AngularJS dan betapa mudahnya mengembangkan aplikasi web yang tangguh. Meskipun banyak yang akan menentang penggunaan kerangka kerja web populer ini, tentu saja memiliki banyak hal untuk ditawarkan dan dapat menjadi pilihan yang tepat untuk beragam kebutuhan. Di sisi lain, komponen yang Anda gunakan di back-end akan menentukan banyak tentang kinerja aplikasi web, karena mereka memiliki pengaruh pada pengalaman pengguna secara keseluruhan. Play adalah kerangka kerja web berkecepatan tinggi untuk Java dan Scala. Ini didasarkan pada arsitektur yang ringan, tanpa kewarganegaraan, ramah-web dan mengikuti pola dan prinsip MVC yang serupa dengan Rails dan Django.
Pada artikel ini, kita akan melihat bagaimana kita dapat menggunakan AngularJS dan Play untuk membangun aplikasi blog sederhana dengan mekanisme otentikasi dasar dan kemampuan untuk membuat posting dan komentar. Pengembangan AngularJS, dengan beberapa fitur Bootstrap Twitter, akan memungkinkan kami untuk memperkuat pengalaman aplikasi satu halaman di atas back-end REST API berbasis Play.
Aplikasi Web - Memulai
Kerangka Aplikasi AngularJS
Aplikasi AngularJS dan Play akan berada di direktori klien dan server yang sesuai. Untuk saat ini, kita akan membuat direktori "client".
mkdir -p blogapp/client
Untuk membuat kerangka aplikasi AngularJS, kami akan menggunakan Yeoman - alat perancah yang luar biasa. Menginstal Yeoman itu mudah. Menggunakannya untuk membuat kerangka aplikasi AngularJS kerangka sederhana mungkin lebih mudah:
cd blogapp/client yo angular
Menjalankan perintah kedua akan diikuti oleh beberapa opsi yang perlu Anda pilih. Untuk proyek ini, kita tidak membutuhkan “Sass (with Compass)”. Kami akan membutuhkan Boostrap bersama dengan plugin AngularJS berikut:
- angular-animate.js
- angular-cookies.js
- angular-resource.js
- angular-route.js
- angular-sanitize.js
- angular-touch.js
Pada titik ini, setelah Anda menyelesaikan pilihan Anda, Anda akan mulai melihat output NPM dan Bower di terminal Anda. Ketika unduhan selesai dan paket telah diinstal, Anda akan memiliki kerangka aplikasi AngularJS yang siap digunakan.
Mainkan Kerangka Aplikasi Kerangka
Cara resmi untuk membuat aplikasi Play baru melibatkan penggunaan alat Typesafe Activator. Sebelum Anda dapat menggunakannya, Anda harus mengunduh dan menginstalnya di komputer Anda. Jika Anda menggunakan Mac OS dan menggunakan Homebrew, Anda dapat menginstal alat ini dengan satu baris perintah:
brew install typesafe-activator
Membuat aplikasi Play dari baris perintah sangat mudah:
cd blogapp/ activator new server play-java cd server/
Mengimpor ke IDE
Untuk mengimpor aplikasi dalam IDE seperti Eclipse atau IntelliJ, Anda perlu "mengeclipsify" atau "mengidealisasikan" aplikasi Anda. Untuk melakukannya, jalankan perintah berikut:
activator
Setelah Anda melihat prompt baru, ketik "eclipse" atau "idea" dan tekan enter untuk menyiapkan kode aplikasi masing-masing untuk Eclipse atau IntelliJ.
Untuk singkatnya, kami hanya akan membahas proses mengimpor proyek ke IntelliJ dalam artikel ini. Proses mengimpornya ke Eclipse harus sama sederhananya. Untuk mengimpor proyek ke IntelliJ, mulailah dengan mengaktifkan opsi "Proyek dari sumber yang ada ..." yang ditemukan di bawah "File -> Baru". Selanjutnya, pilih file build.sbt Anda dan klik "OK". Setelah mengklik "OK" lagi pada dialog berikutnya, IntelliJ harus mulai mengimpor aplikasi Play Anda sebagai proyek SBT.
Typesafe Activator juga dilengkapi dengan antarmuka pengguna grafis, yang dapat Anda gunakan untuk membuat kode aplikasi kerangka ini.
Sekarang kita telah mengimpor aplikasi Play kita ke IntelliJ, kita juga harus mengimpor aplikasi AngularJS kita ke dalam workspace. Kami dapat mengimpornya sebagai proyek terpisah atau sebagai modul ke proyek yang ada di mana aplikasi Play berada.
Di sini, kita akan mengimpor aplikasi Angular sebagai modul. Di bawah menu “File”, kita akan memilih opsi “New -> Module From Existing Sources…”. Dari dialog, kita akan memilih direktori "klien" dan klik "OK". Pada dua layar berikutnya, klik "Next" dan "Finish", masing-masing.
Pemijahan Server Lokal
Pada titik ini, seharusnya mungkin untuk memulai aplikasi AngularJS sebagai tugas Grunt dari IDE. Perluas folder klien Anda dan klik kanan pada Gruntfile.js. Di menu pop-up pilih "Show Grunt Tasks". Panel berlabel "Grunt" akan muncul dengan daftar tugas:
Untuk mulai melayani aplikasi, klik dua kali pada “serve”. Ini akan segera membuka browser web default Anda dan mengarahkannya ke alamat localhost. Anda akan melihat halaman rintisan AngularJS dengan logo Yeoman di atasnya.
Selanjutnya, kita perlu meluncurkan server aplikasi back-end kita. Sebelum kita dapat melanjutkan, kita harus mengatasi beberapa masalah:
- Secara default, baik aplikasi AngularJS (bootstrap oleh Yeoman) dan aplikasi Play mencoba berjalan pada port 9000.
- Dalam produksi, kedua aplikasi kemungkinan akan dijalankan di bawah satu domain, dan kami mungkin akan menggunakan Nginx untuk merutekan permintaan yang sesuai. Namun dalam mode pengembangan, ketika kami mengubah nomor port dari salah satu aplikasi ini, browser web akan memperlakukannya seolah-olah mereka berjalan di domain yang berbeda.
Untuk mengatasi kedua masalah ini, yang perlu kita lakukan adalah menggunakan proxy Grunt sehingga semua permintaan AJAX ke aplikasi Play diproksi. Dengan ini, pada dasarnya kedua server aplikasi ini akan tersedia pada nomor port yang sama.
Mari kita ubah dulu nomor port server aplikasi Play ke 9090. Untuk melakukannya, buka jendela "Run/Debug Configurations" dengan mengklik "Run -> Edit Configurations". Selanjutnya, ubah nomor port pada kolom “Url To Open”. Klik "OK" untuk menyetujui perubahan ini dan tutup jendela. Mengklik tombol "Jalankan" akan memulai proses resolusi ketergantungan - log dari proses ini akan mulai muncul.
Setelah selesai, Anda dapat menavigasi ke http://localhost:9090 di browser web Anda, dan dalam beberapa detik Anda akan dapat melihat aplikasi Play Anda. Untuk mengonfigurasi proxy Grunt, pertama-tama kita perlu menginstal paket Node.js kecil menggunakan NPM:
cd blogapp/client npm install grunt-connect-proxy --save-dev
Selanjutnya kita perlu men-tweak Gruntfile.js kita. Di file itu, cari tugas "sambungkan", dan masukkan kunci/nilai "proksi" setelahnya:
proxies: [ { context: '/app', // the context of the data service host: 'localhost', // wherever the data service is running port: 9090, // the port that the data service is running on changeOrigin: true } ],
Grunt sekarang akan mem-proxy semua permintaan ke “/ app/*” ke aplikasi Play back-end. Ini akan menyelamatkan kita dari keharusan memasukkan setiap panggilan ke back-end. Selanjutnya, kita juga perlu mengubah perilaku livereload kita:
livereload: { options: { open: true, middleware: function (connect) { var middlewares = []; // Setup the proxy middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest); // Serve static files middlewares.push(connect.static('.tmp')); middlewares.push(connect().use( '/bower_components', connect.static('./bower_components') )); middlewares.push(connect().use( '/app/styles', connect.static('./app/styles') )); middlewares.push(connect.static(appConfig.app)); return middlewares; } } },
Terakhir, kita perlu menambahkan dependensi baru “'configureProxies:server” ke tugas “serve”:
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { if (target === 'dist') { return grunt.task.run(['build', 'connect:dist:keepalive']); } grunt.task.run([ 'clean:server', 'wiredep', 'concurrent:server', 'autoprefixer:server', 'configureProxies:server', 'connect:livereload', 'watch' ]); });
Saat memulai ulang Grunt, Anda akan melihat baris berikut di log Anda yang menunjukkan bahwa proxy sedang berjalan:
Running "autoprefixer:server" (autoprefixer) task File .tmp/styles/main.css created. Running "configureProxies:server" (configureProxies) task Running "connect:livereload" (connect) task Started connect web server on http://localhost:9000
Membuat Formulir Pendaftaran
Kita akan mulai dengan membuat formulir pendaftaran untuk aplikasi blog kita. Ini juga akan memungkinkan kami untuk memverifikasi bahwa semuanya berfungsi sebagaimana mestinya. Kita dapat menggunakan Yeoman untuk membuat pengontrol Pendaftaran dan melihat di AngularJS:
yo angular:controller signup yo angular:view signup
Selanjutnya kita harus memperbarui perutean aplikasi kita untuk mereferensikan tampilan yang baru dibuat ini, dan menghapus pengontrol dan tampilan "tentang" yang dibuat secara otomatis. Dari dalam file “app/scripts/app.js”, hapus referensi ke “app/scripts/controllers/about.js” dan “app/views/about.html”, biarkan dengan:
.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });
Demikian pula, perbarui file “app/index.html” untuk menghapus tautan yang berlebihan, dan tambahkan tautan ke halaman pendaftaran:
<div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#/">Home</a></li> <li><a ng-href="#/signup">Signup</a></li> </ul> </div> </div>
Juga, hapus tag skrip untuk "about.js":
<!-- build:js({.tmp,app}) scripts/scripts.js --> <script src="scripts/app.js"></script> <script src="scripts/controllers/main.js"></script> <script src="scripts/controllers/signup.js"></script> <!-- endbuild --> </body> </html>
Selanjutnya, tambahkan formulir ke file “signup.html” kami:
<form name="signupForm" ng-submit="signup()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Sign up!</button> </form>
Kita perlu membuat formulir diproses oleh pengontrol Angular. Perlu dicatat bahwa kita tidak perlu secara khusus menambahkan atribut "ng-controller" dalam tampilan kita, karena logika perutean kita di "app.js" menjalankan pengontrol secara otomatis sebelum tampilan kita dimuat. Yang harus kita lakukan untuk menghubungkan formulir ini adalah memiliki fungsi "pendaftaran" yang tepat yang didefinisikan dalam $scope. Ini harus dilakukan di file "signup.js":
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .success(function(data) { $log.debug(data); }); }; });
Sekarang mari kita buka konsol pengembang Chrome, beralih ke tab "Jaringan", dan coba kirimkan formulir pendaftaran.
Kita akan melihat bahwa back-end Play secara alami membalas dengan halaman kesalahan "Tindakan tidak ditemukan". Hal ini diharapkan karena belum dilaksanakan. Tapi itu juga berarti bahwa pengaturan proxy Grunt kami berfungsi dengan benar!
Selanjutnya, kita akan menambahkan "Aksi" yang pada dasarnya adalah metode di pengontrol aplikasi Play. Di kelas "Application" dalam paket "app/controllers", tambahkan metode baru "signup":
public static Result signup() { return ok("Success!"); }
Sekarang buka file "conf/routes" dan tambahkan baris berikut:
POST /app/signup controllers.Application.signup
Terakhir, kita kembali ke browser web kita, http://localhost:9000/#/signup. Mengklik tombol "Kirim" kali ini akan menghasilkan sesuatu yang berbeda:
Anda akan melihat nilai hardcode yang dikembalikan, yang kami tulis dalam metode pendaftaran. Jika itu masalahnya, kami siap untuk melanjutkan karena lingkungan pengembangan kami siap dan berfungsi untuk aplikasi Angular dan Play.
Mendefinisikan Model Ebean di Play
Sebelum mendefinisikan model, mari kita pilih datastore terlebih dahulu. Pada artikel ini, kita akan menggunakan database dalam memori H2. Untuk mengaktifkannya, temukan dan batalkan komentar pada baris berikut di file “application.conf”:
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play" db.default.user=sa db.default.password="" ... ebean.default="models.*"
Dan tambahkan baris berikut:
applyEvolutions.default=true
Model domain blog kami agak sederhana. Pertama-tama, kami memiliki pengguna yang dapat membuat posting dan kemudian setiap posting dapat dikomentari oleh pengguna yang masuk. Mari kita buat Model Ebean kita.
Pengguna
// User.java @Entity public class User extends Model { @Id public Long id; @Column(length = 255, unique = true, nullable = false) @Constraints.MaxLength(255) @Constraints.Required @Constraints.Email public String email; @Column(length = 64, nullable = false) private byte[] shaPassword; @OneToMany(cascade = CascadeType.ALL) @JsonIgnore public List<BlogPost> posts; public void setPassword(String password) { this.shaPassword = getSha512(password); } public void setEmail(String email) { this.email = email.toLowerCase(); } public static final Finder<Long, User> find = new Finder<Long, User>( Long.class, User.class); public static User findByEmailAndPassword(String email, String password) { return find .where() .eq("email", email.toLowerCase()) .eq("shaPassword", getSha512(password)) .findUnique(); } public static User findByEmail(String email) { return find .where() .eq("email", email.toLowerCase()) .findUnique(); } public static byte[] getSha512(String value) { try { return MessageDigest.getInstance("SHA-512").digest(value.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } }
BlogPost
// BlogPost.java @Entity public class BlogPost extends Model { @Id public Long id; @Column(length = 255, nullable = false) @Constraints.MaxLength(255) @Constraints.Required public String subject; @Column(columnDefinition = "TEXT") @Constraints.Required public String content; @ManyToOne public User user; public Long commentCount; @OneToMany(cascade = CascadeType.ALL) public List<PostComment> comments; public static final Finder<Long, BlogPost> find = new Finder<Long, BlogPost>( Long.class, BlogPost.class); public static List<BlogPost> findBlogPostsByUser(final User user) { return find .where() .eq("user", user) .findList(); } public static BlogPost findBlogPostById(final Long id) { return find .where() .eq("id", id) .findUnique(); } }
Kirim Komentar
// PostComment.java @Entity public class PostComment extends Model { @Id public Long id; @ManyToOne @JsonIgnore public BlogPost blogPost; @ManyToOne public User user; @Column(columnDefinition = "TEXT") public String content; public static final Finder<Long, PostComment> find = new Finder<Long, PostComment>( Long.class, PostComment.class); public static List<PostComment> findAllCommentsByPost(final BlogPost blogPost) { return find .where() .eq("post", blogPost) .findList(); } public static List<PostComment> findAllCommentsByUser(final User user) { return find .where() .eq("user", user) .findList(); } }
Tindakan Pendaftaran Nyata
Sekarang mari kita buat tindakan nyata pertama kita, memungkinkan pengguna untuk mendaftar:
// Application.java public static Result signup() { Form<SignUp> signUpForm = Form.form(SignUp.class).bindFromRequest(); if ( signUpForm.hasErrors()) { return badRequest(signUpForm.errorsAsJson()); } SignUp newUser = signUpForm.get(); User existingUser = User.findByEmail(newUser.email); if(existingUser != null) { return badRequest(buildJsonResponse("error", "User exists")); } else { User user = new User(); user.setEmail(newUser.email); user.setPassword(newUser.password); user.save(); session().clear(); session("username", newUser.email); return ok(buildJsonResponse("success", "User created successfully")); } } public static class UserForm { @Constraints.Required @Constraints.Email public String email; } public static class SignUp extends UserForm { @Constraints.Required @Constraints.MinLength(6) public String password; } private static ObjectNode buildJsonResponse(String type, String message) { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", message); wrapper.put(type, msg); return wrapper; }
Perhatikan bahwa otentikasi yang digunakan dalam aplikasi ini sangat mendasar, dan tidak disarankan untuk penggunaan produksi.
Bagian yang menarik adalah kami menggunakan formulir Play untuk menangani formulir pendaftaran. Kami menetapkan beberapa batasan pada kelas formulir SignUp kami. Validasi akan dilakukan untuk kita secara otomatis tanpa perlu logika validasi eksplisit.
Jika kami kembali ke aplikasi AngularJS kami di browser web dan mengklik "Kirim" lagi, kami akan melihat bahwa server sekarang merespons dengan kesalahan yang sesuai - bahwa bidang ini wajib diisi.
Menangani Kesalahan Server di AngularJS
Jadi kami mendapatkan kesalahan dari server, tetapi pengguna aplikasi tidak tahu apa yang terjadi. Paling tidak yang bisa kita lakukan adalah menampilkan kesalahan kepada pengguna kita. Idealnya, kita perlu memahami jenis kesalahan apa yang kita dapatkan dan menampilkan pesan yang mudah digunakan. Mari kita buat layanan peringatan sederhana yang akan membantu kita menampilkan kesalahan.
Pertama, kita perlu membuat template layanan dengan Yeoman:
yo angular:service alerts
Selanjutnya, tambahkan kode ini ke “alerts.js”:
angular.module('clientApp') .factory('alertService', function($timeout) { var ALERT_TIMEOUT = 5000; function add(type, msg, timeout) { if (timeout) { $timeout(function(){ closeAlert(this); }, timeout); } else { $timeout(function(){ closeAlert(this); }, ALERT_TIMEOUT); } return alerts.push({ type: type, msg: msg, close: function() { return closeAlert(this); } }); } function closeAlert(alert) { return closeAlertIdx(alerts.indexOf(alert)); } function closeAlertIdx(index) { return alerts.splice(index, 1); } function clear(){ alerts = []; } function get() { return alerts; } var service = { add: add, closeAlert: closeAlert, closeAlertIdx: closeAlertIdx, clear: clear, get: get }, alerts = []; return service; } );
Sekarang, mari buat pengontrol terpisah yang bertanggung jawab atas peringatan:
yo angular:controller alerts
angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });
Sekarang kita harus benar-benar menampilkan pesan kesalahan Bootstrap yang bagus. Cara termudah adalah dengan menggunakan Angular UI. Kita dapat menggunakan Bower untuk menginstalnya:
bower install angular-bootstrap --save
Di "app.js" Anda, tambahkan modul Angular UI:
angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])
Mari tambahkan arahan peringatan ke file "index.html" kami:
<div class="container"> <div ng-controller="AlertsCtrl"> <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="alert.close()">{{ alert.msg }}</alert> </div> <div ng-view=""></div> </div>
Terakhir, kita perlu memperbarui pengontrol SignUp:
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location, userService) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } if(status === 500) { alertService.add('danger', 'Internal server error!'); } }) }; });
Sekarang jika kami mengirim formulir kosong lagi, kami akan melihat kesalahan yang ditampilkan di atas formulir:

Sekarang setelah kesalahan ditangani, kita perlu melakukan sesuatu ketika pendaftaran pengguna berhasil. Kami dapat mengarahkan pengguna ke halaman dasbor tempat dia dapat menambahkan posting. Tapi pertama-tama, kita harus membuatnya:
yo angular:view dashboard yo angular:controller dashboard
Ubah metode pendaftaran pengontrol "signup.js" sehingga jika berhasil mengalihkan pengguna:
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });
Tambahkan rute baru di "apps.js":
.when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })
Kami juga perlu melacak jika pengguna masuk. Mari kita buat layanan terpisah untuk itu:
yo angular:service user
// user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });
Dan juga ubah pengontrol pendaftaran untuk mengatur pengguna menjadi pengguna yang baru saja mendaftar:
.success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });
Sebelum kita menambahkan fungsi utama menambahkan posting, mari kita perhatikan beberapa fitur penting lainnya seperti kemampuan untuk login dan logout, menampilkan informasi pengguna di dashboard, dan juga menambahkan dukungan otentikasi di back-end.
Otentikasi Dasar
Mari lompat ke aplikasi Play kami dan terapkan tindakan masuk dan keluar. Tambahkan baris tesis ke "Application.java":
public static Result login() { Form<Login> loginForm = Form.form(Login.class).bindFromRequest(); if (loginForm.hasErrors()) { return badRequest(loginForm.errorsAsJson()); } Login loggingInUser = loginForm.get(); User user = User.findByEmailAndPassword(loggingInUser.email, loggingInUser.password); if(user == null) { return badRequest(buildJsonResponse("error", "Incorrect email or password")); } else { session().clear(); session("username", loggingInUser.email); ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "Logged in successfully"); msg.put("user", loggingInUser.email); wrapper.put("success", msg); return ok(wrapper); } } public static Result logout() { session().clear(); return ok(buildJsonResponse("success", "Logged out successfully")); } public static Result isAuthenticated() { if(session().get("username") == null) { return unauthorized(); } else { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "User is logged in already"); msg.put("user", session().get("username")); wrapper.put("success", msg); return ok(wrapper); } } public static class Login extends UserForm { @Constraints.Required public String password; }
Selanjutnya mari tambahkan kemampuan untuk mengizinkan panggilan back-end tertentu hanya untuk pengguna yang diautentikasi. Buat “Secured.java” dengan kode berikut:
public class Secured extends Security.Authenticator { @Override public String getUsername(Context ctx) { return ctx.session().get("username"); } @Override public Result onUnauthorized(Context ctx) { return unauthorized(); } }
Kami akan menggunakan kelas ini nanti untuk melindungi tindakan baru. Selanjutnya, kita harus men-tweak menu utama aplikasi AngularJS kita sehingga menampilkan link username dan logout. Untuk itu, kita perlu membuat pengontrol:
yo angular:controller menu
// menu.js angular.module('clientApp') .controller('MenuCtrl', function ($scope, $http, userService, $location) { $scope.user = userService; $scope.logout = function() { $http.get('/app/logout') .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = ''; $location.path('/login'); } }); }; $scope.$watch('user.username', function (newVal) { if(newVal === '') { $scope.isLoggedIn = false; } else { $scope.username = newVal; $scope.isLoggedIn = true; } }); });
Kami juga membutuhkan tampilan dan pengontrol untuk halaman login:
yo angular:controller login yo angular:view login
<!-- login.html --> <form name="loginForm" ng-submit="login()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Log in</button> </form>
// login.js angular.module('clientApp') .controller('LoginCtrl', function ($scope, userService, $location, $log, $http, alertService) { $scope.isAuthenticated = function() { if(userService.username) { $log.debug(userService.username); $location.path('/dashboard'); } else { $http.get('/app/isauthenticated') .error(function() { $location.path('/login'); }) .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); } }; $scope.isAuthenticated(); $scope.login = function() { var payload = { email : this.email, password : this.password }; $http.post('/app/login', payload) .error(function(data, status){ if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { alertService.add('danger', 'Invalid login or password!'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data){ $log.debug(data); if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); }; });
Selanjutnya kita tweak menu tersebut agar dapat menampilkan data user :
<!-- index.html --> <div class="collapse navbar-collapse" ng-controller="MenuCtrl"> <ul class="nav navbar-nav pull-right" ng-hide="isLoggedIn"> <li><a ng-href="/#/signup">Sign up!</a></li> <li><a ng-href="/#/login">Login</a></li> </ul> <div class="btn-group pull-right acc-button" ng-show="isLoggedIn"> <button type="button" class="btn btn-default">{{ username }}</button> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span> </button> <ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul> </div> </div>
Sekarang jika Anda masuk ke aplikasi, Anda seharusnya dapat melihat layar berikut:
Menambahkan Posting
Sekarang setelah kita memiliki mekanisme pendaftaran dan otentikasi dasar, kita dapat mulai mengimplementasikan fungsi pengeposan. Mari tambahkan tampilan dan pengontrol baru untuk menambahkan posting.
yo angular:view addpost
<!-- addpost.html --> <form name="postForm" ng-submit="post()" novalidate> <div> <label for="subject">Subject</label> <input name="subject" class="form-control" type="subject" placeholder="Subject" ng-model="subject"> </div> <div> <label for="content">Post</label> <textarea name="content" class="form-control" placeholder="Content" ng-model="content"></textarea> </div> <button type="submit" class="btn btn-primary">Submit post</button> </form>
yo angular:controller addpost
// addpost.js angular.module('clientApp') .controller('AddpostCtrl', function ($scope, $http, alertService, $location) { $scope.post = function() { var payload = { subject : $scope.subject, content: $scope.content }; $http.post('/app/post', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'subject' || key === 'content') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { $scope.subject = ''; $scope.content = ''; alertService.add('success', data.success.message); }); }; });
Kemudian kami memperbarui "app.js" untuk menyertakan:
.when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })
Selanjutnya, kita modifikasi “index.html” untuk menambahkan link untuk tampilan “addpost” kita di menu dashboard:
<ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li><a ng-href="/#/addpost">Add post</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul>
Sekarang di sisi aplikasi Play, mari buat Posting pengontrol baru dengan metode addPost:
// Post.java public class Post extends Controller { public static Result addPost() { Form<PostForm> postForm = Form.form(PostForm.class).bindFromRequest(); if (postForm.hasErrors()) { return badRequest(postForm.errorsAsJson()); } else { BlogPost newBlogPost = new BlogPost(); newBlogPost.commentCount = 0L; newBlogPost.subject = postForm.get().subject; newBlogPost.content = postForm.get().content; newBlogPost.user = getUser(); newBlogPost.save(); } return ok(Application.buildJsonResponse("success", "Post added successfully")); } private static User getUser() { return User.findByEmail(session().get("username")); } public static class PostForm { @Constraints.Required @Constraints.MaxLength(255) public String subject; @Constraints.Required public String content; } }
Tambahkan entri baru ke file rute untuk dapat menangani metode yang baru ditambahkan dalam perutean:
POST /app/post controllers.Post.addPost
Pada titik ini, Anda harus dapat menambahkan posting baru.
Menampilkan Posting
Menambahkan posting memiliki nilai kecil, jika kita tidak dapat menampilkannya. Yang ingin kami lakukan adalah membuat daftar semua posting di halaman utama. Kami mulai dengan menambahkan metode baru di pengontrol aplikasi kami:
// Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }
Dan mendaftarkannya di file rute kami:
GET /app/posts controllers.Application.getPosts
Selanjutnya, dalam aplikasi AngularJS kami, kami memodifikasi pengontrol utama kami:
// main.js angular.module('clientApp') .controller('MainCtrl', function ($scope, $http) { $scope.getPosts = function() { $http.get('app/posts') .success(function(data) { $scope.posts = data; }); }; $scope.getPosts(); });
Terakhir, hapus semuanya dari "main.html" dan tambahkan ini:
<div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | <a ng-href="/#/viewpost/{{ post.id }}">Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>
Sekarang jika Anda memuat halaman beranda aplikasi Anda, Anda akan melihat sesuatu yang mirip dengan ini:
Kami juga mungkin harus memiliki tampilan terpisah untuk masing-masing pos.
yo angular:controller viewpost yo angular:view viewpost
// viewpost.js angular.module('clientApp') .controller('ViewpostCtrl', function ($scope, $http, alertService, userService, $location) { $scope.user = userService; $scope.params = $routeParams; $scope.postId = $scope.params.postId; $scope.viewPost = function() { $http.get('/app/post/' + $scope.postId) .error(function(data) { alertService.add('danger', data.error.message); }) .success(function(data) { $scope.post = data; }); }; $scope.viewPost(); });
<!-- viewpost.html --> <div class="panel panel-default" ng-show="post"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>
Dan rute AngularJS:
app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })
Seperti sebelumnya, kami menambahkan metode baru ke pengontrol aplikasi kami:
// Application.java public static Result getPost(Long id) { BlogPost blogPost = BlogPost.findBlogPostById(id); if(blogPost == null) { return notFound(buildJsonResponse("error", "Post not found")); } return ok(Json.toJson(blogPost)); }
… Dan rute baru:
GET /app/post/:id controllers.Application.getPost(id: Long)
Sekarang jika Anda menavigasi ke http://localhost:9000/#/viewpost/1, Anda akan dapat memuat tampilan untuk posting tertentu. Selanjutnya, mari tambahkan kemampuan untuk melihat postingan pengguna di dasbor:
// dashboard.js angular.module('clientApp') .controller('DashboardCtrl', function ($scope, $log, $http, alertService, $location) { $scope.loadPosts = function() { $http.get('/app/userposts') .error(function(data, status) { if(status === 401) { $location.path('/login'); } else { alertService.add('danger', data.error.message); } }) .success(function(data) { $scope.posts = data; }); }; $scope.loadPosts(); });
<!-- dashboard.html --> <h4>My Posts</h4> <div ng-hide="posts.length">No posts yet. <a ng-href="/#/addpost">Add a post</a></div> <div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <a ng-href="/#/viewpost/{{ post.id }}">{{ post.subject }}</a> | Comments <span class="badge">{{ post.commentCount }}</span> </div> </div>
Tambahkan juga metode baru ke pengontrol Post, diikuti dengan rute yang sesuai dengan metode ini:
// Post.java public static Result getUserPosts() { User user = getUser(); if(user == null) { return badRequest(Application.buildJsonResponse("error", "No such user")); } return ok(Json.toJson(BlogPost.findBlogPostsByUser(user))); }
GET /app/userposts controllers.Post.getUserPosts
Sekarang ketika Anda membuat posting, mereka akan terdaftar di dasbor:
Mengomentari Fungsionalitas
Untuk mengimplementasikan fungsionalitas komentar, kita akan mulai dengan menambahkan metode baru di pengontrol Post:
// Post.java public static Result addComment() { Form<CommentForm> commentForm = Form.form(CommentForm.class).bindFromRequest(); if (commentForm.hasErrors()) { return badRequest(commentForm.errorsAsJson()); } else { PostComment newComment = new PostComment(); BlogPost blogPost = BlogPost.findBlogPostById(commentForm.get().postId); blogPost.commentCount++; blogPost.save(); newComment.blogPost = blogPost; newComment.user = getUser(); newComment.content = commentForm.get().comment; newComment.save(); return ok(Application.buildJsonResponse("success", "Comment added successfully")); } } public static class CommentForm { @Constraints.Required public Long postId; @Constraints.Required public String comment; }
And as always, we need to register a new route for this method:
POST /app/comment controllers.Post.addComment
In our AngularJS application, we add the following to “viewpost.js”:
$scope.addComment = function() { var payload = { postId: $scope.postId, comment: $scope.comment }; $http.post('/app/comment', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'comment') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { alertService.add('success', data.success.message); $scope.comment = ''; $scope.viewPost(); }); };
And finally add the following lines to “viewpost.html”:
<div class="well" ng-repeat="comment in post.comments"> <span class="label label-default">By: {{ comment.user.email }}</span> <br/> {{ comment.content }} </div> <div ng-hide="user.username || !post"><h4><a ng-href="/#/login">Login</a> to comment</h4></div> <form name="addCommentForm" ng-submit="addComment()" novalidate ng-show="user.username"> <div><h4>Add comment</h4></div> <div> <label for="comment">Comment</label> <textarea name="comment" class="form-control" placeholder="Comment" ng-model="comment"></textarea> </div> <button type="submit" class="btn btn-primary">Add comment</button> </form>
Now if you open any post, you will be able to add and view comments.
What's Next?
In this tutorial, we have built an AngularJS blog with a Play application serving as a REST API back-end. Although the application lacks robust data validation (especially on the client side) and security, these topics were out of the scope of this tutorial. It was aiming to demonstrate one of the many possible ways of building an application of this kind. For convenience, the source code of this application has been uploaded to a GitHub repository.
If you find this combination of AngularJS and Play in web application development interesting, I highly recommend you review the following topics further:
- AngularJS documentation
- Play documentation
- Recommended security approach in one page JS app with Play as back-end (contains example)
- Secure REST API without OAuth
- Ready Play authentication plug-in (might be not fully usable for single-page JavaScript applications, but can be used as a good example)