├── CONTRIBUTING.md ├── public ├── app.css └── mix-manifest.json ├── resources ├── sass │ ├── app.scss │ └── base.scss ├── js │ ├── event-bus.js │ ├── main.styl │ ├── models │ │ ├── Field.js │ │ ├── Submission.js │ │ ├── Model.js │ │ └── Form.js │ ├── pages │ │ ├── dashboard.vue │ │ ├── fields │ │ │ └── index.vue │ │ └── forms │ │ │ ├── index.vue │ │ │ ├── fields │ │ │ └── create.vue │ │ │ └── edit.vue │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ └── forms.js │ ├── components │ │ ├── FormActionsComponent.vue │ │ ├── FieldsComponent.vue │ │ ├── NavComponent.vue │ │ ├── DialogComponent.vue │ │ ├── Alert.vue │ │ ├── FormsComponent.vue │ │ ├── FormSubmissionsComponent.vue │ │ └── FormFieldsComponent.vue │ ├── routes.js │ ├── app.js │ └── base.js └── views │ ├── submissions │ └── edit.blade.php │ ├── forms │ └── _form-questions.blade.php │ ├── layouts │ └── front.blade.php │ └── layout.blade.php ├── screenshots ├── new_field.png ├── new_form.png ├── form_details.png ├── forms_list.png ├── field_details.png ├── front_end_form.png └── submission_details.png ├── src ├── Fields │ ├── Date.php │ ├── Email.php │ ├── File.php │ ├── Text.php │ ├── Number.php │ ├── Password.php │ ├── TextArea.php │ ├── CheckBox.php │ ├── Radio.php │ ├── Select.php │ └── FormField.php ├── User.php ├── Http │ ├── Controllers │ │ ├── SubmissionController.php │ │ ├── DashboardController.php │ │ ├── FieldController.php │ │ ├── Controller.php │ │ ├── FormController.php │ │ ├── FormSubmissionController.php │ │ └── FormFieldController.php │ ├── Requests │ │ ├── ListFormRequest.php │ │ ├── DeleteFormRequest.php │ │ ├── CreateFormRequest.php │ │ ├── UpdateFormRequest.php │ │ ├── CreateFormQuestionRequest.php │ │ ├── UpdateFormQuestionRequest.php │ │ └── CreateFormSubmissionRequest.php │ └── routes.php ├── Services │ ├── SubmissionService.php │ ├── AnswerService.php │ ├── FormService.php │ └── QuestionService.php ├── Facades │ └── FormFacade.php ├── Transformers │ ├── FieldTypeTransformer.php │ ├── AnswerTransformer.php │ ├── SubmissionTransformer.php │ ├── FormTransformer.php │ ├── Transformer.php │ └── FieldTransformer.php ├── Models │ ├── Answer.php │ ├── Submission.php │ ├── Form.php │ └── Question.php ├── Rules │ └── ReCaptcha.php ├── Form.php └── FormServiceProvider.php ├── .travis.yml ├── tests ├── Feature │ ├── Form │ │ ├── DeleteTest.php │ │ ├── CreateTest.php │ │ ├── UpdateTest.php │ │ └── ListTest.php │ └── Question │ │ ├── DeleteTest.php │ │ ├── UpdateTest.php │ │ └── CreateTest.php ├── Unit │ ├── SubmissionTest.php │ ├── FormTest.php │ ├── AnswerTest.php │ ├── QuestionTest.php │ └── Fields │ │ └── FieldsRenderTest.php ├── TestCase.php └── BaseTestCase.php ├── .gitignore ├── phpunit.xml.dist ├── composer.json ├── webpack.mix.js ├── README.md ├── package.json ├── config └── laravel_forms.php └── database ├── seeds └── FormsDatabaseSeeder.php ├── factories └── ModelFactory.php └── migrations └── create_form_tables.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/sass/base.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/js/event-bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | export const EventBus = new Vue(); -------------------------------------------------------------------------------- /resources/js/main.styl: -------------------------------------------------------------------------------- 1 | @import '~vuetify/src/stylus/main' // Ensure you are using stylus-loader -------------------------------------------------------------------------------- /screenshots/new_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musonza/laravel-forms/HEAD/screenshots/new_field.png -------------------------------------------------------------------------------- /screenshots/new_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musonza/laravel-forms/HEAD/screenshots/new_form.png -------------------------------------------------------------------------------- /screenshots/form_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musonza/laravel-forms/HEAD/screenshots/form_details.png -------------------------------------------------------------------------------- /screenshots/forms_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musonza/laravel-forms/HEAD/screenshots/forms_list.png -------------------------------------------------------------------------------- /screenshots/field_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musonza/laravel-forms/HEAD/screenshots/field_details.png -------------------------------------------------------------------------------- /screenshots/front_end_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musonza/laravel-forms/HEAD/screenshots/front_end_form.png -------------------------------------------------------------------------------- /screenshots/submission_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musonza/laravel-forms/HEAD/screenshots/submission_details.png -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=080f3fb6c90b157f5680", 3 | "/app.css": "/app.css?id=d41d8cd98f00b204e980" 4 | } 5 | -------------------------------------------------------------------------------- /src/Fields/Date.php: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | name: "Dashboard" 4 | }; 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/User.php: -------------------------------------------------------------------------------- 1 | 2 | import FieldsComponent from "@/components/FieldsComponent"; 3 | export default { 4 | name: "FormsPage", 5 | 6 | components: { 7 | FieldsComponent 8 | } 9 | }; 10 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /resources/js/pages/forms/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /resources/js/components/FormActionsComponent.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /resources/js/models/Model.js: -------------------------------------------------------------------------------- 1 | import { Model as BaseModel } from 'vue-api-query' 2 | 3 | export default class Model extends BaseModel { 4 | 5 | // define a base url for a REST API 6 | baseURL() { 7 | return ''; 8 | } 9 | 10 | // implement a default request method 11 | request(config) { 12 | return this.$http.request(config) 13 | } 14 | } -------------------------------------------------------------------------------- /src/Http/Controllers/SubmissionController.php: -------------------------------------------------------------------------------- 1 | delete(); 12 | 13 | return response('', 201); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /resources/js/models/Form.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | import Field from './Field'; 3 | import Submission from './Submission'; 4 | 5 | export default class Form extends Model { 6 | resource() { 7 | return 'forms'; 8 | } 9 | 10 | fields() { 11 | return this.hasMany(Field); 12 | } 13 | 14 | submissions() { 15 | return this.hasMany(Submission); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Services/SubmissionService.php: -------------------------------------------------------------------------------- 1 | submission = $submission; 12 | } 13 | 14 | public function getById($id) 15 | { 16 | return $this->submission->findOrFail($id); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Facades/FormFacade.php: -------------------------------------------------------------------------------- 1 | attributes['class'] = "form-control"; 13 | $attributes = $this->attributes($this->attributes); 14 | return "" . e($value) . ""; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Services/AnswerService.php: -------------------------------------------------------------------------------- 1 | form = $form; 13 | $this->answer = $answer; 14 | } 15 | 16 | public function getById($id) 17 | { 18 | return $this->answer->findOrFail($id); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Services/FormService.php: -------------------------------------------------------------------------------- 1 | form = $form; 12 | } 13 | 14 | public function create(array $data) 15 | { 16 | return $this->form->create($data); 17 | } 18 | 19 | public function getById($id) 20 | { 21 | return $this->form->findOrFail($id); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Controllers/DashboardController.php: -------------------------------------------------------------------------------- 1 | $cssFile, 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Transformers/FieldTypeTransformer.php: -------------------------------------------------------------------------------- 1 | $field, 13 | 'title' => $this->title($field), 14 | 'has_choices' => $fieldObj->hasChoices(), 15 | ]; 16 | } 17 | 18 | protected function title($field) 19 | { 20 | return substr($field, strrpos($field, '\\') + 1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Services/QuestionService.php: -------------------------------------------------------------------------------- 1 | form = $form; 13 | $this->question = $question; 14 | } 15 | 16 | public function create(array $data) 17 | { 18 | return $this->question->create($data); 19 | } 20 | 21 | public function getById($id) 22 | { 23 | return $this->question->findOrFail($id); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Requests/ListFormRequest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 19 | } 20 | 21 | public function testDeleteSuccess() 22 | { 23 | $response = $this 24 | ->deleteJson(route('forms.destroy', $this->form->id)) 25 | ->assertStatus(201); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Http/Controllers/FieldController.php: -------------------------------------------------------------------------------- 1 | fieldTransformer = $fieldTransformer; 17 | } 18 | 19 | public function index() 20 | { 21 | $fields = $this->fieldTransformer->transformCollection(config('laravel_forms.fields')); 22 | 23 | return response($fields); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /resources/js/routes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { path: '/', redirect: '/forms' }, 3 | { 4 | path: '/dashboard', 5 | name: 'dashboard', 6 | component: require('./pages/dashboard') 7 | }, 8 | { 9 | path: '/forms', 10 | name: 'forms-index', 11 | component: require('./pages/forms/index') 12 | }, 13 | { 14 | path: '/forms/:id', 15 | name: 'forms-edit', 16 | component: require('./pages/forms/edit') 17 | }, 18 | { 19 | path: '/fields', 20 | name: 'fields-index', 21 | component: require('./pages/fields/index') 22 | }, 23 | { 24 | path: '/forms/:id/fields/create', 25 | name: 'formFieldCreate', 26 | component: require('./pages/forms/fields/create') 27 | }, 28 | ]; -------------------------------------------------------------------------------- /src/Transformers/AnswerTransformer.php: -------------------------------------------------------------------------------- 1 | toArray(), 11 | [ 12 | 'response' => $this->getResponse($answer), 13 | 'question' => $answer->question, 14 | ] 15 | ); 16 | } 17 | 18 | public function getResponse($answer) 19 | { 20 | if ($answer->question->options && isset($answer->question->options[$answer->value])) { 21 | return $answer->question->options[$answer->value]; 22 | } 23 | 24 | return $answer->value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Fields/CheckBox.php: -------------------------------------------------------------------------------- 1 | attributes['class'] = ""; 13 | $this->attributes['name'] = "{$this->attributes['name']}[]"; 14 | 15 | $html = ""; 16 | 17 | foreach ($this->options as $value => $label) { 18 | $html .= 'attributes($this->attributes) 19 | . ' value="' 20 | . $value 21 | . '"> ' 22 | . $label 23 | . '
'; 24 | } 25 | return $html; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders # 2 | ########### 3 | dist/ 4 | vendor/ 5 | node_modules/ 6 | CVS/ 7 | .idea 8 | deploy.php 9 | composer.lock 10 | # Compiled source # 11 | ################### 12 | *.com 13 | *.class 14 | *.dll 15 | *.exe 16 | *.o 17 | *.so 18 | 19 | # Packages # 20 | ############ 21 | # it's better to unpack these files and commit the raw source 22 | # git has its own built in compression methods 23 | *.7z 24 | *.dmg 25 | *.gz 26 | *.iso 27 | *.jar 28 | *.rar 29 | *.tar 30 | *.zip 31 | 32 | # Logs and databases # 33 | ###################### 34 | *.log 35 | *.sqlite 36 | 37 | # OS generated files # 38 | ###################### 39 | .DS_Store 40 | .DS_Store? 41 | ._* 42 | .Spotlight-V100 43 | .Trashes 44 | ehthumbs.db 45 | Thumbs.db 46 | 47 | docs/ 48 | phpDocumentor.phar -------------------------------------------------------------------------------- /src/Fields/Radio.php: -------------------------------------------------------------------------------- 1 | attributes['class'] = ""; 13 | $attributes = $this->attributes($this->attributes); 14 | 15 | $html = ""; 16 | 17 | foreach ($this->options as $value => $label) { 18 | $html .= ' ' 24 | . $label 25 | . '
'; 26 | } 27 | 28 | return $html; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Requests/CreateFormRequest.php: -------------------------------------------------------------------------------- 1 | 'Required', 11 | 'description' => '', 12 | 'status' => '', 13 | ]; 14 | 15 | /** 16 | * Determine if the user is authorized to make this request. 17 | * 18 | * @return bool 19 | */ 20 | public function authorize() 21 | { 22 | return true; 23 | } 24 | 25 | /** 26 | * Get the validation rules that apply to the request. 27 | * 28 | * @return array 29 | */ 30 | public function rules() 31 | { 32 | return self::$rules; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Requests/UpdateFormRequest.php: -------------------------------------------------------------------------------- 1 | 'Required', 11 | 'description' => '', 12 | 'status' => '', 13 | ]; 14 | 15 | /** 16 | * Determine if the user is authorized to make this request. 17 | * 18 | * @return bool 19 | */ 20 | public function authorize() 21 | { 22 | return true; 23 | } 24 | 25 | /** 26 | * Get the validation rules that apply to the request. 27 | * 28 | * @return array 29 | */ 30 | public function rules() 31 | { 32 | return self::$rules; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | 'web', 7 | 'namespace' => 'Musonza\Form\Http\Controllers', 8 | ], function () { 9 | Route::resource('fields', 'FieldController'); 10 | Route::resource('forms', 'FormController'); 11 | Route::resource('forms.fields', 'FormFieldController'); 12 | Route::resource('forms.submissions', 'FormSubmissionController'); 13 | Route::resource('submissions', 'SubmissionController'); 14 | }); 15 | 16 | Route::group([ 17 | 'middleware' => 'web', 18 | 'namespace' => 'Musonza\Form\Http\Controllers', 19 | ], function () use ($dashboardPathPrefix) { 20 | Route::get($dashboardPathPrefix, 'DashboardController@index'); 21 | }); 22 | -------------------------------------------------------------------------------- /resources/views/submissions/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('laravel-forms::layouts.front') 2 | 3 | @section('content') 4 | 5 |

{{ $form['title'] }}

6 | 7 |
8 | {{ csrf_field() }} 9 | 10 | @include('laravel-forms::forms._form-questions') 11 | 12 | @if($googleRecaptchaEnabled) 13 |
14 |
16 |
17 |
18 | @endif 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | @endsection 28 | -------------------------------------------------------------------------------- /src/Fields/Select.php: -------------------------------------------------------------------------------- 1 | attributes($this->attributes); 15 | $html = ""; 16 | foreach ($this->options as $value => $label) { 17 | if ($value == $selected) { 18 | $selectedAttribute = ' selected="selected"'; 19 | } else { 20 | $selectedAttribute = ''; 21 | } 22 | $html .= ''; 23 | } 24 | return $html . ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Models/Answer.php: -------------------------------------------------------------------------------- 1 | belongsTo(Question::class)->orderBy('position'); 25 | } 26 | 27 | /** 28 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 29 | */ 30 | public function submission() 31 | { 32 | return $this->belongsTo(Submission::class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | { 9 | return state.forms; 10 | } 11 | }; 12 | 13 | const BASEURL = ''; //http://127.0.0.1:8000'; 14 | 15 | const actions = { 16 | loadForms({ commit }) { 17 | commit('LOADING', true); 18 | axios.get(BASEURL + '/forms') 19 | .then((response) => { 20 | commit('SET_FORMS', response.data); 21 | commit('LOADING', false); 22 | return response; 23 | }); 24 | }, 25 | }; 26 | 27 | const mutations = { 28 | ['LOADING'](state, payload) { 29 | state.loading = payload; 30 | }, 31 | ['SET_FORMS'](state, payload) { 32 | state.forms = payload; 33 | }, 34 | }; 35 | 36 | export default { 37 | state, 38 | getters, 39 | actions, 40 | mutations, 41 | }; -------------------------------------------------------------------------------- /tests/Feature/Form/CreateTest.php: -------------------------------------------------------------------------------- 1 | postJson(route('forms.store'), ['title' => 'Contact Form', 'description' => 'Our Form']); 13 | 14 | $response 15 | ->assertStatus(200) 16 | ->assertJson([ 17 | 'title' => 'Contact Form', 18 | 'description' => 'Our Form', 19 | ]); 20 | } 21 | 22 | public function testFormRequiresTitle() 23 | { 24 | $response = $this->postJson(route('forms.store'), []) 25 | ->assertStatus(422) 26 | ->assertJson([ 27 | 'message' => 'The given data was invalid.', 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Requests/CreateFormQuestionRequest.php: -------------------------------------------------------------------------------- 1 | 'Required', 11 | 'field_type' => 'Required', 12 | 'help_text' => '', 13 | 'placeholder' => '', 14 | 'value' => '', 15 | 'columns_count' => '', 16 | ]; 17 | 18 | /** 19 | * Determine if the user is authorized to make this request. 20 | * 21 | * @return bool 22 | */ 23 | public function authorize() 24 | { 25 | return true; 26 | } 27 | 28 | /** 29 | * Get the validation rules that apply to the request. 30 | * 31 | * @return array 32 | */ 33 | public function rules() 34 | { 35 | return self::$rules; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Requests/UpdateFormQuestionRequest.php: -------------------------------------------------------------------------------- 1 | 'Required', 11 | 'field_type' => 'Required', 12 | 'is_required' => '', 13 | 'help_text' => '', 14 | 'placeholder' => '', 15 | 'default_value' => '', 16 | //'columns_count' => '', 17 | ]; 18 | 19 | /** 20 | * Determine if the user is authorized to make this request. 21 | * 22 | * @return bool 23 | */ 24 | public function authorize() 25 | { 26 | return true; 27 | } 28 | 29 | /** 30 | * Get the validation rules that apply to the request. 31 | * 32 | * @return array 33 | */ 34 | public function rules() 35 | { 36 | return self::$rules; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Feature/Form/UpdateTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 19 | } 20 | 21 | public function testUpdateSuccess() 22 | { 23 | $response = $this->putJson(route('forms.update', $this->form->id), [ 24 | 'title' => 'Contact Form2', 25 | 'label' => 'Contact Form2', 26 | 'description' => 'Our Form2', 27 | ]); 28 | 29 | $response 30 | ->assertStatus(200) 31 | ->assertJson([ 32 | 'title' => 'Contact Form2', 33 | 'description' => 'Our Form2', 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resources/views/forms/_form-questions.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $count = 0; 3 | @endphp 4 | 5 |
6 | 7 | @if ($errors->any()) 8 |
    9 | @foreach ($errors->all() as $error) 10 |
  • {{ $error }}
  • 11 | @endforeach 12 |
13 | @endif 14 | 15 | @foreach($form['questions']['data'] as $question) 16 | 17 | @php 18 | $count += $question['columns_count']; 19 | @endphp 20 |
21 | 24 |
25 | {!! $question['render'] !!} 26 | @if(isset($question['help_text'])) 27 | {{ $question['help_text'] }} 28 | @endif 29 |
30 |
31 | 32 | @if($count == 12) 33 |
34 |
35 | @endif 36 | @endforeach 37 | -------------------------------------------------------------------------------- /tests/Feature/Question/DeleteTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 18 | $this->question = Form::createQuestion( 19 | [ 20 | 'label' => 'First Name', 21 | 'description' => 'Description', 22 | 'field_type' => Text::class, 23 | 'form_id' => $this->form->id, 24 | ] 25 | ); 26 | } 27 | 28 | public function testDeleteSuccess() 29 | { 30 | $response = $this 31 | ->deleteJson(route('forms.fields.destroy', [$this->form->id, $this->question->id])) 32 | ->assertStatus(201); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/SubmissionTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 20 | $this->submission = $this->form->addSubmission([]); 21 | } 22 | 23 | public function testFormSubmissions() 24 | { 25 | $this->assertInstanceOf(Collection::class, $this->form->submissions); 26 | } 27 | 28 | public function testGetSubmissionById() 29 | { 30 | $submission = Form::submissionService()->getById(1); 31 | $this->assertEquals($this->submission->id, $submission->id); 32 | $this->assertInstanceOf(Submission::class, $submission); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Transformers/SubmissionTransformer.php: -------------------------------------------------------------------------------- 1 | $submission->id, 16 | 'form_id' => $submission->form_id, 17 | 'ip_address' => $submission->ip_address, 18 | 'response' => $submission->response, 19 | 'is_complete' => $submission->is_complete, 20 | 'created_at_readable' => $submission->created_at->diffForHumans(), 21 | 'created_at' => $submission->created_at, 22 | 'updated_at' => $submission->updated_at, 23 | ]; 24 | } 25 | 26 | public function includeAnswers($submission) 27 | { 28 | return $this->collection($submission->answers, new AnswerTransformer()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Models/Submission.php: -------------------------------------------------------------------------------- 1 | 'array', 24 | 'is_complete' => 'boolean', 25 | ]; 26 | 27 | /** 28 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 29 | */ 30 | public function form() 31 | { 32 | return $this->belongsTo(Form::class); 33 | } 34 | 35 | /** 36 | * Submission has answers. 37 | * 38 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 39 | */ 40 | public function answers() 41 | { 42 | return $this->hasMany(Answer::class); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Feature 15 | 16 | 17 | ./tests/Unit 18 | 19 | 20 | 21 | 22 | src/ 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Models/Form.php: -------------------------------------------------------------------------------- 1 | hasMany(Question::class)->orderBy('position'); 28 | } 29 | 30 | /** 31 | * A form has many submissions. 32 | * 33 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 34 | */ 35 | public function submissions() 36 | { 37 | return $this->hasMany(Submission::class); 38 | } 39 | 40 | public function addSubmission($submission) 41 | { 42 | return $this->submissions()->create($submission); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Rules/ReCaptcha.php: -------------------------------------------------------------------------------- 1 | post( 16 | 'https://www.google.com/recaptcha/api/siteverify', 17 | ['form_params' => 18 | [ 19 | 'secret' => config('laravel_forms.google_recaptcha_secret'), 20 | 'response' => $value, 21 | 'remoteip' => $_SERVER['REMOTE_ADDR'], 22 | ], 23 | ] 24 | ); 25 | 26 | $body = json_decode((string) $response->getBody()); 27 | 28 | return $body->success; 29 | } 30 | 31 | /** 32 | * Get the validation error message. 33 | * 34 | * @return string 35 | */ 36 | public function message() 37 | { 38 | return 'Please ensure that you are a human!'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/js/components/FieldsComponent.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | 40 | -------------------------------------------------------------------------------- /src/Transformers/FormTransformer.php: -------------------------------------------------------------------------------- 1 | $form->id, 16 | 'title' => $form->title, 17 | 'description' => $form->description, 18 | 'created_at' => $form->created_at, 19 | 'status' => [ 20 | 'value' => (int)$form->status, 21 | 'label' => $statuses[$form->status]['label'], 22 | 'class' => $statuses[$form->status]['class'], 23 | ], 24 | 'statuses' => $statuses, 25 | 'submissions_count' => $form->submissions->count(), 26 | ]; 27 | } 28 | 29 | public function includeQuestions($form) 30 | { 31 | return $this->collection($form->questions, new FieldTransformer()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Feature/Form/ListTest.php: -------------------------------------------------------------------------------- 1 | 'Contact Form1']); 14 | Form::create(['title' => 'Contact Form2']); 15 | Form::create(['title' => 'Contact Form3']); 16 | } 17 | 18 | public function testListForms() 19 | { 20 | // $response = $this->get(route('forms.index')); 21 | 22 | // $response->dump(); 23 | 24 | $response = $this->getJson(route('forms.index')); 25 | 26 | $response 27 | ->assertStatus(200) 28 | ->assertJsonCount(3, 'data'); 29 | } 30 | 31 | public function testGetForm() 32 | { 33 | $response = $this->getJson(route('forms.show', $id = 2)); 34 | 35 | $response 36 | ->assertStatus(200) 37 | ->assertJson([ 38 | 'title' => 'Contact Form2', 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /resources/js/components/NavComponent.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | -------------------------------------------------------------------------------- /tests/Feature/Question/UpdateTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 18 | $this->data = [ 19 | 'label' => 'First Name', 20 | 'description' => 'Description', 21 | 'field_type' => Text::class, 22 | 'form_id' => $this->form->id, 23 | ]; 24 | $this->question = Form::createQuestion($this->data); 25 | } 26 | 27 | public function testUpdateSuccess() 28 | { 29 | $this->data['label'] = 'First Name Updated'; 30 | 31 | $response = $this 32 | ->putJson(route('forms.fields.update', [$this->form->id, $this->question->id]), $this->data) 33 | ->assertStatus(200) 34 | ->assertJson([ 35 | 'label' => 'First Name Updated', 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "musonza/laravel-forms", 3 | "authors": [ 4 | { 5 | "name": "Tinashe Musonza", 6 | "email": "tinashemusonza@gmail.com", 7 | "role": "Developer" 8 | } 9 | ], 10 | "require": { 11 | "league/fractal": "^0.17.0", 12 | "laravel/framework": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", 13 | "guzzlehttp/guzzle": "~6.0" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^5.7|6.2|^7.0", 17 | "orchestra/testbench": "~3.3.0|~3.4.2|^3.5.0|~3.7.0", 18 | "orchestra/database": "~3.3.0|~3.4.2|^3.5.0|~3.7.0", 19 | "mockery/mockery": "^1.0.0", 20 | "spatie/phpunit-watcher": "dev-master", 21 | "orchestra/testbench-dusk": "^3.7@dev" 22 | }, 23 | "minimum-stability": "dev", 24 | "autoload": { 25 | "psr-4": { 26 | "Musonza\\Form\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Musonza\\Form\\Tests\\": "tests" 32 | } 33 | }, 34 | "scripts": { 35 | "test": "phpunit" 36 | }, 37 | "extra": { 38 | "laravel": { 39 | "providers": [ 40 | "Musonza\\Form\\FormServiceProvider" 41 | ] 42 | } 43 | }, 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unit/FormTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 19 | } 20 | 21 | public function testCreatesForm() 22 | { 23 | $this->assertDatabaseHas($this->tablePrefix . 'forms', ['id' => 1, 'title' => 'Contact Form']); 24 | } 25 | 26 | public function testGetFormById() 27 | { 28 | $form = Form::formService()->getById($this->form->id); 29 | $this->assertEquals($this->form->id, $form->id); 30 | } 31 | 32 | public function testDeleteForm() 33 | { 34 | $this->assertDatabaseHas($this->tablePrefix . 'forms', ['id' => 1, 'title' => 'Contact Form']); 35 | 36 | Form::formService() 37 | ->getById($this->form->id) 38 | ->delete(); 39 | 40 | $this->assertDatabaseMissing($this->tablePrefix . 'forms', ['id' => 1, 'title' => 'Contact Form']); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | artisan('migrate', ['--database' => 'testbench']); 16 | $this->withFactories(__DIR__ . '/../database/factories'); 17 | $this->migrate(); 18 | $this->users = $this->createUsers(6); 19 | } 20 | 21 | /** 22 | * Define environment setup. 23 | * 24 | * @param \Illuminate\Foundation\Application $app 25 | * 26 | * @return void 27 | */ 28 | protected function getEnvironmentSetUp($app) 29 | { 30 | parent::getEnvironmentSetUp($app); 31 | 32 | // Setup default database to use sqlite :memory: 33 | $app['config']->set('database.default', 'testbench'); 34 | $app['config']->set('database.connections.testbench', [ 35 | 'driver' => 'sqlite', 36 | 'database' => ':memory:', 37 | 'prefix' => '', 38 | ]); 39 | $app['config']->set('app.debug', true); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | const webpack = require('webpack'); 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Mix Asset Management 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Mix provides a clean, fluent API for defining some Webpack build steps 10 | | for your Laravel application. By default, we are compiling the Sass 11 | | file for the application as well as bundling up all the JS files. 12 | | 13 | */ 14 | 15 | mix 16 | .options({ 17 | uglify: { 18 | uglifyOptions: { 19 | compress: { 20 | drop_console: true, 21 | } 22 | } 23 | } 24 | }) 25 | .setPublicPath('public') 26 | .js('resources/js/app.js', 'public') 27 | .sass('resources/sass/app.scss', 'public') 28 | .version() 29 | .webpackConfig({ 30 | resolve: { 31 | symlinks: false, 32 | alias: { 33 | '@': path.resolve(__dirname, 'resources/js/'), 34 | } 35 | }, 36 | plugins: [ 37 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) 38 | ], 39 | }); 40 | -------------------------------------------------------------------------------- /resources/views/layouts/front.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Forms 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | @foreach (['danger', 'warning', 'success', 'info'] as $msg) 19 | @if(Session::has('alert-' . $msg)) 20 |

{{ Session::get('alert-' . $msg) }}

21 | @endif 22 | @endforeach 23 |
24 |
25 |
26 |
27 | @yield('content') 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Http/Requests/CreateFormSubmissionRequest.php: -------------------------------------------------------------------------------- 1 | [ 33 | $form->googleRecaptchaEnabled() ? 'required' : '', 34 | new ReCaptcha, 35 | ], 36 | ]; 37 | } 38 | 39 | /** 40 | * Get the error messages for the defined validation rules. 41 | * 42 | * @return array 43 | */ 44 | public function messages() 45 | { 46 | return [ 47 | 'g-recaptcha-response.required' => 'Captcha response is required.', 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Base from './base'; 3 | import axios from 'axios'; 4 | import Routes from './routes'; 5 | import VueRouter from 'vue-router'; 6 | import TreeView from 'vue-json-tree-view'; 7 | import store from './store/index'; 8 | import Vuetify from 'vuetify'; 9 | import 'vuetify/dist/vuetify.min.css'; 10 | import { Model } from 'vue-api-query'; 11 | import VeeValidate from 'vee-validate' 12 | 13 | // inject global axios instance as http client to Model 14 | Model.$http = axios 15 | require('bootstrap'); 16 | let token = document.head.querySelector('meta[name="csrf-token"]'); 17 | if (token) { 18 | axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 19 | } 20 | 21 | Vue.use(Vuetify); 22 | Vue.use(VueRouter); 23 | Vue.use(TreeView); 24 | Vue.use(VeeValidate); 25 | 26 | Vue.component('nav-component', require('./components/NavComponent.vue')); 27 | Vue.component('alert', require('./components/Alert.vue')); 28 | 29 | const router = new VueRouter({ 30 | routes: Routes, 31 | // mode: 'history', 32 | base: '/laravel-forms/', 33 | }); 34 | 35 | Vue.mixin(Base); 36 | 37 | new Vue({ 38 | el: '#laravel-forms', 39 | router, 40 | store, 41 | // render: (h) => h(App), 42 | data() { 43 | return { 44 | alert: { 45 | type: null, 46 | show: false, 47 | autoDismiss: 5000, 48 | message: '', 49 | title: '', 50 | confirmationAgree: null, 51 | }, 52 | } 53 | }, 54 | }); -------------------------------------------------------------------------------- /src/Transformers/Transformer.php: -------------------------------------------------------------------------------- 1 | setPaginator(new IlluminatePaginatorAdapter($paginator)); 20 | } 21 | 22 | if ($meta) { 23 | $resource->setMeta($meta); 24 | } 25 | 26 | return $this->fractalManager($resource); 27 | } 28 | 29 | public function fractalManager($resource) 30 | { 31 | $fractal = new Manager(); 32 | 33 | $fractal->setSerializer(new ArraySerializer()); 34 | 35 | if ($includes = request('include', null)) { 36 | $fractal->parseIncludes($includes); 37 | } 38 | 39 | return $fractal->createData($resource)->toArray(); 40 | } 41 | 42 | public function transformItem($item) 43 | { 44 | $resource = new Item($item, $this); 45 | 46 | return $this->fractalManager($resource); 47 | } 48 | 49 | abstract public function transform($item); 50 | } 51 | -------------------------------------------------------------------------------- /resources/views/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Laravel Forms 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /resources/js/components/DialogComponent.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 71 | -------------------------------------------------------------------------------- /src/Form.php: -------------------------------------------------------------------------------- 1 | formService = $formService; 21 | $this->questionService = $questionService; 22 | $this->answerService = $answerService; 23 | $this->submissionService = $submissionService; 24 | } 25 | 26 | public function create(array $data) 27 | { 28 | return $this->formService->create($data); 29 | } 30 | 31 | public function formService() 32 | { 33 | return $this->formService; 34 | } 35 | 36 | public function createQuestion(array $data) 37 | { 38 | return $this->questionService->create($data); 39 | } 40 | 41 | public function questionService() 42 | { 43 | return $this->questionService; 44 | } 45 | 46 | public function answerService() 47 | { 48 | return $this->answerService; 49 | } 50 | 51 | public function submissionService() 52 | { 53 | return $this->submissionService; 54 | } 55 | 56 | public function googleRecaptchaEnabled() 57 | { 58 | return config('laravel_forms.google_recaptcha_enabled'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Forms 2 | 3 | [![Build Status](https://travis-ci.org/musonza/laravel-forms.svg?branch=master)](https://travis-ci.org/musonza/laravel-forms) 4 | [![Packagist](https://img.shields.io/packagist/v/musonza/laravel-forms.svg)](https://packagist.org/packages/musonza/laravel-forms) 5 | 6 | forms list 7 | 8 | ## Installation 9 | 1. Install composer package 10 | ```sh 11 | composer require musonza/laravel-forms 12 | ``` 13 | 14 | 1. Publish Assets 15 | ```sh 16 | php artisan vendor:publish 17 | ``` 18 | 1. Add Form facade to `config/app.php` 19 | ```php 20 | 'Form' => Musonza\Form\Facades\FormFacade::class, 21 | ``` 22 | 23 | 1. Run migrations 24 | ```sh 25 | php artisan migrate 26 | ``` 27 | 28 | 1. Check the published file config/laravel_forms.php 29 | - You can enable / disable captcha 30 | - You can configure the path for your forms dashboard 31 | - You can add custom field types 32 | 33 | 1. Access dashboard at 34 | 35 | http//your-url.com/laravel-forms (you can change the path in config/laravel_forms.php) 36 | 37 | ## Adding a Form 38 | adding a form 39 | 40 | ## Form details 41 | form details 42 | 43 | ## Adding a Field 44 | adding a field 45 | 46 | ## Field details 47 | field details 48 | 49 | ## Sample Form Output 50 | form output 51 | 52 | ## Sample Submission 53 | 54 | 55 | ## TODO 56 | - Multi page forms 57 | 58 | ## Credits 59 | https://github.com/laravel/telescope for some of the front-end structuring 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "@vue/cli-plugin-babel": "^3.1.1", 14 | "@vue/cli-plugin-eslint": "^3.1.5", 15 | "@vue/cli-service": "^3.1.4", 16 | "axios": "^0.18", 17 | "babel-eslint": "^10.0.1", 18 | "bootstrap": "^4.0.0", 19 | "cross-env": "^5.1", 20 | "eslint": "^5.8.0", 21 | "eslint-plugin-vue": "^5.0.0-beta.5", 22 | "highlight.js": "^9.12.0", 23 | "jquery": "^3.2", 24 | "laravel-mix": "^2.0", 25 | "lodash": "^4.17.4", 26 | "moment": "^2.10.6", 27 | "moment-timezone": "^0.5.21", 28 | "popper.js": "^1.14.5", 29 | "sql-formatter": "^2.3.1", 30 | "stylus": "^0.54.5", 31 | "stylus-loader": "^3.0.1", 32 | "vue": "^2.5.7", 33 | "vue-cli-plugin-vuetify": "^0.4.6", 34 | "vue-json-tree-view": "^2.1.4", 35 | "vue-router": "^3.0.2", 36 | "vue-template-compiler": "^2.5.17", 37 | "vuetify-loader": "^1.0.7" 38 | }, 39 | "dependencies": { 40 | "api-class": "0.0.2", 41 | "md5": "^2.2.1", 42 | "sortablejs": "^1.7.0", 43 | "vee-validate": "^2.1.3", 44 | "vue-api-query": "^1.2.0", 45 | "vuedraggable": "^2.16.0", 46 | "vuetify": "^1.3.9", 47 | "vuex": "^3.0.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /config/laravel_forms.php: -------------------------------------------------------------------------------- 1 | 'laravel-forms', 5 | 'dashboard_css_file' => 'app.css', 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Field Types 9 | |-------------------------------------------------------------------------- 10 | | 11 | | Here you may define add additional field types for use in your application. 12 | | 13 | */ 14 | 'fields' => [ 15 | Musonza\Form\Fields\CheckBox::class, 16 | Musonza\Form\Fields\Date::class, 17 | Musonza\Form\Fields\File::class, 18 | Musonza\Form\Fields\Password::class, 19 | Musonza\Form\Fields\Radio::class, 20 | Musonza\Form\Fields\Select::class, 21 | Musonza\Form\Fields\Text::class, 22 | Musonza\Form\Fields\TextArea::class, 23 | Musonza\Form\Fields\Email::class, 24 | Musonza\Form\Fields\Number::class, 25 | ], 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Form Statuses 29 | |-------------------------------------------------------------------------- 30 | | 31 | | 32 | */ 33 | 'form_statuses' => [ 34 | 0 => [ 35 | 'label' => 'Draft', 36 | 'class' => 'warning', 37 | ], 38 | 1 => [ 39 | 'label' => 'Published', 40 | 'class' => 'success', 41 | ], 42 | 2 => [ 43 | 'label' => 'Unpublished', 44 | 'class' => 'error', 45 | ], 46 | ], 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Google Recaptcha 50 | |-------------------------------------------------------------------------- 51 | | 52 | | 53 | */ 54 | 'google_recaptcha_enabled' => true, 55 | 'google_recaptcha_key' => env('GOOGLE_RECAPTCHA_KEY'), 56 | 'google_recaptcha_secret' => env('GOOGLE_RECAPTCHA_SECRET'), 57 | ]; 58 | -------------------------------------------------------------------------------- /src/Transformers/FieldTransformer.php: -------------------------------------------------------------------------------- 1 | $question->id, 11 | 'title' => $question->title, 12 | 'label' => $question->label, 13 | 'field_type' => $question->field_type, 14 | 'field_type_name' => $this->fieldTypeTitle($question->field_type), 15 | 'has_choices' => $this->hasChoices($question->field_type), 16 | 'help_text' => $question->help_text, 17 | 'placeholder' => $question->placeholder, 18 | 'render' => $question->field()->render(), 19 | 'is_required' => $question->is_required, 20 | 'description' => $question->description, 21 | 'validations' => $question->validations, 22 | 'options' => $question->options, 23 | 'options_text' => implode(PHP_EOL, $question->options), 24 | 'position' => $question->position, 25 | 'default_value' => $question->default_value, 26 | 'columns_count' => $question->columns_count ?? 12, 27 | ]; 28 | } 29 | 30 | protected function fieldTypeTitle($field) 31 | { 32 | return substr($field, strrpos($field, '\\') + 1); 33 | } 34 | 35 | protected function hasChoices($field) 36 | { 37 | return app($field)->hasChoices(); 38 | } 39 | } 40 | 41 | // "id": { 42 | // "id": 1, 43 | // "form_id": "1", 44 | // "title": "wqeqw", 45 | // "label": "qweqwe", 46 | // "place_holder": "qweqwe", 47 | // "help_text": null, 48 | // "is_required": true, 49 | // "description": "eqweqweqwe", 50 | // "field_type": "Musonza\\Form\\Fields\\CheckBox", 51 | // "validations": null, 52 | // "properties": null, 53 | // "options": [], 54 | // "created_at": "2018-10-13 16:33:29", 55 | // "updated_at": "2018-10-13 16:33:29" 56 | // } 57 | -------------------------------------------------------------------------------- /src/Models/Question.php: -------------------------------------------------------------------------------- 1 | 'boolean', 31 | 'properties' => 'array', 32 | 'options' => 'array', 33 | 'position' => 'integer', 34 | ]; 35 | 36 | protected $attributes = [ 37 | 'options' => '{}', 38 | ]; 39 | 40 | public function __construct(array $attributes = array(), $exists = false) 41 | { 42 | parent::__construct($attributes, $exists); 43 | 44 | // $this->initListify(); 45 | } 46 | 47 | /** 48 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 49 | */ 50 | public function form() 51 | { 52 | return $this->belongsTo(Form::class); 53 | } 54 | 55 | public function answers() 56 | { 57 | return $this->hasMany(Answer::class); 58 | } 59 | 60 | public function addValidations($validations) 61 | { 62 | $this->validations = $validations; 63 | $this->save(); 64 | return $this; 65 | } 66 | 67 | /** 68 | * Get prefilled value for the field. 69 | * 70 | * @return mixed 71 | */ 72 | public function getValueAttribute() 73 | { 74 | return ""; 75 | } 76 | 77 | public function field() 78 | { 79 | return new $this->field_type($this, $this->options); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Feature/Question/CreateTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 20 | } 21 | 22 | public function testCreateSuccess() 23 | { 24 | $data = [ 25 | 'label' => 'First Name Label', 26 | 'description' => 'Description', 27 | 'field_type' => Text::class, 28 | ]; 29 | 30 | $response = $this->postJson(route('forms.fields.store', $this->form->id), $data); 31 | 32 | $response 33 | ->assertStatus(200) 34 | ->assertJson([ 35 | 'label' => 'First Name Label', 36 | 'description' => 'Description', 37 | 'field_type' => 'Musonza\Form\Fields\Text', 38 | ]); 39 | } 40 | 41 | public function testQuestionRequiresLabel() 42 | { 43 | $data = [ 44 | 'description' => 'Description', 45 | 'field_type' => Text::class, 46 | ]; 47 | 48 | $response = $this->postJson(route('forms.fields.store', $this->form->id), $data) 49 | ->assertStatus(422) 50 | ->assertJson([ 51 | 'message' => 'The given data was invalid.', 52 | ]); 53 | } 54 | 55 | public function testQuestionRequiresFieldType() 56 | { 57 | $data = [ 58 | 'label' => 'First Name Label', 59 | 'description' => 'Description', 60 | ]; 61 | 62 | $response = $this->postJson(route('forms.fields.store', $this->form->id), $data) 63 | ->assertStatus(422) 64 | ->assertJson([ 65 | 'message' => 'The given data was invalid.', 66 | ]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/AnswerTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 21 | $this->question = Form::createQuestion( 22 | [ 23 | 'title' => 'First Name', 24 | 'label' => 'First Name', 25 | 'description' => 'Description', 26 | 'field_type' => Text::class, 27 | 'form_id' => $this->form->id, 28 | ] 29 | ); 30 | 31 | $submission = $this->form->addSubmission([]); 32 | $question = $this->form->questions()->first(); 33 | $this->answer = $question->answers()->create([ 34 | 'value' => 'Jane', 35 | 'submission_id' => $submission->id, 36 | ]); 37 | } 38 | 39 | public function testCreateFormQuestionAnswer() 40 | { 41 | $this->assertInstanceOf(Answer::class, $this->answer); 42 | $this->assertInstanceOf(Submission::class, $this->answer->submission); 43 | } 44 | 45 | public function testGetAnswerById() 46 | { 47 | $answer = Form::answerService()->getById(1); 48 | $this->assertEquals($this->answer->id, $answer->id); 49 | $this->assertInstanceOf(Answer::class, $answer); 50 | } 51 | 52 | public function testDeleteAnswer() 53 | { 54 | $this->assertDatabaseHas($this->tablePrefix . 'answers', ['id' => 1, 'value' => 'Jane']); 55 | 56 | Form::answerService() 57 | ->getById(1) 58 | ->delete(); 59 | 60 | $this->assertDatabaseMissing($this->tablePrefix . 'answers', ['id' => 1, 'value' => 'Jane']); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /resources/js/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 85 | -------------------------------------------------------------------------------- /database/seeds/FormsDatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 19 | $fieldTypes = ['text', 'textarea', 'checkbox', 'radio', 'select']; 20 | $props = []; 21 | $props['choices'] = [ 22 | [ 23 | 'label' => 'Choice1', 24 | 'recode' => 1, 25 | ], 26 | [ 27 | 'label' => 'Choice2', 28 | 'description' => 'This is a test description', 29 | 'attachment' => [ 30 | "type" => "image", 31 | "href" => "http://example.com/img", 32 | ], 33 | ], 34 | ['label' => 'Choice3', 'recode' => 3], 35 | ]; 36 | 37 | factory(Form::class, 55)->create()->each(function ($form) use ($props) { 38 | $form->questions()->save(factory(Question::class)->make([ 39 | 'form_id' => $form->id, 40 | 'field_type' => Text::class, 41 | ])); 42 | $form->questions()->save(factory(Question::class)->make([ 43 | 'form_id' => $form->id, 44 | 'field_type' => TextArea::class, 45 | ])); 46 | $form->questions()->save(factory(Question::class)->make([ 47 | 'form_id' => $form->id, 48 | 'field_type' => Text::class, 49 | 'properties' => $props, 50 | ])); 51 | $form->questions()->save(factory(Question::class)->make([ 52 | 'form_id' => $form->id, 53 | 'field_type' => Text::class, 54 | 'properties' => $props, 55 | ])); 56 | $form->questions()->save(factory(Question::class)->make([ 57 | 'form_id' => $form->id, 58 | 'field_type' => Text::class, 59 | 'properties' => $props, 60 | ])); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->string('name'); 20 | $table->string('email')->unique(); 21 | $table->string('password'); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | protected function migrate() 28 | { 29 | (new CreateFormTables)->up(); 30 | $this->migrateTestTables(); 31 | } 32 | 33 | public function tearDown(): void 34 | { 35 | $this->rollbackTestTables(); 36 | (new CreateFormTables)->down(); 37 | // parent::tearDown(); 38 | } 39 | 40 | public function createUsers($count = 1) 41 | { 42 | return factory(User::class, $count)->create(); 43 | } 44 | 45 | protected function rollbackTestTables() 46 | { 47 | Schema::drop('users'); 48 | } 49 | 50 | protected function getPackageProviders($app) 51 | { 52 | return [ 53 | \Orchestra\Database\ConsoleServiceProvider::class, 54 | \Musonza\Form\FormServiceProvider::class, 55 | ]; 56 | } 57 | 58 | protected function getPackageAliases($app) 59 | { 60 | return [ 61 | 'Form' => \Musonza\Form\Facades\FormFacade::class, 62 | ]; 63 | } 64 | 65 | protected function disableExceptionHandling($app) 66 | { 67 | $app->instance(ExceptionHandler::class, new class extends Handler 68 | { 69 | public function __construct() 70 | { 71 | } 72 | 73 | public function report(\Exception $e) 74 | { 75 | // no-op 76 | } 77 | 78 | public function render($request, \Exception $e) 79 | { 80 | throw $e; 81 | } 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/FormServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishMigrations(); 24 | $this->publishDatabaseSeeds(); 25 | $this->publishConfig(); 26 | 27 | require __DIR__ . '/Http/routes.php'; 28 | 29 | $this->publishes([ 30 | __DIR__ . '/../public' => public_path('vendor/laravel-forms'), 31 | ], 'laravel-forms-assets'); 32 | } 33 | 34 | /** 35 | * Register application services. 36 | * 37 | * @return void 38 | */ 39 | public function register() 40 | { 41 | $this->app->bind('form', function () { 42 | return $this->app->make(\Musonza\Form\Form::class); 43 | }); 44 | } 45 | 46 | /** 47 | * Publish package's migrations. 48 | * 49 | * @return void 50 | */ 51 | public function publishMigrations() 52 | { 53 | $timestamp = date('Y_m_d_His', time()); 54 | $stub = __DIR__ . '/../database/migrations/create_form_tables.php'; 55 | $target = $this->app->databasePath() . '/migrations/' . $timestamp . '_create_form_tables.php'; 56 | 57 | $this->publishes([$stub => $target], 'laravel_forms.migrations'); 58 | } 59 | 60 | /** 61 | * Publish database seeds. 62 | * 63 | * @return void 64 | */ 65 | public function publishDatabaseSeeds() 66 | { 67 | $this->publishes([ 68 | __DIR__ . '/../database/seeds' => database_path() . '/seeds', 69 | ], 'laravel_forms.database_seeds'); 70 | } 71 | 72 | /** 73 | * Publish package's config file. 74 | * 75 | * @return void 76 | */ 77 | public function publishConfig() 78 | { 79 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'laravel-forms'); 80 | $this->publishes([ 81 | __DIR__ . '/../config' => config_path(), 82 | ], 'laravel_forms.config'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /resources/js/base.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | /** 4 | * Show an error message. 5 | */ 6 | alertError(message) { 7 | this.$root.alert.type = 'error'; 8 | this.$root.alert.message = message; 9 | this.$root.alert.show = true; 10 | }, 11 | 12 | alertWarning(message) { 13 | this.$root.alert.type = 'warning'; 14 | this.$root.alert.message = message; 15 | this.$root.alert.show = true; 16 | }, 17 | 18 | /** 19 | * Show a success message. 20 | */ 21 | alertSuccess(message, autoClose) { 22 | this.$root.alert.type = 'success'; 23 | this.$root.alert.message = message; 24 | this.$root.alert.show = true; 25 | }, 26 | 27 | /** 28 | * Show confirmation message. 29 | */ 30 | alertConfirm(message, success, failure, title) { 31 | this.$root.alert.type = 'confirmation'; 32 | this.$root.alert.autoClose = false; 33 | this.$root.alert.title = title; 34 | this.$root.alert.message = message; 35 | this.$root.alert.confirmationAgree = success; 36 | this.$root.alert.confirmationCancel = failure; 37 | this.$root.alert.show = true; 38 | }, 39 | 40 | dismissAlert() { 41 | this.$root.alert.show = false; 42 | }, 43 | 44 | formatErrorMessage(response) { 45 | let message = ''; 46 | if (response.data) { 47 | let data = response.data; 48 | message += data.message; 49 | 50 | if (data.errors) { 51 | for (let [key, value] of Object.entries(data.errors)) { 52 | message += ' ' + value; 53 | break; 54 | } 55 | } 56 | } 57 | return message; 58 | }, 59 | 60 | getConfirmationMessages() { 61 | return { 62 | 'delete_form': { 63 | 'title': 'Delete Form', 64 | 'message': 'Are you sure you want to delete this form? This action cannot be undone.' 65 | }, 66 | 'delete_field': { 67 | 'title': 'Delete Field', 68 | 'message': 'Are you sure you want to delete this field? This action cannot be undone.' 69 | }, 70 | 'delete_submission': { 71 | 'title': 'Delete Submission', 72 | 'message': 'Are you sure you want to delete this submission? This action cannot be undone.' 73 | }, 74 | } 75 | } 76 | } 77 | }; -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | define(Musonza\Form\User::class, function (Faker $faker) { 11 | return [ 12 | 'name' => $faker->name, 13 | 'email' => $faker->unique()->safeEmail, 14 | 'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret 15 | 'remember_token' => str_random(10), 16 | ]; 17 | }); 18 | 19 | $factory->define(Form::class, function (Faker $faker) { 20 | return [ 21 | 'title' => $faker->sentence, 22 | 'description' => $faker->sentence, 23 | ]; 24 | }); 25 | 26 | $factory->define(Question::class, function (Faker $faker) { 27 | return [ 28 | 'label' => $faker->sentence, 29 | 'description' => $faker->sentence, 30 | 'form_id' => function () { 31 | return factory(Form::class)->create()->id; 32 | }, 33 | 'field_type' => Text::class, 34 | 'is_required' => true, 35 | ]; 36 | }); 37 | 38 | $factory->define(Submission::class, function (Faker $faker) { 39 | return [ 40 | 'ip_address' => $faker->ipv4, 41 | 'response' => [ 42 | 'field1' => [ 43 | 'field_identifier' => 'field1', 44 | 'response_text' => $faker->sentence, 45 | ], 46 | 'field2' => [ 47 | 'field_identifier' => 'field2', 48 | 'response_text' => $faker->paragraph, 49 | ], 50 | 'field3' => [ 51 | 'field_identifier' => 'field3', 52 | 'response_text' => $faker->sentence, 53 | ], 54 | ], 55 | 'form_id' => function () { 56 | return factory(Form::class)->create()->id; 57 | }, 58 | ]; 59 | }); 60 | 61 | $factory->define(SubmissionResponse::class, function (Faker $faker) { 62 | return [ 63 | 'submission_id' => function () { 64 | return factory(Submission::class)->create()->id; 65 | }, 66 | 'question_id' => function () { 67 | return factory(Question::class)->create()->id; 68 | }, 69 | 'response_text' => $faker->sentence, 70 | ]; 71 | }); 72 | -------------------------------------------------------------------------------- /tests/Unit/QuestionTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 19 | $this->question = Form::createQuestion( 20 | [ 21 | 'label' => 'First Name', 22 | 'description' => 'Description', 23 | 'field_type' => Text::class, 24 | 'form_id' => $this->form->id, 25 | ] 26 | ); 27 | } 28 | 29 | public function testCreatesQuestion() 30 | { 31 | $this->assertDatabaseHas($this->tablePrefix . 'questions', ['id' => 1]); 32 | } 33 | 34 | public function testGetQuestionById() 35 | { 36 | $question = Form::questionService()->getById(1); 37 | $this->assertEquals(1, $question->id); 38 | } 39 | 40 | public function testQuestionBelongsToForm() 41 | { 42 | $this->assertInstanceOf(\Musonza\Form\Models\Form::class, $this->question->form); 43 | } 44 | 45 | public function testUpdatesQuestion() 46 | { 47 | $this->question->update([ 48 | 'label' => 'New Label', 49 | 'field_type' => TextArea::class, 50 | ]); 51 | 52 | $this->assertEquals('New Label', $this->question->label); 53 | $this->assertInstanceOf(TextArea::class, $this->question->field()); 54 | } 55 | 56 | public function testDeleteQuestion() 57 | { 58 | $this->assertDatabaseHas($this->tablePrefix . 'questions', ['id' => 1]); 59 | Form::questionService() 60 | ->getById(1) 61 | ->delete(); 62 | $this->assertDatabaseMissing($this->tablePrefix . 'questions', ['id' => 1]); 63 | } 64 | 65 | public function testAddQuestionValidations() 66 | { 67 | $validations = "required | email"; 68 | $this->question->addValidations($validations); 69 | $this->assertEquals($validations, $this->question->validations); 70 | } 71 | 72 | public function testResolvesQuestionField() 73 | { 74 | $this->assertInstanceOf(Text::class, $this->question->field()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /resources/js/pages/forms/fields/create.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 95 | -------------------------------------------------------------------------------- /tests/Unit/Fields/FieldsRenderTest.php: -------------------------------------------------------------------------------- 1 | form = Form::create(['title' => 'Contact Form']); 22 | } 23 | 24 | /** 25 | * @dataProvider fieldsDataProvider 26 | */ 27 | public function testFieldRender($field, $options, $expected) 28 | { 29 | $question = $this->createQuestion($field, $options); 30 | 31 | $this->assertEquals($expected, $question->field()->render()); 32 | } 33 | 34 | public function fieldsDataProvider() 35 | { 36 | return [ 37 | [ 38 | TextArea::class, [], 39 | '', 40 | ], 41 | [ 42 | Text::class, [], 43 | '', 44 | ], 45 | [ 46 | Password::class, [], 47 | '', 48 | ], 49 | [ 50 | Radio::class, 51 | ['male'], 52 | ' male
', 53 | ], 54 | 55 | [ 56 | Select::class, 57 | ['male', 'female'], 58 | '', 59 | ], 60 | [ 61 | Select::class, 62 | ['m' => 'male', 'f' => 'female'], 63 | '', 64 | ], 65 | ]; 66 | } 67 | 68 | public function createQuestion($field, $options) 69 | { 70 | return Form::createQuestion( 71 | [ 72 | 'label' => 'First Name', 73 | 'description' => 'Description', 74 | 'field_type' => $field, 75 | 'options' => $options, 76 | 'form_id' => $this->form->id, 77 | ] 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /resources/js/components/FormsComponent.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 89 | -------------------------------------------------------------------------------- /src/Http/Controllers/FormController.php: -------------------------------------------------------------------------------- 1 | formTransformer = $formTransformer; 29 | $this->form = $form; 30 | } 31 | 32 | /** 33 | * List forms. 34 | * 35 | * @param ListFormRequest $request 36 | * @return \Illuminate\Http\Response 37 | */ 38 | public function index(ListFormRequest $request) 39 | { 40 | $forms = FormModel::all(); 41 | 42 | return response($this->formTransformer->transformCollection($forms)); 43 | } 44 | 45 | /** 46 | * Gets the form by id. 47 | * 48 | * @param FormModel $form 49 | * @return \Illuminate\Http\Response 50 | */ 51 | public function show(FormModel $form) 52 | { 53 | request()->query->add(['include' => 'questions']); 54 | 55 | return response($this->formTransformer->transformItem($form)); 56 | } 57 | 58 | /** 59 | * Stores the created form. 60 | * 61 | * @param CreateFormRequest $request 62 | * @return \Illuminate\Http\Response 63 | */ 64 | public function store(CreateFormRequest $request) 65 | { 66 | $form = $this->form->create($request->validated()); 67 | 68 | return response($this->formTransformer->transformItem($form)); 69 | } 70 | 71 | /** 72 | * Updates form details. 73 | * 74 | * @param UpdateFormRequest $request 75 | * @param FormModel $form 76 | * @return \Illuminate\Http\Response 77 | */ 78 | public function update(UpdateFormRequest $request, FormModel $form) 79 | { 80 | $form->update($request->validated()); 81 | 82 | return response($this->formTransformer->transformItem($form)); 83 | } 84 | 85 | /** 86 | * Deletes a form. 87 | * 88 | * @param DeleteFormRequest $request 89 | * @param FormModel $form 90 | * @return \Illuminate\Http\Response 91 | * @throws \Exception 92 | */ 93 | public function destroy(DeleteFormRequest $request, FormModel $form) 94 | { 95 | $form->delete(); 96 | 97 | return response('', 201); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Fields/FormField.php: -------------------------------------------------------------------------------- 1 | question = $question; 26 | $this->name = "field_{$question->id}"; 27 | $this->options = $options; 28 | $this->fieldHtmlId = $this->nameToId(); 29 | } 30 | 31 | protected function input() 32 | { 33 | $this->attributes['type'] = $this->controlType; 34 | $this->attributes['value'] = $this->question->value; 35 | $this->attributes['placeholder'] = $this->question->placeholder; 36 | $this->attributes = $this->attributes($this->attributes); 37 | 38 | $input = 'attributes . '>'; 39 | 40 | //dd($input); 41 | 42 | return 'attributes . '>'; 43 | } 44 | 45 | /** 46 | * [attributes description] 47 | * @param array $attributes [description] 48 | * @return [type] [description] 49 | */ 50 | protected function attributes(array $attributes) 51 | { 52 | $html = array(); 53 | 54 | foreach ((array) $attributes as $key => $value) { 55 | if (is_numeric($key)) { 56 | $key = $value; 57 | } 58 | if ($value !== null) { 59 | $html[] = $key . '="' . e($value) . '"'; 60 | } 61 | } 62 | return empty($html) ? '' : ' ' . implode(' ', $html); 63 | } 64 | 65 | /** 66 | * [nameToId description] 67 | * @return [type] [description] 68 | */ 69 | protected function nameToId() 70 | { 71 | return str_replace(array('.', '[]', '[', ']'), array('_', '', '_', ''), $this->name); 72 | } 73 | 74 | /** 75 | * [render description] 76 | * @return [type] [description] 77 | */ 78 | public function render() 79 | { 80 | if (!isset($this->attributes['name'])) { 81 | $this->attributes['name'] = $this->name; 82 | } 83 | 84 | if ($this->question->is_required) { 85 | $this->attributes['required'] = true; 86 | } 87 | 88 | if (!isset($this->attributes['id'])) { 89 | $this->attributes['id'] = $this->nameToId(); 90 | } 91 | 92 | $this->attributes['class'] = "form-control"; 93 | 94 | return $this->input(); 95 | } 96 | 97 | public function hasChoices() 98 | { 99 | return $this->hasChoices; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Http/Controllers/FormSubmissionController.php: -------------------------------------------------------------------------------- 1 | formTransformer = $formTransformer; 36 | $this->submissionTransformer = $submissionTransformer; 37 | $this->form = $form; 38 | } 39 | 40 | public function index(FormModel $form) 41 | { 42 | $submissions = $this->submissionTransformer->transformCollection($form->submissions()->orderBy('id', 'DESC')->get()); 43 | $data = ['form' => $this->formTransformer->transformItem($form), 'submissions' => $submissions]; 44 | 45 | return response($data); 46 | } 47 | 48 | public function create(Request $request, FormModel $form) 49 | { 50 | request()->query->add(['include' => 'questions']); 51 | 52 | $form = $this->formTransformer->transformItem($form); 53 | 54 | if (request()->wantsJson()) { 55 | return response($form); 56 | } 57 | 58 | $googleRecaptchaEnabled = $this->form->googleRecaptchaEnabled(); 59 | 60 | return view('laravel-forms::submissions.edit', compact('form', 'googleRecaptchaEnabled')); 61 | } 62 | 63 | public function store(CreateFormSubmissionRequest $request, FormModel $form) 64 | { 65 | $data = $request->except([ 66 | 'g-recaptcha-response', 67 | '_token' 68 | ]); 69 | 70 | $submission = $form->addSubmission($data); 71 | $answers = []; 72 | 73 | foreach ($data as $key => $value) { 74 | // if ($value) { 75 | $questionId = str_replace('field_', '', $key); 76 | $answers[$questionId]['question_id'] = $questionId; 77 | $answers[$questionId]['value'] = $value; 78 | $answers[$questionId]['submission_id'] = $submission->id; 79 | // } 80 | } 81 | 82 | $submission->answers()->insert($answers); 83 | 84 | // Do this in an event listener 85 | $submission->is_complete = true; 86 | $submission->save(); 87 | 88 | $this->flashSuccess('Your submission was stored'); 89 | 90 | return back(); 91 | } 92 | 93 | public function show(FormModel $form, Submission $submission) 94 | { 95 | request()->query->add(['include' => 'answers']); 96 | 97 | return response($this->submissionTransformer->transformItem($submission)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /database/migrations/create_form_tables.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('title'); 19 | $table->string('description')->nullable(); 20 | $table->unsignedInteger('status')->default(0); 21 | $table->boolean('enable_captcha')->default(true); 22 | $table->timestamps(); 23 | }); 24 | 25 | Schema::create('mc_questions', function (Blueprint $table) { 26 | $table->increments('id'); 27 | $table->unsignedInteger('form_id'); 28 | $table->string('label')->nullable(); 29 | $table->string('placeholder')->nullable(); 30 | $table->string('help_text')->nullable(); 31 | $table->boolean('is_required')->default(true); 32 | $table->text('description')->nullable(); 33 | $table->text('field_type'); 34 | $table->text('validations')->nullable(); 35 | $table->json('properties')->nullable(); 36 | $table->json('options')->default(); 37 | $table->text('default_value')->nullable(); 38 | $table->unsignedInteger('columns')->default(12); 39 | $table->unsignedInteger('position')->nullable(); 40 | $table->timestamps(); 41 | 42 | $table->foreign('form_id') 43 | ->references('id') 44 | ->on('mc_forms') 45 | ->onDelete('cascade'); 46 | }); 47 | 48 | Schema::create('mc_answers', function (Blueprint $table) { 49 | $table->increments('id'); 50 | $table->unsignedInteger('question_id'); 51 | $table->unsignedInteger('submission_id'); 52 | $table->text('value'); 53 | $table->timestamps(); 54 | 55 | $table->foreign('question_id') 56 | ->references('id') 57 | ->on('mc_questions') 58 | ->onDelete('cascade'); 59 | 60 | $table->foreign('submission_id') 61 | ->references('id') 62 | ->on('mc_submissions') 63 | ->onDelete('cascade'); 64 | }); 65 | 66 | Schema::create('mc_submissions', function (Blueprint $table) { 67 | $table->increments('id'); 68 | $table->unsignedInteger('form_id'); 69 | $table->unsignedInteger('user_id')->nullable(); 70 | $table->json('ip_address')->nullable(); 71 | $table->json('response')->nullable(); 72 | $table->boolean('is_complete')->default(false); 73 | $table->timestamps(); 74 | 75 | $table->foreign('form_id') 76 | ->references('id') 77 | ->on('mc_forms') 78 | ->onDelete('cascade'); 79 | }); 80 | } 81 | 82 | /** 83 | * Reverse the migrations. 84 | * 85 | * @return void 86 | */ 87 | public function down() 88 | { 89 | Schema::dropIfExists('mc_forms'); 90 | Schema::dropIfExists('mc_questions'); 91 | Schema::dropIfExists('mc_answers'); 92 | Schema::dropIfExists('mc_submissions'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Http/Controllers/FormFieldController.php: -------------------------------------------------------------------------------- 1 | fieldTransformer = $fieldTransformer; 28 | $this->fieldTypeTransformer = $fieldTypeTransformer; 29 | } 30 | 31 | public function index(FormModel $form) 32 | { 33 | $fields = $form->questions()->orderBy('position')->get(); 34 | 35 | $fields = $this->fieldTransformer->transformCollection($fields); 36 | 37 | return response($fields); 38 | } 39 | 40 | public function create(FormModel $form) 41 | { 42 | $fieldTypes = $this->fieldTypes(); 43 | 44 | $field = []; 45 | 46 | return view('laravel-forms::forms.fields.create', compact('form', 'fieldTypes', 'field')); 47 | } 48 | 49 | public function show(FormModel $form, Question $field) 50 | { 51 | return response($field); 52 | } 53 | 54 | public function store(CreateFormQuestionRequest $request, FormModel $form) 55 | { 56 | $data = $request->all(); 57 | $data['options'] = []; 58 | 59 | if ($request->options) { 60 | $options = $this->normalizeOptions($request->options); 61 | $data['options'] = $options; 62 | } 63 | 64 | $field = $form->questions()->create($data); 65 | return response($field); 66 | } 67 | 68 | public function destroy(Request $request, FormModel $form, Question $field) 69 | { 70 | $field->delete(); 71 | return response('', 201); 72 | } 73 | 74 | public function edit(FormModel $form, Question $field) 75 | { 76 | $field = $this->fieldTransformer->transformItem($field); 77 | $fieldTypes = $this->fieldTypes(); 78 | return view('laravel-forms::forms.fields.edit', compact('form', 'field', 'fieldTypes')); 79 | } 80 | 81 | public function update(UpdateFormQuestionRequest $request, FormModel $form, Question $field) 82 | { 83 | $data = $request->validated(); 84 | $data['options'] = []; 85 | 86 | if ($request->options) { 87 | $options = $this->normalizeOptions($request->options); 88 | $data['options'] = $options; 89 | } 90 | 91 | if ($request->position && $request->position != $field->position) { 92 | $field->insertAt($request->position); 93 | } 94 | 95 | $field->update($data); 96 | 97 | return response($this->fieldTransformer->transformItem($field)); 98 | } 99 | 100 | protected function normalizeOptions($options) 101 | { 102 | $options = array_unique($options); 103 | return array_values($options); 104 | } 105 | 106 | protected function fieldTypes($value = '') 107 | { 108 | return $this->fieldTypeTransformer->transformCollection(config('laravel_forms.fields')); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /resources/js/components/FormSubmissionsComponent.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 110 | -------------------------------------------------------------------------------- /resources/js/pages/forms/edit.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 175 | -------------------------------------------------------------------------------- /resources/js/components/FormFieldsComponent.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 201 | 202 | 213 | 214 | --------------------------------------------------------------------------------