'admin::form.hasmany', 'tab' => 'admin::form.hasmanytab', 'table' => 'admin::form.hasmanytable', ]; /** * Options for template. * * @var array */ protected $options = [ 'allowCreate' => true, 'allowDelete' => true, ]; protected $columnClass; /** * Create a new HasMany field instance. * * @param $relationName * @param array $arguments */ public function __construct($relationName, $arguments = []) { $this->relationName = $relationName; $this->column = $relationName; $this->columnClass = $this->formatClass($relationName); if (count($arguments) == 1) { $this->label = $this->formatLabel(); $this->builder = $arguments[0]; } if (count($arguments) == 2) { [$this->label, $this->builder] = $arguments; } } protected function formatClass(string $column) { return str_replace('.', '-', $column); } /** * Get validator for this field. * * @param array $input * * @return bool|Validator */ public function getValidator(array $input) { if (! Arr::has($input, $this->column)) { return false; } $form = $this->buildNestedForm(); $rules = $attributes = $messages = []; /* @var Field $field */ foreach ($form->fields() as $field) { if (! $fieldRules = $field->getRules()) { continue; } if ($field instanceof File) { $fieldRules = is_string($fieldRules) ? explode('|', $fieldRules) : $fieldRules; Helper::deleteByValue($fieldRules, ['image', 'file']); } $column = $field->column(); if (is_array($column)) { foreach ($column as $key => $name) { $rules[$name.$key] = $fieldRules; } $this->resetInputKey($input, $column); } else { $rules[$column] = $fieldRules; } $attributes = array_merge( $attributes, $this->formatValidationAttribute($input, $field->label(), $column) ); $messages = array_merge( $messages, $this->formatValidationMessages($input, $field->getValidationMessages()) ); } Arr::forget($rules, NestedForm::REMOVE_FLAG_NAME); if (empty($rules)) { return false; } $newRules = []; $newInput = []; foreach ($rules as $column => $rule) { foreach (array_keys(Arr::get($input, $this->column)) as $key) { if (Arr::get($input, "{$this->column}.{$key}.".NestedForm::REMOVE_FLAG_NAME)) { continue; } $newRules["{$this->column}.$key.$column"] = $rule; $ruleValue = Arr::get($input, "{$this->column}.$key.$column"); if (is_array($ruleValue)) { foreach ($ruleValue as $vkey => $value) { $newInput["{$this->column}.$key.{$column}$vkey"] = $value; } } } } if (empty($newInput)) { $newInput = $input; } return Validator::make($newInput, $newRules, array_merge($this->getValidationMessages(), $messages), $attributes); } /** * Format validation messages. * * @param array $input * @param array $messages * * @return array */ protected function formatValidationMessages(array $input, array $messages) { $result = []; foreach (Arr::get($input, $this->column) as $key => &$value) { $newKey = $this->column.'.'.$key; foreach ($messages as $k => $message) { $result[$newKey.'.'.$k] = $message; } } return $result; } /** * Format validation attributes. * * @param array $input * @param string $label * @param string $column * * @return array */ protected function formatValidationAttribute($input, $label, $column) { $new = $attributes = []; if (is_array($column)) { foreach ($column as $index => $col) { $new[$col.$index] = $col; } } foreach (array_keys(Arr::dot($input)) as $key) { if (is_string($column)) { if (Str::endsWith($key, ".$column")) { $attributes[$key] = $label; } } else { foreach ($new as $k => $val) { if (Str::endsWith($key, ".$k")) { $attributes[$key] = $label."[$val]"; } } } } return $attributes; } /** * Reset input key for validation. * * @param array $input * @param array $column $column is the column name array set * * @return void. */ protected function resetInputKey(array &$input, array $column) { /** * flip the column name array set. * * for example, for the DateRange, the column like as below * * ["start" => "created_at", "end" => "updated_at"] * * to: * * [ "created_at" => "start", "updated_at" => "end" ] */ $column = array_flip($column); /** * $this->column is the inputs array's node name, default is the relation name. * * So... $input[$this->column] is the data of this column's inputs data * * in the HasMany relation, has many data/field set, $set is field set in the below */ foreach (Arr::get($input, $this->column) as $index => $set) { /* * foreach the field set to find the corresponding $column */ foreach ($set as $name => $value) { /* * if doesn't have column name, continue to the next loop */ if (! array_key_exists($name, $column)) { continue; } /** * example: $newKey = created_atstart. * * Σ( ° △ °|||)︴ * * I don't know why a form need range input? Only can imagine is for range search.... */ $newKey = $name.$column[$name]; /* * set new key */ Arr::set($input, "{$this->column}.$index.$newKey", $value); /* * forget the old key and value */ Arr::forget($input, "{$this->column}.$index.$name"); } } } /** * Prepare input data for insert or update. * * @param array $input * * @return array */ protected function prepareInputValue($input) { $form = $this->buildNestedForm(); return array_values( $form->setOriginal($this->original, $this->getKeyName())->prepare($input) ); } /** * Build a Nested form. * * @param null $key * * @return NestedForm */ public function buildNestedForm($key = null) { $form = new Form\NestedForm($this->column, $key); $form->setForm($this->form); call_user_func($this->builder, $form); $form->hidden($this->getKeyName()); $form->hidden(NestedForm::REMOVE_FLAG_NAME)->default(0)->addElementClass(NestedForm::REMOVE_FLAG_CLASS); return $form; } /** * Get the HasMany relation key name. * * @return string */ public function getKeyName() { if (is_null($this->form)) { return; } return $this->relationKeyName; } /** * @param string $relationKeyName */ public function setRelationKeyName(?string $relationKeyName) { $this->relationKeyName = $relationKeyName; return $this; } /** * Set view mode. * * @param string $mode currently support `tab` mode. * * @return $this * * @author Edwin Hui */ public function mode($mode) { $this->viewMode = $mode; return $this; } /** * Use tab mode to showing hasmany field. * * @return HasMany */ public function useTab() { return $this->mode('tab'); } /** * Use table mode to showing hasmany field. * * @return HasMany */ public function useTable() { return $this->mode('table'); } /** * Build Nested form for related data. * * @throws \Exception * * @return array */ protected function buildRelatedForms() { if (is_null($this->form)) { return []; } $forms = []; /* * If redirect from `exception` or `validation error` page. * * Then get form data from session flash. * * Else get data from database. */ foreach (Helper::array($this->value()) as $idx => $data) { $key = Arr::get($data, $this->getKeyName(), $idx); $forms[$key] = $this->buildNestedForm($key) ->fill($data); } return $forms; } /** * Disable create button. * * @return $this */ public function disableCreate() { $this->options['allowCreate'] = false; return $this; } /** * Disable delete button. * * @return $this */ public function disableDelete() { $this->options['allowDelete'] = false; return $this; } public function value($value = null) { if ($value === null) { return Helper::array(parent::value($value)); } return parent::value($value); } /** * Render the `HasMany` field. * * @throws \Exception * * @return \Illuminate\View\View|string */ public function render() { if (! $this->shouldRender()) { return ''; } if ($this->viewMode == 'table') { return $this->renderTable(); } // specify a view to render. $this->view = $this->views[$this->viewMode]; [$template, $script] = $this->buildNestedForm() ->getTemplateHtmlAndScript(); $this->addVariables([ 'forms' => $this->buildRelatedForms(), 'template' => $template, 'relationName' => $this->relationName, 'options' => $this->options, 'templateScript' => $script, 'count' => count($this->value()), 'columnClass' => $this->columnClass, ]); return parent::render(); } /** * Render the `HasMany` field for table style. * * @throws \Exception * * @return mixed */ protected function renderTable() { $headers = []; $fields = []; $hidden = []; $scripts = []; /* @var Field $field */ foreach ($this->buildNestedForm()->fields() as $field) { $field->runScript(false); if (is_a($field, Hidden::class)) { $hidden[] = $field->render(); } else { /* Hide label and set field width 100% */ $field->setLabelClass(['hidden']); $field->width(12, 0); $fields[] = $field->render(); $headers[] = $field->label(); } /* * Get and remove the last script of Admin::$script stack. */ if ($script = $field->getScript()) { $scripts[] = $script; } } /* Build row elements */ $template = array_reduce($fields, function ($all, $field) { $all .= "