├── .gitignore ├── .travis.yml ├── README.md ├── pom.xml ├── src ├── main │ ├── java │ │ └── fr │ │ │ └── redfroggy │ │ │ └── dynamicforms │ │ │ ├── Application.java │ │ │ ├── configuration │ │ │ └── ExtraFieldConfiguration.java │ │ │ ├── converter │ │ │ └── HashMapToStringConverter.java │ │ │ ├── model │ │ │ ├── Customer.java │ │ │ └── FieldExtensible.java │ │ │ ├── repository │ │ │ └── CustomerRepository.java │ │ │ ├── rest │ │ │ └── CustomerResource.java │ │ │ ├── service │ │ │ └── CustomerService.java │ │ │ └── utils │ │ │ ├── Form.java │ │ │ ├── FormField.java │ │ │ ├── FormFieldDropDown.java │ │ │ ├── FormFieldLabel.java │ │ │ ├── FormFieldValue.java │ │ │ └── FormUtils.java │ └── resources │ │ ├── customer.json │ │ └── static │ │ ├── app │ │ ├── app.component.ts │ │ ├── app.html │ │ ├── app.module.ts │ │ ├── app.routes.ts │ │ ├── customer │ │ │ ├── customer.html │ │ │ ├── customer.module.ts │ │ │ └── customer.ts │ │ ├── customers │ │ │ ├── customers.html │ │ │ ├── customers.module.ts │ │ │ └── customers.ts │ │ ├── form │ │ │ ├── error │ │ │ │ └── control-messages.ts │ │ │ ├── extra-form.ts │ │ │ ├── fields │ │ │ │ ├── date-input-extra-field.ts │ │ │ │ ├── extra-field.ts │ │ │ │ ├── file-input-extra-field.ts │ │ │ │ ├── input-extra-field.ts │ │ │ │ ├── select-extra-field.ts │ │ │ │ └── textarea-extra-field.ts │ │ │ ├── form.module.ts │ │ │ ├── model │ │ │ │ └── form.ts │ │ │ └── validators │ │ │ │ └── validator.service.ts │ │ ├── header │ │ │ ├── header.html │ │ │ ├── header.module.ts │ │ │ └── header.ts │ │ ├── main.ts │ │ └── pipes │ │ │ ├── pipes.module.ts │ │ │ └── values.pipe.ts │ │ ├── assets │ │ ├── img │ │ │ └── red_froggy.png │ │ └── styles │ │ │ └── style.css │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── init.js │ │ ├── package.json │ │ └── systemjs.config.js └── test │ ├── java │ └── fr │ │ └── redfroggy │ │ └── dynamicforms │ │ ├── ApplicationTest.java │ │ ├── TestUtil.java │ │ ├── configuration │ │ └── ExtraFieldConfigurationTest.java │ │ └── rest │ │ └── CustomerResourceTest.java │ └── resources │ └── customer.json ├── tsconfig.json ├── tslint.json └── typings └── lodash └── lodash.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | src/main/resources/static/node_modules 2 | target/ 3 | .idea/ 4 | *.iml 5 | src/main/resources/static/app/**/*.js 6 | src/main/resources/static/app/**/*.js.map 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: true 3 | services: 4 | - docker 5 | script: 6 | - mvn clean test -P travis 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/70a442e7b3eb421998f522a4a9dcd4fb)](https://www.codacy.com/app/michaeldesigaud/angular-spring-dynamic-form?utm_source=github.com&utm_medium=referral&utm_content=RedFroggy/angular-spring-dynamic-form&utm_campaign=badger) 2 | Dynamic form creation with Angular 2 and Spring [![Build Status](https://travis-ci.org/RedFroggy/angular-spring-dynamic-form.svg?branch=master)](https://travis-ci.org/RedFroggy/angular-spring-dynamic-form) 3 | =============================================== 4 | 5 | Dynamic form creation and validation using Angular 2 and Spring for a list of customers 6 | 7 | #Stack 8 | - Spring Boot 9 | - Spring MVC 10 | - H2 in-memory database 11 | - Angular 2.4.1 12 | 13 | #Features 14 | - Extra fields description with a JSON file (server-side) 15 | - Customer list 16 | - Customer creation / edition 17 | - Dynamic rendering of input: Tested for text,email,url,file,date inputs 18 | - Dynamic rendering of textara, select. 19 | - Each dynamic field is fully validated 20 | 21 | # Example of fields description with JSON file 22 | ```` 23 | { 24 | "entityName":"Customer", 25 | "version":1, 26 | "fields":[ 27 | { 28 | "id":1, 29 | "type":"email", 30 | "name":"email", 31 | "value":"defaultemail@redfroggy.fr", 32 | "label":"Email", 33 | "required":true, 34 | "showAsColumn":true 35 | }, 36 | { 37 | "id":2, 38 | "type":"number", 39 | "name":"age", 40 | "value":"28", 41 | "label":"Age", 42 | "required":true, 43 | "min":5, 44 | "max":100 45 | }, 46 | { 47 | "id":3, 48 | "type":"text", 49 | "name":"company", 50 | "label":"Company", 51 | "required":false, 52 | "minLength":3, 53 | "maxLength":10, 54 | "showAsColumn":true 55 | }, 56 | { 57 | "id":4, 58 | "type":"textarea", 59 | "name":"description", 60 | "label":"Description", 61 | "required":true 62 | }, 63 | { 64 | "id":5, 65 | "type":"file", 66 | "name":"attachment", 67 | "label":"Attachment", 68 | "fileAccept":"application/pdf" 69 | }, 70 | { 71 | "id":6, 72 | "type":"password", 73 | "name":"password", 74 | "label":"Password", 75 | "placeholder":"Ex: myp4ssw0rd", 76 | "pattern":"^[a-z0-9_-]{6,18}$" 77 | }, 78 | { 79 | "id":7, 80 | "type":"select", 81 | "name":"roles", 82 | "label":"Roles", 83 | "required":true, 84 | "options":[ 85 | { 86 | "id":1, 87 | "value":"Admin" 88 | }, 89 | { 90 | "id":2, 91 | "value":"Manager" 92 | }, 93 | { 94 | "id":1, 95 | "value":"User" 96 | } 97 | ] 98 | }, 99 | { 100 | "id":8, 101 | "type":"text", 102 | "name":"readable", 103 | "label":"Readable field", 104 | "value":"Readable value", 105 | "writable":false 106 | }, 107 | { 108 | "id":9, 109 | "type":"date", 110 | "name":"birthDate", 111 | "label":"Birth date", 112 | "required":true 113 | } 114 | ] 115 | } 116 | ```` 117 | 118 | #To run Java unit tests 119 | ````bash 120 | $ mvn test 121 | ```` 122 | 123 | #To run the application 124 | ````bash 125 | $ mvn spring-boot:run 126 | ```` 127 | - Npm modules should be automatically installed and typescript files compiled (see pom.xml file) 128 | - Then go to http://localhost:8080 129 | 130 | # Contributors 131 | * Brian Long ([@blong](https://github.com/b-long)) 132 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | fr.redfroggy.pocs 8 | spring-angular-dynamic-form 9 | 1.0-SNAPSHOT 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 1.3.2.RELEASE 15 | 16 | jar 17 | 18 | UTF-8 19 | 1.7 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-data-jpa 30 | 31 | 32 | com.h2database 33 | h2 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-test 38 | test 39 | 40 | 41 | 42 | commons-codec 43 | commons-codec 44 | 1.4 45 | 46 | 47 | commons-collections 48 | commons-collections 49 | 50 | 51 | org.apache.commons 52 | commons-lang3 53 | 3.4 54 | 55 | 56 | commons-io 57 | commons-io 58 | 2.4 59 | 60 | 61 | 62 | joda-time 63 | joda-time 64 | 65 | 66 | 67 | 68 | 69 | 70 | dev 71 | 72 | true 73 | 74 | 75 | 76 | 77 | org.codehaus.mojo 78 | exec-maven-plugin 79 | 80 | 81 | exec-npm-install 82 | generate-sources 83 | 84 | ${project.basedir}/src/main/resources/static 85 | npm 86 | 87 | install 88 | 89 | 90 | 91 | exec 92 | 93 | 94 | 95 | exec-npm-run-tsc 96 | generate-sources 97 | 98 | ${project.basedir}/src/main/resources/static 99 | npm 100 | 101 | run 102 | build 103 | 104 | 105 | 106 | exec 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | travis 116 | 117 | 118 | 119 | org.springframework.boot 120 | spring-boot-maven-plugin 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | org.springframework.boot 131 | spring-boot-maven-plugin 132 | 133 | 134 | 135 | repackage 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/Application.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms; 2 | 3 | import fr.redfroggy.dynamicforms.model.Customer; 4 | import fr.redfroggy.dynamicforms.repository.CustomerRepository; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.boot.CommandLineRunner; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.context.annotation.Bean; 11 | 12 | /** 13 | * Entry point 14 | * Created by Michael DESIGAUD on 25/02/2016. 15 | */ 16 | @SpringBootApplication 17 | public class Application { 18 | 19 | private static final Logger log = LoggerFactory.getLogger(Application.class); 20 | 21 | public static void main(String[] args) { 22 | SpringApplication.run(Application.class); 23 | } 24 | 25 | @Bean 26 | public CommandLineRunner initBDD(final CustomerRepository repository) { 27 | return new CommandLineRunner() { 28 | @Override 29 | public void run(String... strings) throws Exception { 30 | 31 | // save a couple of customers 32 | repository.save(new Customer("Jack", "Bauer")); 33 | repository.save(new Customer("Chloe", "O'Brian")); 34 | repository.save(new Customer("Kim", "Bauer")); 35 | repository.save(new Customer("David", "Palmer")); 36 | repository.save(new Customer("Michelle", "Dessler")); 37 | 38 | // fetch all customers 39 | log.info("Customers found with findAll():"); 40 | log.info("-------------------------------"); 41 | for (Customer customer : repository.findAll()) { 42 | log.info(customer.toString()); 43 | } 44 | log.info(""); 45 | 46 | // fetch an individual customer by ID 47 | Customer customer = repository.findOne(1L); 48 | log.info("Customer found with findOne(1L):"); 49 | log.info("--------------------------------"); 50 | log.info(customer.toString()); 51 | log.info(""); 52 | 53 | // fetch customers by last name 54 | log.info("Customer found with findByLastName('Bauer'):"); 55 | log.info("--------------------------------------------"); 56 | for (Customer bauer : repository.findByLastName("Bauer")) { 57 | log.info(bauer.toString()); 58 | } 59 | log.info(""); 60 | } 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/configuration/ExtraFieldConfiguration.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.configuration; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.exc.PropertyBindingException; 5 | import fr.redfroggy.dynamicforms.utils.Form; 6 | import fr.redfroggy.dynamicforms.utils.FormUtils; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.core.io.Resource; 13 | 14 | import javax.annotation.PostConstruct; 15 | import java.io.IOException; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | /** 20 | * Extra field json files configuration 21 | * Created by Michael DESIGAUD on 25/02/2016. 22 | */ 23 | @Configuration 24 | public class ExtraFieldConfiguration { 25 | 26 | private static final Logger log = LoggerFactory.getLogger(ExtraFieldConfiguration.class); 27 | 28 | @Autowired 29 | private ApplicationContext context; 30 | 31 | @PostConstruct 32 | public void init() throws IOException { 33 | 34 | ObjectMapper objectMapper = new ObjectMapper(); 35 | 36 | Resource[] resources = context.getResources("classpath:*.json"); 37 | log.info("{} json files found",resources.length); 38 | 39 | for (Resource resource : resources) { 40 | 41 | String fileName = resource.getFilename(); 42 | try { 43 | log.info("Reading file: {}",fileName); 44 | Form form = objectMapper.readValue(resource.getInputStream(), Form.class); 45 | 46 | FormUtils.forms.add(form); 47 | 48 | log.info("File name: {}", form); 49 | } catch(PropertyBindingException ex){ 50 | log.error("Error while reading file '{}': wrong json format",fileName,ex); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/converter/HashMapToStringConverter.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.converter; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.persistence.AttributeConverter; 9 | import javax.persistence.Converter; 10 | import java.io.IOException; 11 | import java.util.HashMap; 12 | 13 | /** 14 | * Hash map converter 15 | * Created by Michael DESIGAUD on 26/02/2016. 16 | */ 17 | @Converter 18 | public class HashMapToStringConverter implements AttributeConverter{ 19 | 20 | private static final Logger log = LoggerFactory.getLogger(HashMapToStringConverter.class); 21 | 22 | private ObjectMapper objectMapper; 23 | 24 | public HashMapToStringConverter(){ 25 | this.objectMapper = new ObjectMapper(); 26 | } 27 | 28 | /** Singleton Holder */ 29 | private static class HashMapToStringConverterHolder 30 | { 31 | /** unique Instance */ 32 | private final static HashMapToStringConverter instance = new HashMapToStringConverter(); 33 | } 34 | 35 | /** entry point for Singleton*/ 36 | public static HashMapToStringConverter getInstance() 37 | { 38 | return HashMapToStringConverterHolder.instance; 39 | } 40 | 41 | 42 | /** 43 | * Converts the value stored in the entity attribute into the 44 | * data representation to be stored in the database. 45 | * 46 | * @param attribute the entity attribute value to be converted 47 | * @return the converted data to be stored in the database column 48 | */ 49 | @Override 50 | public String convertToDatabaseColumn(HashMap attribute) { 51 | try { 52 | return this.objectMapper.writeValueAsString(attribute); 53 | } catch (IOException ex) { 54 | log.error("Cannot convert map into json string",ex); 55 | } 56 | return null; 57 | } 58 | 59 | /** 60 | * Converts the data stored in the database column into the 61 | * value to be stored in the entity attribute. 62 | * Note that it is the responsibility of the converter writer to 63 | * specify the correct dbData type for the corresponding column 64 | * for use by the JDBC driver: i.e., persistence providers are 65 | * not expected to do such type conversion. 66 | * 67 | * @param dbData the data from the database column to be converted 68 | * @return the converted value to be stored in the entity attribute 69 | */ 70 | @Override 71 | public HashMap convertToEntityAttribute(String dbData) { 72 | try { 73 | if(dbData == null){ 74 | return null; 75 | } 76 | return objectMapper.readValue(dbData,new TypeReference(){}); 77 | } catch (IOException ex) { 78 | log.error("Cannot convert string into map",ex); 79 | } 80 | return null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/model/Customer.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.model; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | 8 | /** 9 | * Customer entity 10 | * Created by Michael DESIGAUD on 25/02/2016. 11 | */ 12 | @Entity 13 | public class Customer extends FieldExtensible{ 14 | 15 | @Id 16 | @GeneratedValue(strategy= GenerationType.AUTO) 17 | private Long id; 18 | private String firstName; 19 | private String lastName; 20 | 21 | protected Customer() {} 22 | 23 | public Customer(String firstName, String lastName) { 24 | this.firstName = firstName; 25 | this.lastName = lastName; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return String.format( 31 | "Customer[id=%d, firstName='%s', lastName='%s']", 32 | id, firstName, lastName); 33 | } 34 | 35 | public Long getId() { 36 | return id; 37 | } 38 | 39 | public void setId(Long id) { 40 | this.id = id; 41 | } 42 | 43 | public String getFirstName() { 44 | return firstName; 45 | } 46 | 47 | public void setFirstName(String firstName) { 48 | this.firstName = firstName; 49 | } 50 | 51 | public String getLastName() { 52 | return lastName; 53 | } 54 | 55 | public void setLastName(String lastName) { 56 | this.lastName = lastName; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/model/FieldExtensible.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.model; 2 | 3 | import fr.redfroggy.dynamicforms.converter.HashMapToStringConverter; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Lob; 7 | import javax.persistence.MappedSuperclass; 8 | import java.io.Serializable; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * Add extra fields to a given jpa entity 14 | * Created by Michael DESIGAUD on 26/02/2016. 15 | */ 16 | @MappedSuperclass 17 | @SuppressWarnings("unchecked") 18 | public abstract class FieldExtensible implements Serializable { 19 | 20 | @Lob 21 | private String extraFields; 22 | 23 | /** 24 | * Get extra fields 25 | * @return HashMap with extra fields key and value 26 | */ 27 | public Map getExtraFields() { 28 | return HashMapToStringConverter.getInstance().convertToEntityAttribute(extraFields); 29 | } 30 | 31 | /** 32 | * Set extra fields 33 | * @param extraFields set HashMap extra fields 34 | */ 35 | public void setExtraFields(Map extraFields) { 36 | this.extraFields = HashMapToStringConverter.getInstance().convertToDatabaseColumn((HashMap) extraFields); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.repository; 2 | 3 | import fr.redfroggy.dynamicforms.model.Customer; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Customer repository 10 | * Created by Michael DESIGAUD on 25/02/2016. 11 | */ 12 | public interface CustomerRepository extends JpaRepository { 13 | 14 | List findByLastName(String lastName); 15 | } -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/rest/CustomerResource.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.rest; 2 | 3 | import fr.redfroggy.dynamicforms.model.Customer; 4 | import fr.redfroggy.dynamicforms.repository.CustomerRepository; 5 | import fr.redfroggy.dynamicforms.service.CustomerService; 6 | import fr.redfroggy.dynamicforms.utils.Form; 7 | import fr.redfroggy.dynamicforms.utils.FormUtils; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Customer api resource 15 | * Created by Michael DESIGAUD on 25/02/2016. 16 | */ 17 | @RestController 18 | @RequestMapping(path = "/api/customers") 19 | public class CustomerResource { 20 | 21 | @Autowired 22 | private CustomerService customerService; 23 | 24 | @RequestMapping(path = "",method = RequestMethod.GET) 25 | public List query(){ 26 | return this.customerService.query(); 27 | } 28 | 29 | @RequestMapping(path = "/{id}",method = RequestMethod.GET) 30 | public Customer get(@PathVariable Long id){ 31 | return this.customerService.get(id); 32 | } 33 | 34 | @RequestMapping(path = "",method = RequestMethod.POST) 35 | public Customer save(@RequestBody Customer customer){ 36 | return this.customerService.save(customer); 37 | } 38 | 39 | @RequestMapping(path = "",method = RequestMethod.PUT) 40 | public Customer update(@RequestBody Customer customer){ 41 | return this.customerService.save(customer); 42 | } 43 | 44 | @RequestMapping(path = "/form",method = RequestMethod.GET) 45 | public Form getForm(@RequestParam(name = "onlyExtraFields",required = false) Boolean onlyExtraFields){ 46 | return customerService.getForm(onlyExtraFields); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/service/CustomerService.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.service; 2 | 3 | import fr.redfroggy.dynamicforms.model.Customer; 4 | import fr.redfroggy.dynamicforms.repository.CustomerRepository; 5 | import fr.redfroggy.dynamicforms.utils.Form; 6 | import fr.redfroggy.dynamicforms.utils.FormUtils; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Customer service 15 | * Created by Michael DESIGAUD on 26/02/2016. 16 | */ 17 | @Service 18 | @Transactional 19 | public class CustomerService { 20 | 21 | @Autowired 22 | private CustomerRepository customerRepository; 23 | 24 | @Transactional(readOnly = true) 25 | public List query(){ 26 | return this.customerRepository.findAll(); 27 | } 28 | 29 | @Transactional(readOnly = true) 30 | public Customer get(Long id){ 31 | return this.customerRepository.findOne(id); 32 | } 33 | 34 | public Customer save(Customer customer){ 35 | return this.customerRepository.saveAndFlush(customer); 36 | } 37 | 38 | @Transactional(readOnly = true) 39 | public Form getForm(Boolean onlyExtraFields){ 40 | if(onlyExtraFields == null){ 41 | onlyExtraFields = false; 42 | } 43 | return FormUtils.describe(Customer.class,onlyExtraFields); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/utils/Form.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | 5 | import java.io.Serializable; 6 | import java.util.List; 7 | 8 | /** 9 | * Form description 10 | * Created by Michael DESIGAUD on 25/02/2016. 11 | */ 12 | @JsonInclude(JsonInclude.Include.NON_NULL) 13 | public class Form implements Serializable { 14 | 15 | private String entityName; 16 | 17 | private Integer version; 18 | 19 | private List fields; 20 | 21 | public String getEntityName() { 22 | return entityName; 23 | } 24 | 25 | public void setEntityName(String entityName) { 26 | this.entityName = entityName; 27 | } 28 | 29 | public List getFields() { 30 | return fields; 31 | } 32 | 33 | public void setFields(List fields) { 34 | this.fields = fields; 35 | } 36 | 37 | public Integer getVersion() { 38 | return version; 39 | } 40 | 41 | public void setVersion(Integer version) { 42 | this.version = version; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/utils/FormField.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * Form field description 12 | * Created by Michael DESIGAUD on 25/02/2016. 13 | */ 14 | @JsonInclude(JsonInclude.Include.NON_NULL) 15 | public class FormField implements Serializable { 16 | 17 | @JsonIgnore 18 | public static final String EXTRA_FIELD_PREFIX = "extra-"; 19 | 20 | private Integer id; 21 | 22 | private String entityName; 23 | 24 | private String type; 25 | 26 | private String name; 27 | 28 | private String label; 29 | 30 | @JsonIgnore 31 | private FormFieldValue value; 32 | 33 | @JsonProperty(value = "required") 34 | private Boolean required = false; 35 | 36 | @JsonIgnore 37 | private FormFieldLabel formFieldLabel; 38 | 39 | /** 40 | * list of element if is dropdown field 41 | */ 42 | @JsonProperty(value = "options") 43 | private List dropdownOptions; 44 | 45 | private Boolean extrafield = true; 46 | 47 | private Boolean readable = true; 48 | 49 | private Boolean writable = true; 50 | 51 | private String pattern; 52 | 53 | private List enumValues; 54 | 55 | private Integer min; 56 | 57 | private Integer max; 58 | 59 | private Integer minLength; 60 | 61 | private Integer maxLength; 62 | 63 | private Boolean showAsColumn; 64 | 65 | private String fileAccept; 66 | 67 | private String placeholder; 68 | 69 | public Integer getId() { 70 | return id; 71 | } 72 | 73 | public void setId(Integer id) { 74 | this.id = id; 75 | } 76 | 77 | public String getEntityName() { 78 | return entityName; 79 | } 80 | 81 | public void setEntityName(String entityName) { 82 | this.entityName = entityName; 83 | } 84 | 85 | public String getType() { 86 | return type; 87 | } 88 | 89 | public void setType(String type) { 90 | this.type = type; 91 | } 92 | 93 | public void setName(String name) { 94 | this.name = name; 95 | } 96 | 97 | public FormFieldValue getValue() { 98 | return value; 99 | } 100 | 101 | public void setValue(FormFieldValue value) { 102 | this.value = value; 103 | } 104 | 105 | public Boolean getRequired() { 106 | return required; 107 | } 108 | 109 | public void setRequired(Boolean required) { 110 | this.required = required; 111 | } 112 | 113 | public List getDropdownOptions() { 114 | return dropdownOptions; 115 | } 116 | 117 | public void setDropdownOptions(List dropdownOptions) { 118 | this.dropdownOptions = dropdownOptions; 119 | } 120 | 121 | public Boolean getExtrafield() { 122 | return extrafield; 123 | } 124 | 125 | public void setExtrafield(Boolean extrafield) { 126 | this.extrafield = extrafield; 127 | } 128 | 129 | public Boolean getReadable() { 130 | return readable; 131 | } 132 | 133 | public void setReadable(Boolean readable) { 134 | this.readable = readable; 135 | } 136 | 137 | public Boolean getWritable() { 138 | return writable; 139 | } 140 | 141 | public void setWritable(Boolean writable) { 142 | this.writable = writable; 143 | } 144 | 145 | public String getPattern() { 146 | return pattern; 147 | } 148 | 149 | public void setPattern(String pattern) { 150 | this.pattern = pattern; 151 | } 152 | 153 | public List getEnumValues() { 154 | return enumValues; 155 | } 156 | 157 | public void setEnumValues(List enumValues) { 158 | this.enumValues = enumValues; 159 | } 160 | 161 | 162 | public Integer getMin() { 163 | return min; 164 | } 165 | 166 | public void setMin(Integer min) { 167 | this.min = min; 168 | } 169 | 170 | public Integer getMax() { 171 | return max; 172 | } 173 | 174 | public void setMax(Integer max) { 175 | this.max = max; 176 | } 177 | 178 | public Integer getMaxLength() { 179 | return maxLength; 180 | } 181 | 182 | public void setMaxLength(Integer maxLength) { 183 | this.maxLength = maxLength; 184 | } 185 | 186 | public Integer getMinLength() { 187 | return minLength; 188 | } 189 | 190 | public void setMinLength(Integer minLength) { 191 | this.minLength = minLength; 192 | } 193 | 194 | public Boolean getShowAsColumn() { 195 | return showAsColumn; 196 | } 197 | 198 | public void setShowAsColumn(Boolean showAsColumn) { 199 | this.showAsColumn = showAsColumn; 200 | } 201 | 202 | public String getFileAccept() { 203 | return fileAccept; 204 | } 205 | 206 | public void setFileAccept(String fileAccept) { 207 | this.fileAccept = fileAccept; 208 | } 209 | 210 | public String getPlaceholder() { 211 | if(this.placeholder != null){ 212 | return this.placeholder; 213 | } 214 | return this.label; 215 | } 216 | 217 | public void setPlaceholder(String placeholder) { 218 | this.placeholder = placeholder; 219 | } 220 | 221 | /** 222 | * Get field name 223 | * @return field name 224 | */ 225 | public String getName(){ 226 | /*if(this.getExtrafield()){ 227 | return ExtraFieldsUtils.formatExtraFieldLabel(this.name); 228 | }*/ 229 | return this.name; 230 | } 231 | 232 | /** 233 | * Get field value 234 | * @return field value 235 | */ 236 | @JsonProperty(value = "value") 237 | public String getFieldValue() { 238 | return value == null ? null : value.getValue(); 239 | } 240 | 241 | /** 242 | * Set value 243 | * @param newValue new value 244 | */ 245 | @JsonProperty(value = "value") 246 | public void setValue(String newValue) { 247 | if (value == null) { 248 | value = new FormFieldValue(); 249 | } 250 | value.setValue(newValue); 251 | } 252 | 253 | /** 254 | * Set field value 255 | * @param fieldValue field value 256 | */ 257 | public void setFieldValue(FormFieldValue fieldValue) { 258 | this.value = fieldValue; 259 | } 260 | 261 | /** 262 | * Set field Label 263 | * @param fieldLabel field formFieldLabel 264 | */ 265 | public void setFieldLabel(FormFieldLabel fieldLabel) { 266 | this.formFieldLabel = fieldLabel; 267 | } 268 | 269 | /** 270 | * Format extra field formFieldLabel (adding "extra-" prefix) 271 | * @param label formFieldLabel to format 272 | * @return formatted formFieldLabel (with prefix "extra-") 273 | */ 274 | @JsonIgnore 275 | protected String formatExtraFieldLabel(String label){ 276 | if(label!= null && !label.contains(EXTRA_FIELD_PREFIX)){ 277 | return EXTRA_FIELD_PREFIX+label; 278 | } 279 | return label; 280 | } 281 | 282 | /** 283 | * Get title extra field 284 | * @return field title 285 | */ 286 | /*public String getLabel() { 287 | return formFieldLabel != null ? formFieldLabel.getLabel() : null; 288 | }*/ 289 | 290 | public String getLabel() { 291 | return label; 292 | } 293 | 294 | public void setLabel(String label) { 295 | this.label = label; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/utils/FormFieldDropDown.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | /** 8 | * Form field dropdown 9 | * Created by Michael DESIGAUD on 25/02/2016. 10 | */ 11 | @JsonInclude(JsonInclude.Include.NON_NULL) 12 | public class FormFieldDropDown { 13 | 14 | private Integer id; 15 | private String value; 16 | 17 | @JsonIgnore 18 | private FormFieldLabel fieldLabel; 19 | 20 | @JsonProperty(value = "label") 21 | public String getOptionTitle() { 22 | return fieldLabel != null ? fieldLabel.getLabel() : null; 23 | } 24 | 25 | @JsonProperty(value = "label") 26 | public void setOptionTitle(String optionTitle) { 27 | if (fieldLabel == null) { 28 | fieldLabel = new FormFieldLabel(); 29 | } 30 | fieldLabel.setLabel(optionTitle); 31 | } 32 | 33 | public Integer getId() { 34 | return id; 35 | } 36 | 37 | public void setId(Integer id) { 38 | this.id = id; 39 | } 40 | 41 | public String getValue() { 42 | return value; 43 | } 44 | 45 | public void setValue(String value) { 46 | this.value = value; 47 | } 48 | 49 | public FormFieldLabel getFieldLabel() { 50 | return fieldLabel; 51 | } 52 | 53 | public void setFieldLabel(FormFieldLabel fieldLabel) { 54 | this.fieldLabel = fieldLabel; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/utils/FormFieldLabel.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | /** 8 | * Form field label 9 | * Created by Michael DESIGAUD on 25/02/2016. 10 | */ 11 | @JsonInclude(JsonInclude.Include.NON_NULL) 12 | public class FormFieldLabel { 13 | 14 | @JsonProperty(value = "label") 15 | private String label; 16 | 17 | public String getLabel() { 18 | return label; 19 | } 20 | 21 | public void setLabel(String label) { 22 | this.label = label; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/utils/FormFieldValue.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.utils; 2 | 3 | /** 4 | * Form field value 5 | * Created by Michael DESIGAUD on 25/02/2016. 6 | */ 7 | public class FormFieldValue { 8 | 9 | private String value; 10 | private String defaultValue; 11 | 12 | public String getValue() { 13 | return value; 14 | } 15 | 16 | public void setValue(String value) { 17 | this.value = value; 18 | } 19 | 20 | public String getDefaultValue() { 21 | return defaultValue; 22 | } 23 | 24 | public void setDefaultValue(String defaultValue) { 25 | this.defaultValue = defaultValue; 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/fr/redfroggy/dynamicforms/utils/FormUtils.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.utils; 2 | 3 | import org.apache.commons.collections.CollectionUtils; 4 | import org.apache.commons.collections.Predicate; 5 | import org.apache.commons.lang3.ArrayUtils; 6 | import org.joda.time.DateTime; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import javax.validation.constraints.NotNull; 11 | import java.lang.annotation.Annotation; 12 | import java.lang.reflect.Field; 13 | import java.lang.reflect.Modifier; 14 | import java.lang.reflect.ParameterizedType; 15 | import java.util.*; 16 | 17 | /** 18 | * Form reflection utils 19 | * Created by Michael DESIGAUD on 25/02/2016. 20 | */ 21 | public class FormUtils { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(FormUtils.class); 24 | 25 | public static List
forms = new ArrayList<>(); 26 | 27 | public static final String TEXT_TYPE = "text"; 28 | public static final String DATE_TYPE = "date"; 29 | public static final String OBJECT_TYPE = "object"; 30 | public static final String RELATION_TYPE = "relation"; 31 | public static final String CHECKBOX_TYPE = "checkbox"; 32 | public static final String DROPDOWN_TYPE = "dropdown"; 33 | 34 | private static final Map[],String> HTML_TYPES = new HashMap(){{ 35 | put(new Class[]{String.class,Integer.class},TEXT_TYPE); 36 | put(new Class[]{Date.class,DateTime.class},DATE_TYPE); 37 | put(new Class[]{Boolean.class},CHECKBOX_TYPE); 38 | put(new Class[]{Enum.class},DROPDOWN_TYPE); 39 | }}; 40 | 41 | 42 | public static String getFieldType(Field field){ 43 | String type = null; 44 | for(Map.Entry[],String> entry : HTML_TYPES.entrySet()){ 45 | Class[] javaTypes = entry.getKey(); 46 | if(ArrayUtils.contains(javaTypes,field.getType())){ 47 | type = entry.getValue(); 48 | break; 49 | } 50 | } 51 | if(field.getType().isEnum()){ 52 | type = DROPDOWN_TYPE; 53 | } 54 | 55 | if(type == null){ 56 | type = OBJECT_TYPE; 57 | } 58 | return type; 59 | } 60 | /** 61 | * Check if the given field is required 62 | * @param formField form field 63 | * @return true if the given field is required; false otherwise 64 | */ 65 | public static Boolean isMandatory(FormField formField){ 66 | if(formField != null){ 67 | return formField.getRequired(); 68 | } 69 | return false; 70 | } 71 | 72 | /** 73 | * Check if the given field is required 74 | * @param type type 75 | * @param name field name 76 | * @return true if the given field is required; false otherwise 77 | */ 78 | /*public static Boolean isMandatory(Class type, String name){ 79 | return isMandatory(getField(type,name)); 80 | }*/ 81 | 82 | /** 83 | * Check if the field is mandatory (i.e: annotated with @NotNull or @NotEmpty or @FormRequired) 84 | * @see @javax.validation.constraints.NotNull 85 | * @see @javax.validation.constraints.NotEmpty 86 | * @param field field to check 87 | * @return true if the field is mandatory, false otherwise 88 | */ 89 | protected static Boolean isReflectFieldMandatory(Field field){ 90 | if(field != null){ 91 | for(Annotation annotation : field.getDeclaredAnnotations()){ 92 | if(annotation instanceof NotNull || 93 | annotation instanceof org.hibernate.validator.constraints.NotEmpty){ 94 | return true; 95 | } 96 | } 97 | } 98 | return false; 99 | } 100 | 101 | /** 102 | * Get all reflection fields (including parent classes fields) 103 | * @see java.lang.reflect.Field 104 | * @param type entity type 105 | * @return List of Field 106 | */ 107 | public static List getReflectionFields(Class type){ 108 | List fields = new ArrayList<>(); 109 | 110 | //Get all fields from current class and parent ones 111 | Class currentClass = type; 112 | while(!currentClass.equals(Object.class)){ 113 | CollectionUtils.addAll(fields, currentClass.getDeclaredFields()); 114 | currentClass = currentClass.getSuperclass(); 115 | } 116 | return fields; 117 | } 118 | 119 | /** 120 | * Get field by name 121 | * Lookup on parent class also 122 | * @param entityType current class to look into 123 | * @param name name of the field to find 124 | * @return Field if found or null otherwise 125 | */ 126 | public static Field getField(Class entityType,String name){ 127 | List fields = getReflectionFields(entityType); 128 | if(fields != null){ 129 | for(Field field : fields){ 130 | if(field.getName().equals(name)){ 131 | return field; 132 | } 133 | } 134 | } 135 | return null; 136 | } 137 | 138 | /** 139 | * Get fields 140 | * @param type dto type 141 | * @param onlyExtraFields include only extra fields or no 142 | * @return List of FormField 143 | */ 144 | public static List getFields(Class type,Boolean onlyExtraFields){ 145 | 146 | Boolean useOnlyExtraFields = onlyExtraFields; 147 | if(type == null){ 148 | return new ArrayList<>(); 149 | } 150 | if(useOnlyExtraFields == null){ 151 | useOnlyExtraFields = false; 152 | } 153 | Set formFields = new HashSet<>(); 154 | 155 | if(!useOnlyExtraFields) { 156 | for (Field field : getReflectionFields(type)) { 157 | log.info("Reflect field: {}", field); 158 | //Ignore static field and make sure the user has the authority to read field 159 | // && (fieldList == null || fieldList.contains(field.getName())) 160 | if (!Modifier.isStatic(field.getModifiers())) { 161 | final FormField formField = new FormField(); 162 | formField.setExtrafield(false); 163 | formField.setEntityName(type.getSimpleName()); 164 | formField.setRequired(isReflectFieldMandatory(field)); 165 | formField.setName(field.getName()); 166 | formField.setType(getFieldType(field)); 167 | formField.setLabel(null); 168 | if (DROPDOWN_TYPE.equals(formField.getType())) { 169 | formField.setEnumValues(new ArrayList()); 170 | for (Object enumValue : field.getType().getEnumConstants()) { 171 | formField.getEnumValues().add(((Enum) enumValue).name()); 172 | } 173 | } 174 | formField.setReadable(true); 175 | formField.setWritable(true); 176 | 177 | Boolean existingField = CollectionUtils.exists(formFields, new Predicate() { 178 | @Override 179 | public boolean evaluate(Object object) { 180 | return ((FormField) object).getName().equals(formField.getName()); 181 | } 182 | }); 183 | if (!existingField) { 184 | formFields.add(formField); 185 | } 186 | } 187 | } 188 | } 189 | Form form = getFormByType(type); 190 | if(form != null){ 191 | formFields.addAll(form.getFields()); 192 | } 193 | return new ArrayList<>(formFields); 194 | } 195 | 196 | public static Form getFormByType(final Class entityType){ 197 | if(!forms.isEmpty()){ 198 | return (Form) CollectionUtils.find(forms, new Predicate() { 199 | @Override 200 | public boolean evaluate(Object o) { 201 | Form form = (Form) o; 202 | return form != null && form.getEntityName().toLowerCase().equals(entityType.getSimpleName().toLowerCase()); 203 | } 204 | }); 205 | } 206 | return null; 207 | } 208 | 209 | /** 210 | * Describe entity 211 | * @param type dto type 212 | * @param onlyExtraFields include only extra fields or no 213 | * @return ExtraFieldEntity 214 | */ 215 | public static Form describe(Class type,Boolean onlyExtraFields){ 216 | if(type != null) { 217 | Form form = new Form(); 218 | form.setEntityName(type.getSimpleName()); 219 | form.setFields(getFields(type,onlyExtraFields)); 220 | return form; 221 | } 222 | return null; 223 | } 224 | 225 | /** 226 | * Get list generic type 227 | * @param field current field 228 | * @return List generic type 229 | */ 230 | public static Class getJavaType(Field field){ 231 | if(field.getGenericType() instanceof ParameterizedType){ 232 | ParameterizedType pt = (ParameterizedType) field.getGenericType(); 233 | return (Class) pt.getActualTypeArguments()[0]; 234 | } 235 | return field.getType(); 236 | } 237 | 238 | /** 239 | * Check if the field is a collection type 240 | * @param field field to check 241 | * @return true if the field is assignable from Collection 242 | */ 243 | public static Boolean isCollection(Field field){ 244 | return Collection.class.isAssignableFrom(field.getType()); 245 | } 246 | } -------------------------------------------------------------------------------- /src/main/resources/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "entityName":"Customer", 3 | "version":1, 4 | "fields":[ 5 | { 6 | "id":1, 7 | "type":"email", 8 | "name":"email", 9 | "value":"defaultemail@redfroggy.fr", 10 | "label":"Email", 11 | "required":true, 12 | "showAsColumn":true 13 | }, 14 | { 15 | "id":2, 16 | "type":"number", 17 | "name":"age", 18 | "value":"28", 19 | "label":"Age", 20 | "required":true, 21 | "min":5, 22 | "max":100 23 | }, 24 | { 25 | "id":3, 26 | "type":"text", 27 | "name":"company", 28 | "label":"Company", 29 | "required":false, 30 | "minLength":3, 31 | "maxLength":10, 32 | "showAsColumn":true 33 | }, 34 | { 35 | "id":4, 36 | "type":"textarea", 37 | "name":"description", 38 | "label":"Description", 39 | "required":true 40 | }, 41 | { 42 | "id":5, 43 | "type":"file", 44 | "name":"attachment", 45 | "label":"Attachment", 46 | "fileAccept":"application/pdf" 47 | }, 48 | { 49 | "id":6, 50 | "type":"password", 51 | "name":"password", 52 | "label":"Password", 53 | "placeholder":"Ex: myp4ssw0rd", 54 | "pattern":"^[a-z0-9_-]{6,18}$" 55 | }, 56 | { 57 | "id":7, 58 | "type":"select", 59 | "name":"roles", 60 | "label":"Roles", 61 | "required":true, 62 | "options":[ 63 | { 64 | "id":1, 65 | "value":"Admin" 66 | }, 67 | { 68 | "id":2, 69 | "value":"Manager" 70 | }, 71 | { 72 | "id":1, 73 | "value":"User" 74 | } 75 | ] 76 | }, 77 | { 78 | "id":8, 79 | "type":"text", 80 | "name":"readable", 81 | "label":"Readable field", 82 | "value":"Readable value", 83 | "writable":false 84 | }, 85 | { 86 | "id":9, 87 | "type":"date", 88 | "name":"birthDate", 89 | "label":"Birth date", 90 | "required":true 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /src/main/resources/static/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | 4 | 5 | @Component({ 6 | selector: 'dynamic-app', 7 | templateUrl:'./app/app.html' 8 | }) 9 | export class AppComponent { 10 | constructor(private router:Router) {} 11 | ngOnInit() { 12 | this.router.navigate(['/customers']); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/static/app/app.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
-------------------------------------------------------------------------------- /src/main/resources/static/app/app.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Application module 3 | * Created by Michael DESIGAUD on 10/11/2016. 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { HttpModule } from '@angular/http'; 7 | import { RouterModule } from '@angular/router'; 8 | import {LocationStrategy, HashLocationStrategy} from '@angular/common'; 9 | import { BrowserModule } from '@angular/platform-browser'; 10 | 11 | import {AppComponent} from './app.component'; 12 | 13 | import {CustomerModule} from './customer/customer.module'; 14 | import {CustomersModule} from './customers/customers.module'; 15 | import {RoutesModule} from './app.routes'; 16 | import {HeaderModule} from './header/header.module'; 17 | 18 | @NgModule({ 19 | imports: [ HttpModule, RouterModule, BrowserModule, CustomerModule, CustomersModule, RoutesModule, HeaderModule ], 20 | declarations: [ AppComponent ], 21 | bootstrap: [ AppComponent ], 22 | providers: [ {provide: LocationStrategy, useClass: HashLocationStrategy} ] 23 | }) 24 | export class AppModule { } 25 | -------------------------------------------------------------------------------- /src/main/resources/static/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Application routes 3 | * Created by Michael DESIGAUD on 11/11/2016. 4 | */ 5 | 6 | import {Routes, RouterModule} from '@angular/router'; 7 | import {ModuleWithProviders} from '@angular/core'; 8 | 9 | import {Customer} from './customer/customer'; 10 | import {Customers} from './customers/customers'; 11 | 12 | const routes: Routes = [ 13 | {path: 'customers', component: Customers}, 14 | {path: 'customer/create', component: Customer}, 15 | {path: 'customer/:id', component: Customer}, 16 | {path: '', component: Customers}, 17 | ]; 18 | 19 | export const RoutesModule: ModuleWithProviders = RouterModule.forRoot(routes); 20 | -------------------------------------------------------------------------------- /src/main/resources/static/app/customer/customer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | General Information 7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 |
19 | Additional Information (Extra fields) 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 |
29 |
-------------------------------------------------------------------------------- /src/main/resources/static/app/customer/customer.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Customer module 3 | * Created by Michael DESIGAUD on 11/11/2016. 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { FormModule } from '../form/form.module'; 7 | 8 | import {Customer} from './customer'; 9 | 10 | @NgModule({ 11 | imports: [ FormModule ], 12 | declarations: [ Customer ], 13 | bootstrap: [ Customer ] 14 | }) 15 | export class CustomerModule { } 16 | -------------------------------------------------------------------------------- /src/main/resources/static/app/customer/customer.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Router, ActivatedRoute} from '@angular/router'; 3 | import {Http,Headers,RequestOptionsArgs} from '@angular/http'; 4 | import {FormBuilder, Validators, FormGroup, FormControl} from '@angular/forms'; 5 | 6 | 7 | @Component({ 8 | selector: 'customer', 9 | templateUrl: './app/customer/customer.html' 10 | }) 11 | export class Customer { 12 | customerGroup:FormGroup; 13 | customer:any; 14 | customerPromise:Promise; 15 | isEdition:boolean; 16 | nbErrors:number; 17 | private sub:any; 18 | constructor(private route:ActivatedRoute, private http:Http, private router:Router, private form: FormBuilder) { 19 | this.customer = {}; 20 | this.nbErrors = 0; 21 | 22 | this.customerGroup = form.group({ 23 | firstName: new FormControl('', [Validators.required]), 24 | lastName: new FormControl('', [Validators.required]) 25 | }); 26 | 27 | this.customerGroup.valueChanges.subscribe(() => { 28 | if(this.customerGroup.errors) { 29 | this.nbErrors = Object.keys(this.customerGroup.errors).length; 30 | } 31 | }); 32 | } 33 | ngOnInit() { 34 | this.route.params 35 | .forEach(params => { 36 | this.isEdition = !!params['id']; 37 | 38 | if(this.isEdition) { 39 | this.getCustomer(params['id']); 40 | } else { 41 | this.customer.extraFields = {}; 42 | this.customerPromise = Promise.resolve(this.customer); 43 | } 44 | }); 45 | } 46 | getCustomer(id:string):void { 47 | this.customerPromise = this.http.get('http://localhost:8080/api/customers/'+id) 48 | .map(res => res.json()).toPromise(); 49 | 50 | this.customerPromise.then((customer:any) => { 51 | this.customer = customer; 52 | return customer; 53 | }); 54 | } 55 | cancel():void { 56 | this.router.navigate(['/customers']); 57 | } 58 | saveCustomer():void { 59 | let headers = new Headers(); 60 | headers.append('Content-Type','application/json'); 61 | 62 | let reqOptions:RequestOptionsArgs = {}; 63 | reqOptions.body = JSON.stringify(this.customer); 64 | reqOptions.headers = headers; 65 | this.isEdition ? reqOptions.method ='PUT' : reqOptions.method ='POST'; 66 | 67 | this.http.request('http://localhost:8080/api/customers',reqOptions) 68 | .map(res => res.json()) 69 | .subscribe(() => this.router.navigate(['/customers'])); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/resources/static/app/customers/customers.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Customer list

6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 35 | 36 |
#First nameLast nameExtra fields
{{customer.id}}{{customer.firstName}}{{customer.lastName}} 23 |
24 | 25 | {{extraField.key}}: {{extraField.value}} 26 | 27 | 28 | {{extraField.key}} 29 | 30 | 31 | {{extraField.key}}: 32 | 33 |
34 |
37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /src/main/resources/static/app/customers/customers.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Customers module 3 | * Created by Michael DESIGAUD on 11/11/2016. 4 | */ 5 | 6 | import { NgModule } from '@angular/core'; 7 | import { FormModule } from '../form/form.module'; 8 | 9 | import {Customers} from './customers'; 10 | 11 | @NgModule({ 12 | imports: [ FormModule ], 13 | declarations: [ Customers ], 14 | bootstrap: [ Customers ] 15 | }) 16 | export class CustomersModule { } 17 | -------------------------------------------------------------------------------- /src/main/resources/static/app/customers/customers.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | import {Http} from '@angular/http'; 4 | 5 | @Component({ 6 | selector: 'customers', 7 | templateUrl: './app/customers/customers.html' 8 | }) 9 | export class Customers { 10 | customers:Array; 11 | constructor(private http:Http,private router: Router) { 12 | http.get('http://localhost:8080/api/customers').map(res => res.json()).subscribe((customers:Array) => this.customers = customers); 13 | } 14 | onSelectCustomer(event:Event,id:number):void { 15 | event.preventDefault(); 16 | this.router.navigate(['/customer',id]); 17 | } 18 | addCustomer():void { 19 | this.router.navigate(['/customer/create']); 20 | } 21 | isTypeImage(extraField:any):boolean { 22 | return extraField && extraField.value.indexOf('data:image') !== -1; 23 | } 24 | isTypeApplication(extraField:any):boolean { 25 | return extraField && extraField.value.indexOf('data:application') !== -1; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/error/control-messages.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Input,Component,Inject} from '@angular/core'; 3 | import {NgForm,AbstractControl} from '@angular/forms'; 4 | import {ValidatorService} from '../validators/validator.service'; 5 | 6 | @Component({ 7 | selector:'error-messages', 8 | template: `
{{error}} 
` 9 | }) 10 | export class ControlMessages { 11 | @Input('control') controlName: string; 12 | constructor(@Inject(NgForm) private formDir: NgForm) {} 13 | get errors() { 14 | let c:AbstractControl = this.formDir.form.get(this.controlName); 15 | if(c) { 16 | for (let propertyName in c.errors) { 17 | if (c.errors.hasOwnProperty(propertyName)) { 18 | return ValidatorService.getValidatorErrorMessage(c); 19 | } 20 | } 21 | } 22 | return []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/extra-form.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input,ComponentFactoryResolver,ComponentRef,Type,ViewChild,ViewContainerRef} from '@angular/core'; 2 | import {Http} from '@angular/http'; 3 | import {InputExtraField} from './fields/input-extra-field'; 4 | import {ExtraForm,ExtraFormField} from './model/form'; 5 | import {TextAreaExtraField} from './fields/textarea-extra-field'; 6 | import {FileInputExtraField} from './fields/file-input-extra-field'; 7 | import {SelectExtraField} from './fields/select-extra-field'; 8 | import {DateInputExtraField} from './fields/date-input-extra-field'; 9 | import {FormGroup} from "@angular/forms"; 10 | 11 | @Component({ 12 | selector: 'extra-form', 13 | template:'
' 14 | }) 15 | export class DynamicForm { 16 | @Input('entity') entityPromise:Promise<{extraFields:any}>; 17 | @Input('formGroup') formGroup:FormGroup; 18 | @ViewChild('extraField', {read: ViewContainerRef}) extraFieldRef:ViewContainerRef; 19 | form:ExtraForm; 20 | onlyExtraFields:boolean = true; 21 | constructor(private http:Http,private componentFactoryResolver: ComponentFactoryResolver) { 22 | this.form = new ExtraForm(); 23 | } 24 | ngOnInit():void { 25 | 26 | let formPromise:Promise<{entityName:string,version:number,fields:Array}> = this.http.get('http://localhost:8080/api/customers/form?onlyExtraFields='+this.onlyExtraFields) 27 | .map(res => res.json()) 28 | .toPromise(); 29 | 30 | Promise.all([this.entityPromise,formPromise]).then((values) => { 31 | let entity:{extraFields:any} = values[0]; 32 | 33 | this.form = new ExtraForm(values[1]); 34 | 35 | if(!entity.extraFields) { 36 | entity.extraFields = {}; 37 | } 38 | this.form.fields.forEach((field:ExtraFormField) => { 39 | let type:Type; 40 | if(field.isInput()) { 41 | type = InputExtraField; 42 | } 43 | if(field.isTypeTextArea()) { 44 | type = TextAreaExtraField; 45 | } 46 | if(field.isTypeFile()) { 47 | type = FileInputExtraField; 48 | } 49 | if(field.isTypeSelect()) { 50 | type = SelectExtraField; 51 | } 52 | if(field.isTypeDate()) { 53 | type = DateInputExtraField; 54 | } 55 | 56 | let factory = this.componentFactoryResolver.resolveComponentFactory(type); 57 | let componentRef:ComponentRef = this.extraFieldRef.createComponent(factory); 58 | componentRef.instance.entity = entity; 59 | componentRef.instance.field = field; 60 | componentRef.instance.formGroup = this.formGroup; 61 | }); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/fields/date-input-extra-field.ts: -------------------------------------------------------------------------------- 1 | import {Component,Inject,Input} from '@angular/core'; 2 | import {NgForm, FormGroup} from '@angular/forms'; 3 | import {ExtraFormField} from '../model/form'; 4 | import {ExtraField} from './extra-field'; 5 | 6 | @Component({ 7 | selector: 'date-input-extra-field', 8 | template:` 9 |
10 | 11 | 13 | 14 |
15 | ` 16 | }) 17 | export class DateInputExtraField extends ExtraField { 18 | @Input() field:ExtraFormField; 19 | @Input() entity:{extraFields:Object}; 20 | @Input() formGroup:FormGroup; 21 | constructor(@Inject(NgForm) formDir: NgForm) { 22 | super(formDir); 23 | } 24 | get disabled():string { 25 | if(this.field && !this.field.writable) { 26 | return 'disabled'; 27 | } 28 | return null; 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/resources/static/app/form/fields/extra-field.ts: -------------------------------------------------------------------------------- 1 | import {NgForm,FormControl} from '@angular/forms'; 2 | import {ExtraFormField} from '../model/form'; 3 | 4 | export abstract class ExtraField { 5 | field:ExtraFormField; 6 | entity:{extraFields:Object}; 7 | fieldControl:FormControl; 8 | constructor(private formDir: NgForm) {} 9 | ngOnInit():void { 10 | this.fieldControl = this.field.getControl(); 11 | setTimeout(()=> { 12 | this.formDir.form.addControl(this.field.name,this.fieldControl); 13 | 14 | let value:any = ''; 15 | if(this.entity && this.entity.extraFields && this.entity.extraFields[this.field.name]) { 16 | value = this.entity.extraFields[this.field.name]; 17 | } else if(this.field.hasValue()) { 18 | value = this.field.value; 19 | this.entity.extraFields[this.field.name] = value; 20 | } 21 | if(!this.field.isTypeFile()) { 22 | this.fieldControl.patchValue(value); 23 | } 24 | }, 0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/fields/file-input-extra-field.ts: -------------------------------------------------------------------------------- 1 | import {Component,Inject,Input} from '@angular/core'; 2 | import {NgForm} from '@angular/forms'; 3 | import {ExtraFormField} from '../model/form'; 4 | import {ExtraField} from './extra-field'; 5 | 6 | @Component({ 7 | selector: 'file-input-extra-field', 8 | template:` 9 |
10 | 11 | 15 | 16 |
17 | ` 18 | }) 19 | export class FileInputExtraField extends ExtraField { 20 | @Input() field:ExtraFormField; 21 | @Input() entity:{extraFields:Object}; 22 | constructor(@Inject(NgForm) formDir: NgForm) { 23 | super(formDir); 24 | } 25 | onChange(event:{preventDefault:Function,target:{files:Array}}) { 26 | event.preventDefault(); 27 | if(this.field.isTypeFile()) { 28 | let file:File = event.target.files[0]; 29 | let fileReader:FileReader = new FileReader(); 30 | fileReader.readAsDataURL(file); 31 | 32 | fileReader.onloadend = () => { 33 | this.entity.extraFields[this.field.name] = fileReader.result; 34 | }; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/fields/input-extra-field.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core'; 2 | import {NgForm, FormGroup} from '@angular/forms'; 3 | import {ExtraFormField} from '../model/form'; 4 | import {ExtraField} from './extra-field'; 5 | 6 | @Component({ 7 | selector: 'input-extra-field', 8 | template:` 9 |
10 | 11 | 15 | 16 |
17 | ` 18 | }) 19 | export class InputExtraField extends ExtraField { 20 | @Input() field:ExtraFormField; 21 | @Input() entity:{extraFields:Object}; 22 | @Input() formGroup:FormGroup; 23 | constructor(formDir: NgForm) { 24 | super(formDir); 25 | } 26 | get disabled():string { 27 | if(this.field && !this.field.writable) { 28 | return 'disabled'; 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/fields/select-extra-field.ts: -------------------------------------------------------------------------------- 1 | import {Component,Inject,Input} from '@angular/core'; 2 | import {NgForm, FormGroup} from '@angular/forms'; 3 | import {ExtraFormField} from '../model/form'; 4 | import {ExtraField} from './extra-field'; 5 | 6 | @Component({ 7 | selector: 'select-extra-field', 8 | template:` 9 |
10 | 11 | 15 | 16 |
17 | ` 18 | }) 19 | export class SelectExtraField extends ExtraField { 20 | @Input() field:ExtraFormField; 21 | @Input() entity:{extraFields:Object}; 22 | @Input() formGroup:FormGroup; 23 | constructor(@Inject(NgForm) formDir: NgForm) { 24 | super(formDir); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/fields/textarea-extra-field.ts: -------------------------------------------------------------------------------- 1 | import {Component,Inject,Input} from '@angular/core'; 2 | import {NgForm, FormGroup} from '@angular/forms'; 3 | import {ExtraFormField} from '../model/form'; 4 | import {ExtraField} from './extra-field'; 5 | 6 | @Component({ 7 | selector: 'textarea-extra-field', 8 | template:` 9 |
10 | 11 | 15 | 16 |
17 | ` 18 | }) 19 | export class TextAreaExtraField extends ExtraField { 20 | @Input() field:ExtraFormField; 21 | @Input() entity:{extraFields:Object}; 22 | @Input() formGroup:FormGroup; 23 | constructor(@Inject(NgForm) formDir: NgForm) { 24 | super(formDir); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/form.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael DESIGAUD on 11/11/2016. 3 | */ 4 | import { NgModule } from '@angular/core'; 5 | import { FormsModule,ReactiveFormsModule} from '@angular/forms'; 6 | import { BrowserModule } from '@angular/platform-browser'; 7 | 8 | import {DateInputExtraField} from './fields/date-input-extra-field'; 9 | import {FileInputExtraField} from './fields/file-input-extra-field'; 10 | import {InputExtraField} from './fields/input-extra-field'; 11 | import {SelectExtraField} from './fields/select-extra-field'; 12 | import {TextAreaExtraField} from './fields/textarea-extra-field'; 13 | import {DynamicForm} from './extra-form'; 14 | import {ControlMessages} from './error/control-messages'; 15 | import {PipesModule} from '../pipes/pipes.module'; 16 | 17 | @NgModule({ 18 | imports: [ BrowserModule, FormsModule, ReactiveFormsModule, PipesModule ], 19 | declarations: [ 20 | DateInputExtraField, FileInputExtraField, 21 | InputExtraField, SelectExtraField, TextAreaExtraField, DynamicForm, ControlMessages ], 22 | entryComponents: [ DateInputExtraField, FileInputExtraField, 23 | InputExtraField, SelectExtraField, TextAreaExtraField ], 24 | bootstrap: [ 25 | DynamicForm, ControlMessages ], 26 | exports: [ BrowserModule, FormsModule, ReactiveFormsModule, ControlMessages, DynamicForm, PipesModule ] 27 | }) 28 | export class FormModule { } 29 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/model/form.ts: -------------------------------------------------------------------------------- 1 | import {Validators, FormControl, ValidatorFn} from '@angular/forms'; 2 | import {ValidatorService} from '../validators/validator.service'; 3 | 4 | /// 5 | 6 | const TYPE_EMAIL:string = 'email'; 7 | const TYPE_NUMBER:string = 'number'; 8 | const TYPE_TEXT:string = 'text'; 9 | const TYPE_TEXTAREA:string = 'textarea'; 10 | const TYPE_FILE:string = 'file'; 11 | const TYPE_PASSWORD:string = 'password'; 12 | const TYPE_SELECT:string = 'select'; 13 | const TYPE_DATE:string = 'date'; 14 | 15 | export class ExtraFormField { 16 | id:number; 17 | entityName:string; 18 | type:string; 19 | name:string; 20 | label:string; 21 | required:boolean; 22 | extrafield:boolean; 23 | readable:boolean; 24 | writable:boolean; 25 | pattern:string; 26 | enumValues:Array; 27 | value:string; 28 | min:number; 29 | max:number; 30 | minLength:number; 31 | maxLength:number; 32 | showAsColumn:boolean; 33 | fileAccept:string; 34 | options:Array<{id:number,value:string}>; 35 | private validators:ValidatorFn; 36 | private control:FormControl; 37 | constructor(_field?:any) { 38 | _.assignIn(this,_field); 39 | this.initValidators(); 40 | } 41 | hasValue():boolean { 42 | return !!this.value; 43 | } 44 | isType(type:string):boolean { 45 | return type === this.type; 46 | } 47 | isTypeEmail():boolean { 48 | return this.isType(TYPE_EMAIL); 49 | } 50 | isTypeNumber():boolean { 51 | return this.isType(TYPE_NUMBER); 52 | } 53 | isTypeText():boolean { 54 | return this.isType(TYPE_TEXT); 55 | } 56 | isTypeFile():boolean { 57 | return this.isType(TYPE_FILE); 58 | } 59 | isInput():boolean { 60 | return this.isTypeText() 61 | || this.isTypeEmail() 62 | || this.isTypeNumber() 63 | || this.isTypePassword(); 64 | } 65 | isTypeTextArea():boolean { 66 | return this.isType(TYPE_TEXTAREA); 67 | } 68 | isTypePassword():boolean { 69 | return this.isType(TYPE_PASSWORD); 70 | } 71 | isTypeSelect():boolean { 72 | return this.isType(TYPE_SELECT); 73 | } 74 | isTypeDate():boolean { 75 | return this.isType(TYPE_DATE); 76 | } 77 | getControl():FormControl { 78 | if(!this.control) { 79 | this.control = new FormControl(this.name,this.validators); 80 | } 81 | return this.control; 82 | } 83 | private initValidators():void { 84 | console.log('Adding validators to control ',this.name); 85 | let validators:Array = []; 86 | if(this.required) { 87 | validators.push(Validators.required); 88 | } 89 | if(this.isTypeText() && this.minLength) { 90 | console.log('Adding minLength validator',this.minLength); 91 | validators.push(Validators.minLength(this.minLength)); 92 | } 93 | if(this.isTypeText() && this.maxLength) { 94 | console.log('Adding maxLength validator',this.maxLength); 95 | validators.push(Validators.maxLength(this.maxLength)); 96 | } 97 | if(this.isTypeEmail()) { 98 | validators.push(ValidatorService.emailValidator); 99 | } 100 | if(this.isTypeNumber()) { 101 | validators.push(ValidatorService.numberValidator); 102 | } 103 | if(this.isInput() && this.pattern) { 104 | validators.push(ValidatorService.regexValidator(this.pattern)); 105 | } 106 | console.log(validators.length+' validators added to control',this.name); 107 | if(validators.length > 0) { 108 | this.validators = Validators.compose(validators); 109 | } 110 | } 111 | } 112 | 113 | 114 | export class ExtraForm { 115 | entityName:string; 116 | version:number; 117 | fields:Array; 118 | constructor(_form?:{entityName:string,version:number,fields:Array}) { 119 | if(_form && _form.fields) { 120 | this.entityName = _form.entityName; 121 | this.version = _form.version; 122 | let fields:Array = []; 123 | _form.fields.forEach((field)=> { 124 | fields.push(new ExtraFormField((field))); 125 | }); 126 | this.fields = fields; 127 | } 128 | 129 | 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/resources/static/app/form/validators/validator.service.ts: -------------------------------------------------------------------------------- 1 | import {AbstractControl,FormControl} from '@angular/forms'; 2 | 3 | /// 4 | 5 | const EMAIL_REGEX:RegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/; 6 | 7 | export class ValidatorService { 8 | 9 | static getValidatorErrorMessage(control:AbstractControl):Array { 10 | let errors:Array = []; 11 | if(control.hasError('required')) { 12 | errors.push('This field is required'); 13 | } 14 | if(control.hasError('invalidEmailAddress')) { 15 | errors.push('Invalid email address'); 16 | } 17 | if(control.hasError('invalidNumber')) { 18 | errors.push('Must be a number'); 19 | } 20 | if(control.hasError('minlength')) { 21 | let error:{requiredLength:number,actualLength:number} = control.getError('minlength'); 22 | errors.push('At least '+error.requiredLength+' characters minimum, actual: '+error.actualLength); 23 | } 24 | if(control.hasError('pattern')) { 25 | let error:{regex:string} = control.getError('pattern'); 26 | errors.push('Invalid pattern, must match: '+error.regex); 27 | } 28 | return errors; 29 | } 30 | 31 | static emailValidator(control:FormControl):Object { 32 | if (control.value && control.value.match(EMAIL_REGEX)) { 33 | return null; 34 | } 35 | return { 'invalidEmailAddress': true }; 36 | } 37 | 38 | static numberValidator(control:FormControl):Object { 39 | if(control.value && !isNaN(control.value)) { 40 | return null; 41 | } 42 | return { 'invalidNumber': true }; 43 | } 44 | 45 | static regexValidator(pattern: string): Function { 46 | return (control: FormControl): {[key: string]: any} => { 47 | return control.value && control.value.match(pattern) ? null : {pattern: {regex:pattern}}; 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/static/app/header/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/app/header/header.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael DESIGAUD on 11/11/2016. 3 | */ 4 | 5 | import { NgModule } from '@angular/core'; 6 | 7 | import {Header} from './header'; 8 | 9 | @NgModule({ 10 | declarations: [ Header ], 11 | bootstrap: [ Header ] 12 | }) 13 | export class HeaderModule { } 14 | -------------------------------------------------------------------------------- /src/main/resources/static/app/header/header.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'header', 5 | templateUrl: './app/header/header.html' 6 | }) 7 | export class Header { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/static/app/main.ts: -------------------------------------------------------------------------------- 1 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './app.module'; 3 | import 'rxjs/add/operator/map'; 4 | import 'rxjs/add/observable/timer'; 5 | import 'rxjs/add/operator/toPromise'; 6 | 7 | const platform = platformBrowserDynamic(); 8 | platform.bootstrapModule(AppModule); 9 | -------------------------------------------------------------------------------- /src/main/resources/static/app/pipes/pipes.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pipes module 3 | * Created by Michael DESIGAUD on 11/11/2016. 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | 7 | import {ValuesPipe} from './values.pipe'; 8 | 9 | @NgModule({ 10 | declarations: [ ValuesPipe ], 11 | exports: [ValuesPipe] 12 | }) 13 | export class PipesModule { } 14 | -------------------------------------------------------------------------------- /src/main/resources/static/app/pipes/values.pipe.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform,Pipe} from '@angular/core'; 2 | 3 | @Pipe({ name: 'values', pure: false }) 4 | export class ValuesPipe implements PipeTransform { 5 | transform(value: any, args: any[] = null): any { 6 | return value ? Object.keys(value).map((key) => {return {value:value[key],key:key};}) : value; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/static/assets/img/red_froggy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedFroggy/angular-spring-dynamic-form/314e94fa86587092780f114d9a671d9b3e86b4b2/src/main/resources/static/assets/img/red_froggy.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/styles/style.css: -------------------------------------------------------------------------------- 1 | a.navbar-brand { 2 | padding-top: 7px; 3 | } 4 | 5 | .panel-heading span.toolbar { 6 | float: right !important; 7 | margin-top: -24px; 8 | font-size: 15px; 9 | margin-right: -12px; 10 | } -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedFroggy/angular-spring-dynamic-form/314e94fa86587092780f114d9a671d9b3e86b4b2/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular 2 Dynamic form 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/static/init.js: -------------------------------------------------------------------------------- 1 | System.import('app').catch(function(err){ console.error(err); }); -------------------------------------------------------------------------------- /src/main/resources/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "springDynamicForm", 3 | "version": "1.0.0", 4 | "description": "Dynamic form creation with Angular 2 and Spring", 5 | "author": "Michael DESIGAUD ", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "typescript": "^2.1.4", 9 | "es6-promise": "^4.0.5", 10 | "es6-shim": "^0.35.0" 11 | }, 12 | "engines": { 13 | "node": ">= 4.2.1", 14 | "npm": ">= 3" 15 | }, 16 | "keywords": [ 17 | "angular2", 18 | "form", 19 | "spring" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/RedFroggy/angular-spring-dynamic-form.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/RedFroggy/angular-spring-dynamic-form/issues" 27 | }, 28 | "bin":{ 29 | "ntsc":"node_modules/ntypescript/bin/ntypescript.js" 30 | }, 31 | "scripts": { 32 | "build": "ntsc -p ../../../../" 33 | }, 34 | "dependencies": { 35 | "@angular/common": "2.4.1", 36 | "@angular/compiler": "2.4.1", 37 | "@angular/core": "2.4.1", 38 | "@angular/forms": "2.4.1", 39 | "@angular/http": "2.4.1", 40 | "@angular/platform-browser": "2.4.1", 41 | "@angular/platform-browser-dynamic": "2.4.1", 42 | "@angular/platform-server": "2.4.1", 43 | "@angular/router": "3.4.1", 44 | "@angular/upgrade": "2.4.1", 45 | "bootstrap": "3.3.7", 46 | "core-js": "2.4.1", 47 | "font-awesome": "4.7.0", 48 | "jquery": "3.1.1", 49 | "lodash": "^4.17.1", 50 | "ntypescript": "1.201609302242.1", 51 | "reflect-metadata": "0.1.9", 52 | "rxjs": "5.0.2", 53 | "systemjs": "0.19.41", 54 | "zone.js": "0.7.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/static/systemjs.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System configuration for Angular samples 3 | * Adjust as necessary for your application needs. 4 | */ 5 | (function (global) { 6 | System.config({ 7 | paths: { 8 | // paths serve as alias 9 | 'npm:': 'node_modules/' 10 | }, 11 | // map tells the System loader where to look for things 12 | map: { 13 | // our app is within the app folder 14 | app: 'app', 15 | // angular bundles 16 | '@angular/core': 'node_modules/@angular/core/bundles/core.umd.js', 17 | '@angular/common': 'node_modules/@angular/common/bundles/common.umd.js', 18 | '@angular/compiler': 'node_modules/@angular/compiler/bundles/compiler.umd.js', 19 | '@angular/platform-browser': 'node_modules/@angular/platform-browser/bundles/platform-browser.umd.js', 20 | '@angular/platform-browser-dynamic': 'node_modules/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 21 | '@angular/http': 'node_modules/@angular/http/bundles/http.umd.js', 22 | '@angular/router': 'node_modules/@angular/router/bundles/router.umd.js', 23 | '@angular/forms': 'node_modules/@angular/forms/bundles/forms.umd.js', 24 | '@angular/upgrade': 'node_modules/@angular/upgrade/bundles/upgrade.umd.js', 25 | // other libraries 26 | 'rxjs': 'node_modules/rxjs' 27 | }, 28 | // packages tells the System loader how to load when no filename and/or no extension 29 | packages: { 30 | app: { 31 | main: './main.js', 32 | defaultExtension: 'js' 33 | }, 34 | rxjs: { 35 | defaultExtension: 'js' 36 | } 37 | } 38 | }); 39 | })(this); 40 | -------------------------------------------------------------------------------- /src/test/java/fr/redfroggy/dynamicforms/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms; 2 | 3 | /** 4 | * Application Test 5 | * Created by Michael DESIGAUD on 04/03/2016. 6 | */ 7 | public class ApplicationTest extends Application{ 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/fr/redfroggy/dynamicforms/TestUtil.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import java.io.IOException; 7 | import java.util.List; 8 | 9 | /** 10 | * Test utility class 11 | * Created by Michael DESIGAUD on 04/03/2016. 12 | */ 13 | public class TestUtil { 14 | 15 | private static ObjectMapper objectMapper = new ObjectMapper(); 16 | 17 | public static String toJSON(Object obj) throws JsonProcessingException { 18 | return objectMapper.writeValueAsString(obj); 19 | } 20 | 21 | public static Object fromJSON(String json,Class type,Boolean constructCollectionType) throws IOException { 22 | if(constructCollectionType == null){ 23 | constructCollectionType = false; 24 | } 25 | if(constructCollectionType) { 26 | return objectMapper.readValue(json,objectMapper.getTypeFactory().constructCollectionType(List.class,type)); 27 | } 28 | return objectMapper.readValue(json,type); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/fr/redfroggy/dynamicforms/configuration/ExtraFieldConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.configuration; 2 | 3 | import fr.redfroggy.dynamicforms.TestUtil; 4 | import fr.redfroggy.dynamicforms.model.Customer; 5 | import fr.redfroggy.dynamicforms.utils.Form; 6 | import fr.redfroggy.dynamicforms.utils.FormField; 7 | import fr.redfroggy.dynamicforms.utils.FormUtils; 8 | import org.junit.Assert; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.mockito.Mockito; 15 | import org.mockito.runners.MockitoJUnitRunner; 16 | import org.springframework.context.ApplicationContext; 17 | import org.springframework.core.io.Resource; 18 | 19 | import java.io.ByteArrayInputStream; 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.lang.reflect.Field; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | /** 28 | * ExtraFieldConfiguration tests 29 | * Created by Michael DESIGAUD on 04/03/2016. 30 | */ 31 | @RunWith(MockitoJUnitRunner.class) 32 | public class ExtraFieldConfigurationTest { 33 | 34 | @InjectMocks 35 | private ExtraFieldConfiguration reader = new ExtraFieldConfiguration(); 36 | 37 | @Mock 38 | private ApplicationContext applicationContext; 39 | 40 | @Mock 41 | private File file; 42 | 43 | @Mock 44 | private Resource resource; 45 | 46 | private static final String extraFields = "{\"entityName\":\"Customer\",\"version\":1,\"fields\":[{\"id\":1,\"type\":\"email\",\"name\":\"email\",\"value\":\"defaultemail@redfroggy.fr\",\"label\":\"Email\",\"required\":true,\"showAsColumn\":true},{\"id\":2,\"type\":\"number\",\"name\":\"age\",\"value\":\"28\",\"label\":\"Age\",\"required\":true,\"min\":5,\"max\":100},{\"id\":3,\"type\":\"text\",\"name\":\"company\",\"label\":\"Company\",\"required\":false,\"minLength\":3,\"maxLength\":10,\"showAsColumn\":true},{\"id\":4,\"type\":\"textarea\",\"name\":\"description\",\"label\":\"Description\",\"required\":true},{\"id\":5,\"type\":\"file\",\"name\":\"attachment\",\"label\":\"Attachment\",\"fileAccept\":\"application/pdf\"},{\"id\":6,\"type\":\"password\",\"name\":\"password\",\"label\":\"Password\",\"placeholder\":\"Ex: myp4ssw0rd\",\"pattern\":\"^[a-z0-9_-]{6,18}$\"},{\"id\":7,\"type\":\"select\",\"name\":\"roles\",\"label\":\"Roles\",\"required\":true,\"options\":[{\"id\":1,\"value\":\"Admin\"},{\"id\":2,\"value\":\"Manager\"},{\"id\":1,\"value\":\"User\"}]},{\"id\":8,\"type\":\"text\",\"name\":\"readable\",\"label\":\"Readable field\",\"value\":\"Readable value\",\"writable\":false},{\"id\":9,\"type\":\"date\",\"name\":\"birthDate\",\"label\":\"Birth date\",\"required\":true}]}"; 47 | 48 | private void mockResource() throws IOException { 49 | Mockito.when(resource.getFile()).thenReturn(file); 50 | Mockito.when(file.exists()).thenReturn(true); 51 | Mockito.when(resource.getFilename()).thenReturn("test.xml"); 52 | InputStream is = new ByteArrayInputStream(extraFields.getBytes()); 53 | Mockito.when(resource.getInputStream()).thenReturn(is); 54 | } 55 | 56 | @Before 57 | public void setUp() throws Exception { 58 | FormUtils.forms = new ArrayList<>(); 59 | mockResource(); 60 | Resource[] resources = new Resource[]{resource}; 61 | Mockito.when(applicationContext.getResources("classpath:*.json")).thenReturn(resources); 62 | reader.init(); 63 | } 64 | 65 | @Test 66 | public void jsonParsing() throws IOException { 67 | Form entity = (Form)TestUtil.fromJSON(extraFields, Form.class,false); 68 | Assert.assertNotNull(entity); 69 | } 70 | 71 | @Test 72 | public void getForm() throws IOException { 73 | Form userForm = FormUtils.describe(Customer.class,true); 74 | Assert.assertNotNull(userForm); 75 | } 76 | 77 | @Test 78 | public void getFormFields() throws IOException { 79 | List fields = FormUtils.getFields(Customer.class,true); 80 | Assert.assertNotNull(fields); 81 | Assert.assertTrue(!fields.isEmpty()); 82 | } 83 | 84 | @Test 85 | public void getFormField() throws IOException { 86 | String fieldName = "firstName"; 87 | Field formField = FormUtils.getField(Customer.class,fieldName); 88 | Assert.assertNotNull(formField); 89 | Assert.assertTrue(fieldName.equals(formField.getName())); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/fr/redfroggy/dynamicforms/rest/CustomerResourceTest.java: -------------------------------------------------------------------------------- 1 | package fr.redfroggy.dynamicforms.rest; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.JavaType; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import fr.redfroggy.dynamicforms.ApplicationTest; 8 | import fr.redfroggy.dynamicforms.TestUtil; 9 | import fr.redfroggy.dynamicforms.model.Customer; 10 | import fr.redfroggy.dynamicforms.utils.Form; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Ignore; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.test.SpringApplicationConfiguration; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.test.annotation.DirtiesContext; 20 | import org.springframework.test.context.ActiveProfiles; 21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 22 | import org.springframework.test.context.web.WebAppConfiguration; 23 | import org.springframework.test.web.servlet.MockMvc; 24 | import org.springframework.test.web.servlet.MvcResult; 25 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 26 | import org.springframework.web.context.WebApplicationContext; 27 | 28 | import javax.servlet.Filter; 29 | import java.io.IOException; 30 | import java.util.HashMap; 31 | import java.util.List; 32 | import java.util.Map; 33 | 34 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 35 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 36 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 37 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 38 | 39 | /** 40 | * Customer resource test 41 | * Created by Michael DESIGAUD on 04/03/2016. 42 | */ 43 | @RunWith(SpringJUnit4ClassRunner.class) 44 | @SpringApplicationConfiguration(classes = ApplicationTest.class) 45 | @WebAppConfiguration 46 | @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) 47 | @ActiveProfiles("test") 48 | @SuppressWarnings("unchecked") 49 | public class CustomerResourceTest { 50 | 51 | @Autowired 52 | private WebApplicationContext context; 53 | 54 | //Mock to consume spring mvc rest controllers 55 | private MockMvc mockMVC; 56 | 57 | private ObjectMapper objectMapper; 58 | 59 | @Before 60 | public void setup() { 61 | this.objectMapper = new ObjectMapper(); 62 | this.mockMVC = MockMvcBuilders 63 | .webAppContextSetup(this.context) 64 | .build(); 65 | } 66 | 67 | @Test 68 | public void queryCustomers() throws Exception { 69 | MvcResult mvcResult = mockMVC.perform(get("/api/customers", false) 70 | //Content 71 | .contentType(MediaType.APPLICATION_JSON))// 72 | //Fin content 73 | //Assertions 74 | .andExpect(status().isOk()).andReturn(); 75 | 76 | Assert.assertNotNull(mvcResult); 77 | 78 | List customers = ( List) TestUtil.fromJSON(mvcResult.getResponse().getContentAsString(),Customer.class,true); 79 | Assert.assertNotNull(customers); 80 | Assert.assertTrue(!customers.isEmpty()); 81 | } 82 | 83 | @Test 84 | public void getCustomer() throws Exception { 85 | String customerId = "1"; 86 | MvcResult mvcResult = mockMVC.perform(get("/api/customers/"+customerId, false) 87 | //Content 88 | .contentType(MediaType.APPLICATION_JSON))// 89 | //Fin content 90 | //Assertions 91 | .andExpect(status().isOk()).andReturn(); 92 | 93 | Assert.assertNotNull(mvcResult); 94 | 95 | Customer customer = (Customer) TestUtil.fromJSON(mvcResult.getResponse().getContentAsString(),Customer.class,false); 96 | Assert.assertNotNull(customer); 97 | Assert.assertEquals(customer.getId(),Long.valueOf(customerId)); 98 | Assert.assertNotNull(customer.getFirstName()); 99 | Assert.assertNotNull(customer.getLastName()); 100 | Assert.assertNull(customer.getExtraFields()); 101 | } 102 | 103 | @Test 104 | public void saveCustomer() throws Exception { 105 | 106 | Customer customer = new Customer("Test","test"); 107 | 108 | MvcResult mvcResult = mockMVC.perform(post("/api/customers", false) 109 | //Content 110 | .contentType(MediaType.APPLICATION_JSON) 111 | .content(TestUtil.toJSON(customer)))// 112 | //Fin content 113 | //Assertions 114 | .andExpect(status().isOk()).andReturn(); 115 | 116 | Assert.assertNotNull(mvcResult); 117 | 118 | Customer savedCustomer = (Customer) TestUtil.fromJSON(mvcResult.getResponse().getContentAsString(),Customer.class,false); 119 | Assert.assertNotNull(savedCustomer); 120 | Assert.assertNotNull(savedCustomer.getId()); 121 | Assert.assertEquals(savedCustomer.getFirstName(),customer.getFirstName()); 122 | Assert.assertEquals(savedCustomer.getLastName(),customer.getLastName()); 123 | Assert.assertNull(savedCustomer.getExtraFields()); 124 | } 125 | 126 | @Test 127 | public void saveCustomerWithExtraFields() throws Exception { 128 | 129 | Map extraFields = new HashMap<>(); 130 | extraFields.put("email","michael.desigaud@redfroggy.fr"); 131 | extraFields.put("age","29"); 132 | 133 | Customer customer = new Customer("Test","test"); 134 | customer.setExtraFields(extraFields); 135 | 136 | MvcResult mvcResult = mockMVC.perform(post("/api/customers", false) 137 | //Content 138 | .contentType(MediaType.APPLICATION_JSON) 139 | .content(TestUtil.toJSON(customer)))// 140 | //Fin content 141 | //Assertions 142 | .andExpect(status().isOk()).andReturn(); 143 | 144 | Assert.assertNotNull(mvcResult); 145 | 146 | Customer savedCustomer = (Customer) TestUtil.fromJSON(mvcResult.getResponse().getContentAsString(),Customer.class,false); 147 | Assert.assertNotNull(savedCustomer); 148 | Assert.assertNotNull(savedCustomer.getId()); 149 | Assert.assertEquals(savedCustomer.getFirstName(),customer.getFirstName()); 150 | Assert.assertEquals(savedCustomer.getLastName(),customer.getLastName()); 151 | Assert.assertNotNull(savedCustomer.getExtraFields()); 152 | Assert.assertEquals(savedCustomer.getExtraFields(),extraFields); 153 | } 154 | 155 | @Test 156 | public void updateCustomer() throws Exception { 157 | 158 | Customer customer = new Customer("Test","test"); 159 | customer.setId(1L); 160 | 161 | MvcResult mvcResult = mockMVC.perform(put("/api/customers", false) 162 | //Content 163 | .contentType(MediaType.APPLICATION_JSON) 164 | .content(TestUtil.toJSON(customer)))// 165 | //Fin content 166 | //Assertions 167 | .andExpect(status().isOk()).andReturn(); 168 | 169 | Assert.assertNotNull(mvcResult); 170 | 171 | Customer savedCustomer = (Customer) TestUtil.fromJSON(mvcResult.getResponse().getContentAsString(),Customer.class,false); 172 | Assert.assertNotNull(savedCustomer); 173 | Assert.assertNotNull(savedCustomer.getId()); 174 | Assert.assertEquals(savedCustomer.getFirstName(),customer.getFirstName()); 175 | Assert.assertEquals(savedCustomer.getLastName(),customer.getLastName()); 176 | Assert.assertNull(savedCustomer.getExtraFields()); 177 | } 178 | 179 | @Test 180 | public void getCustomerForm() throws Exception { 181 | MvcResult mvcResult = mockMVC.perform(get("/api/customers/form", false) 182 | //Content 183 | .contentType(MediaType.APPLICATION_JSON))// 184 | //Fin content 185 | //Assertions 186 | .andExpect(status().isOk()).andReturn(); 187 | 188 | Assert.assertNotNull(mvcResult); 189 | 190 | Form customerForm = (Form) TestUtil.fromJSON(mvcResult.getResponse().getContentAsString(),Form.class,false); 191 | Assert.assertNotNull(customerForm); 192 | Assert.assertNotNull(customerForm.getFields()); 193 | Assert.assertTrue(!customerForm.getFields().isEmpty()); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/test/resources/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "entityName":"Customer", 3 | "version":1, 4 | "fields":[ 5 | { 6 | "id":1, 7 | "type":"email", 8 | "name":"email", 9 | "value":"defaultemail@redfroggy.fr", 10 | "label":"Email", 11 | "required":true, 12 | "showAsColumn":true 13 | }, 14 | { 15 | "id":2, 16 | "type":"number", 17 | "name":"age", 18 | "value":"28", 19 | "label":"Age", 20 | "required":true, 21 | "min":5, 22 | "max":100 23 | }, 24 | { 25 | "id":3, 26 | "type":"text", 27 | "name":"company", 28 | "label":"Company", 29 | "required":false, 30 | "minLength":3, 31 | "maxLength":10, 32 | "showAsColumn":true 33 | }, 34 | { 35 | "id":4, 36 | "type":"textarea", 37 | "name":"description", 38 | "label":"Description", 39 | "required":true 40 | }, 41 | { 42 | "id":5, 43 | "type":"file", 44 | "name":"attachment", 45 | "label":"Attachment", 46 | "fileAccept":"application/pdf" 47 | }, 48 | { 49 | "id":6, 50 | "type":"password", 51 | "name":"password", 52 | "label":"Password", 53 | "placeholder":"Ex: myp4ssw0rd", 54 | "pattern":"^[a-z0-9_-]{6,18}$" 55 | }, 56 | { 57 | "id":7, 58 | "type":"select", 59 | "name":"roles", 60 | "label":"Roles", 61 | "required":true, 62 | "options":[ 63 | { 64 | "id":1, 65 | "value":"Admin" 66 | }, 67 | { 68 | "id":2, 69 | "value":"Manager" 70 | }, 71 | { 72 | "id":1, 73 | "value":"User" 74 | } 75 | ] 76 | }, 77 | { 78 | "id":8, 79 | "type":"text", 80 | "name":"readable", 81 | "label":"Readable field", 82 | "value":"Readable value", 83 | "writable":false 84 | }, 85 | { 86 | "id":9, 87 | "type":"date", 88 | "name":"birthDate", 89 | "label":"Birth date", 90 | "required":true 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noEmitOnError": false, 7 | "rootDir": ".", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "sourceRoot": "src/main/webapp/app", 12 | "inlineSourceMap": false, 13 | "inlineSources": false 14 | }, 15 | "exclude": [ 16 | "src/main/resources/static/node_modules" 17 | ], 18 | "compileOnSave": true 19 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "curly": false, 5 | "eofline": true, 6 | "indent": "spaces", 7 | "max-line-length": [ 8 | true, 9 | 200 10 | ], 11 | "member-ordering": [ 12 | true, 13 | "public-before-private", 14 | "static-before-instance", 15 | "variables-before-functions" 16 | ], 17 | "no-arg": true, 18 | "no-construct": true, 19 | "no-duplicate-key": true, 20 | "no-duplicate-variable": true, 21 | "no-empty": false, 22 | "no-eval": true, 23 | "no-trailing-comma": true, 24 | "no-trailing-whitespace": true, 25 | "no-unused-expression": true, 26 | "no-unused-variable": false, 27 | "no-unreachable": true, 28 | "no-use-before-declare": true, 29 | "one-line": [ 30 | true, 31 | "check-open-brace", 32 | "check-catch", 33 | "check-else", 34 | "check-whitespace" 35 | ], 36 | "quotemark": [ 37 | true, 38 | "single" 39 | ], 40 | "semicolon": true, 41 | "triple-equals": [ 42 | true, 43 | "allow-null-check" 44 | ], 45 | "variable-name": false 46 | } 47 | } --------------------------------------------------------------------------------